Skip to content
This repository has been archived by the owner on Sep 25, 2020. It is now read-only.

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahZinsmeister authored and moodysalem committed Sep 24, 2020
0 parents commit f707a46
Show file tree
Hide file tree
Showing 20 changed files with 9,016 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sol linguist-language=Solidity
21 changes: 21 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Tests

on:
push:
branches:
- master
pull_request:

jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
- run: yarn
- run: yarn lint
- run: yarn test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
build/
6 changes: 6 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extension": ["ts"],
"spec": "./test/**/*.spec.ts",
"require": "ts-node/register",
"timeout": 12000
}
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120
}
1 change: 1 addition & 0 deletions .yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore-scripts true
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# liquidity-staker

Forked from
[https://github.com/Synthetixio/synthetix/tree/v2.27.2/](https://github.com/Synthetixio/synthetix/tree/v2.27.2/)

12 changes: 12 additions & 0 deletions contracts/RewardsDistributionRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pragma solidity ^0.5.16;

contract RewardsDistributionRecipient {
address public rewardsDistribution;

function notifyRewardAmount(uint256 reward) external;

modifier onlyRewardsDistribution() {
require(msg.sender == rewardsDistribution, "Caller is not RewardsDistribution contract");
_;
}
}
166 changes: 166 additions & 0 deletions contracts/StakingRewards.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
pragma solidity ^0.5.16;

import "openzeppelin-solidity-2.3.0/contracts/math/Math.sol";
import "openzeppelin-solidity-2.3.0/contracts/math/SafeMath.sol";
import "openzeppelin-solidity-2.3.0/contracts/token/ERC20/ERC20Detailed.sol";
import "openzeppelin-solidity-2.3.0/contracts/token/ERC20/SafeERC20.sol";
import "openzeppelin-solidity-2.3.0/contracts/utils/ReentrancyGuard.sol";

// Inheritance
import "./interfaces/IStakingRewards.sol";
import "./RewardsDistributionRecipient.sol";

contract StakingRewards is IStakingRewards, RewardsDistributionRecipient, ReentrancyGuard {
using SafeMath for uint256;
using SafeERC20 for IERC20;

/* ========== STATE VARIABLES ========== */

IERC20 public rewardsToken;
IERC20 public stakingToken;
uint256 public periodFinish = 0;
uint256 public rewardRate = 0;
uint256 public rewardsDuration = 60 days;
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;

mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;

uint256 private _totalSupply;
mapping(address => uint256) private _balances;

/* ========== CONSTRUCTOR ========== */

constructor(
address _rewardsDistribution,
address _rewardsToken,
address _stakingToken
) public {
rewardsToken = IERC20(_rewardsToken);
stakingToken = IERC20(_stakingToken);
rewardsDistribution = _rewardsDistribution;
}

/* ========== VIEWS ========== */

function totalSupply() external view returns (uint256) {
return _totalSupply;
}

function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}

function lastTimeRewardApplicable() public view returns (uint256) {
return Math.min(block.timestamp, periodFinish);
}

function rewardPerToken() public view returns (uint256) {
if (_totalSupply == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored.add(
lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply)
);
}

function earned(address account) public view returns (uint256) {
return _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account])).div(1e18).add(rewards[account]);
}

function getRewardForDuration() external view returns (uint256) {
return rewardRate.mul(rewardsDuration);
}

/* ========== MUTATIVE FUNCTIONS ========== */

function stakeWithPermit(uint256 amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
_totalSupply = _totalSupply.add(amount);
_balances[msg.sender] = _balances[msg.sender].add(amount);

// permit
IUniswapV2ERC20(address(stakingToken)).permit(msg.sender, address(this), amount, deadline, v, r, s);

stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}

function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
_totalSupply = _totalSupply.add(amount);
_balances[msg.sender] = _balances[msg.sender].add(amount);
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}

function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot withdraw 0");
_totalSupply = _totalSupply.sub(amount);
_balances[msg.sender] = _balances[msg.sender].sub(amount);
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

function getReward() public nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}

function exit() external {
withdraw(_balances[msg.sender]);
getReward();
}

/* ========== RESTRICTED FUNCTIONS ========== */

function notifyRewardAmount(uint256 reward) external onlyRewardsDistribution updateReward(address(0)) {
if (block.timestamp >= periodFinish) {
rewardRate = reward.div(rewardsDuration);
} else {
uint256 remaining = periodFinish.sub(block.timestamp);
uint256 leftover = remaining.mul(rewardRate);
rewardRate = reward.add(leftover).div(rewardsDuration);
}

// Ensure the provided reward amount is not more than the balance in the contract.
// This keeps the reward rate in the right range, preventing overflows due to
// very high values of rewardRate in the earned and rewardsPerToken functions;
// Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
uint balance = rewardsToken.balanceOf(address(this));
require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");

lastUpdateTime = block.timestamp;
periodFinish = block.timestamp.add(rewardsDuration);
emit RewardAdded(reward);
}

/* ========== MODIFIERS ========== */

modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}

/* ========== EVENTS ========== */

event RewardAdded(uint256 reward);
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
}

interface IUniswapV2ERC20 {
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
}
77 changes: 77 additions & 0 deletions contracts/StakingRewardsFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
pragma solidity ^0.5.16;

import 'openzeppelin-solidity-2.3.0/contracts/token/ERC20/IERC20.sol';
import 'openzeppelin-solidity-2.3.0/contracts/ownership/Ownable.sol';

import './StakingRewards.sol';

contract StakingRewardsFactory is Ownable {
// immutables
address public rewardsToken;
uint public stakingRewardsGenesis;

// the staking tokens for which the rewards contract has been deployed
address[] public stakingTokens;

// info about rewards for a particular staking token
struct StakingRewardsInfo {
address stakingRewards;
uint rewardAmount;
}

// rewards info by staking token
mapping(address => StakingRewardsInfo) public stakingRewardsInfoByStakingToken;

constructor(
address _rewardsToken,
uint _stakingRewardsGenesis
) Ownable() public {
require(_stakingRewardsGenesis >= block.timestamp, 'StakingRewardsFactory::constructor: genesis too soon');

rewardsToken = _rewardsToken;
stakingRewardsGenesis = _stakingRewardsGenesis;
}

///// permissioned functions

// deploy a staking reward contract for the staking token, and store the reward amount
// the reward will be distributed to the staking reward contract no sooner than the genesis
function deploy(address stakingToken, uint rewardAmount) public onlyOwner {
StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
require(info.stakingRewards == address(0), 'StakingRewardsFactory::deploy: already deployed');

info.stakingRewards = address(new StakingRewards(/*_rewardsDistribution=*/ address(this), rewardsToken, stakingToken));
info.rewardAmount = rewardAmount;
stakingTokens.push(stakingToken);
}

///// permissionless functions

// call notifyRewardAmount for all staking tokens.
function notifyRewardAmounts() public {
require(stakingTokens.length > 0, 'StakingRewardsFactory::notifyRewardAmounts: called before any deploys');
for (uint i = 0; i < stakingTokens.length; i++) {
notifyRewardAmount(stakingTokens[i]);
}
}

// notify reward amount for an individual staking token.
// this is a fallback in case the notifyRewardAmounts costs too much gas to call for all contracts
function notifyRewardAmount(address stakingToken) public {
require(block.timestamp >= stakingRewardsGenesis, 'StakingRewardsFactory::notifyRewardAmount: not ready');

StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
require(info.stakingRewards != address(0), 'StakingRewardsFactory::notifyRewardAmount: not deployed');

if (info.rewardAmount > 0) {
uint rewardAmount = info.rewardAmount;
info.rewardAmount = 0;

require(
IERC20(rewardsToken).transfer(info.stakingRewards, rewardAmount),
'StakingRewardsFactory::notifyRewardAmount: transfer failed'
);
StakingRewards(info.stakingRewards).notifyRewardAmount(rewardAmount);
}
}
}
27 changes: 27 additions & 0 deletions contracts/interfaces/IStakingRewards.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
pragma solidity >=0.4.24;


interface IStakingRewards {
// Views
function lastTimeRewardApplicable() external view returns (uint256);

function rewardPerToken() external view returns (uint256);

function earned(address account) external view returns (uint256);

function getRewardForDuration() external view returns (uint256);

function totalSupply() external view returns (uint256);

function balanceOf(address account) external view returns (uint256);

// Mutative

function stake(uint256 amount) external;

function withdraw(uint256 amount) external;

function getReward() external;

function exit() external;
}
11 changes: 11 additions & 0 deletions contracts/test/TestERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity =0.5.16;

import "openzeppelin-solidity-2.3.0/contracts/token/ERC20/ERC20Detailed.sol";
import "openzeppelin-solidity-2.3.0/contracts/token/ERC20/ERC20Mintable.sol";

contract TestERC20 is ERC20Detailed, ERC20Mintable {
constructor(uint amount) ERC20Detailed('Test ERC20', 'TEST', 18) public {
mint(msg.sender, amount);
}
}
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@uniswap/liquidity-staker",
"version": "1.0.2",
"author": "Noah Zinsmeister",
"repository": {
"type": "git",
"url": "https://github.com/Uniswap/liquidity-staker"
},
"files": [
"build"
],
"engines": {
"node": ">=10"
},
"scripts": {
"precompile": "rimraf ./build/",
"compile": "waffle",
"pretest": "yarn compile",
"test": "mocha",
"lint": "prettier ./test/**/*.ts --check",
"prepublishOnly": "yarn test"
},
"dependencies": {
"openzeppelin-solidity-2.3.0": "npm:[email protected]"
},
"devDependencies": {
"@types/chai": "^4.2.12",
"@types/mocha": "^8.0.3",
"@uniswap/v2-core": "^1.0.1",
"chai": "^4.2.0",
"ethereum-waffle": "^3.1.0",
"ethereumjs-util": "^7.0.5",
"mocha": "^8.1.3",
"prettier": "^2.1.1",
"rimraf": "^3.0.2",
"solc": "0.5.16",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
}
}
Loading

0 comments on commit f707a46

Please sign in to comment.