👨🌾
Hop ExchangeのStake実装調査メモ(StakingRewards.sol)
コントラクト例
Compiler Version
- v0.5.16+commit.9c3226ce
アウトライン
StakingRewards
interface IERC20 {...}
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;
}
interface IUniswapV2ERC20 {...}
contract ERC20Detailed is IERC20 {...}
contract ReentrancyGuard {...}
contract RewardsDistributionRecipient {...}
contract StakingRewards is IStakingRewards, RewardsDistributionRecipient, ReentrancyGuard {...}
library Math {...}
library SafeMath {...}
library Address {...}
library SafeERC20 {...}
ユースケースごとの処理詳細
LPトークンを預ける (StakingRewards.stake)
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);
}
- updateRewardにて対象者の獲得報酬を更新する(後述)
- ステーク額を加算する
- LPトークンをユーザからコントラクトに送る。例のコントラクトでは
stakingToken
はHop USDC LP Tokenが設定されている
ステーク報酬を取得する (StakingRewards.getReward)
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);
}
}
- updateRewardにて対象者の獲得報酬を更新する(後述)
- 獲得報酬が存在したらユーザにすべて送る。例のコントラクトでは
rewardsToken
はWMATICが設定されている
LPトークンを一部引き出す (StakingRewards.withdraw)
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);
}
- updateRewardにて対象者の獲得報酬を更新する(後述)
- ステーク額を減算する
- トークンをコントラクトからユーザに送る
LPトークンをすべて引き出す (StakingRewards.exit)
function exit() external {
withdraw(_balances[msg.sender]);
getReward();
}
- 全額LPトークンを引き出す(前述の内容)
- ステーク報酬を獲得する(前述の内容)
獲得報酬額更新の仕組み(StakingRewards.updateReward)
関連コード
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (_totalSupply == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored.add(
lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply)
);
}
function lastTimeRewardApplicable() public view returns (uint256) {
return Math.min(block.timestamp, periodFinish);
}
function earned(address account) public view returns (uint256) {
return _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account])).div(1e18).add(rewards[account]);
}
- 報酬額は、決められている毎秒の報酬額をステークシェア率に応じて分配される
- R = 1秒間の報酬
- l(u,t) = ユーザuが時間tにステークしているトークンの量
- L(t) = 時間tにおけるステークされているトークンの総量
- スマートコントラクト上で動くように効率的な式に変換したものが以下(詳細は参考にある動画を参照)
具体例
- 秒間30の報酬を10秒間得られるとする
- reward: 300
- rewardRate: 30
- rewardsDuration: 10
毎秒計算した場合
user | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
_balances | earned | _balances | earned | _balances | earned | _totalSupply | ||||
t=1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |||
t=2 | 100 | 30 | 0 | 0 | 0 | 0 | 100 | |||
t=3 | 100 | 60 | 0 | 0 | 0 | 0 | 100 | |||
t=4 | 100 | 75 | 100 | 15 | 0 | 0 | 200 | |||
t=5 | 100 | 85 | 100 | 25 | 100 | 10 | 300 | |||
t=6 | 100 | 95 | 100 | 35 | 100 | 20 | 300 | |||
t=7 | 100 | 110 | 0 | 35 | 100 | 35 | 200 | |||
t=8 | 100 | 125 | 0 | 35 | 100 | 50 | 200 | |||
t=9 | 500 | 150 | 0 | 35 | 100 | 55 | 600 | |||
t=10 | 500 | 175 | 0 | 35 | 100 | 60 | 600 |
- t=1はステーキングがないためその分を除いた合計270になる
コントラクト実行した場合
- _totalSupplyは処理終了時の値
- _totalSupply以外はupdateReward内での変数代入時点の値
t | user | type | rewardPerTokenStored | rewards[account] | _totalSupply |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | ||
1 | stake | 0 | 0 | 100 | |
3 | stake | 60*10^16 | 0 | 200 | |
4 | stake | 75*10^16 | 0 | 300 | |
6 | exit | 95*10^16 | 35 | 200 | |
8 | stake | 125*10^16 | 125 | 600 | |
10 | exit | 135*10^16 | 175 | 100 | |
11 | exit | 135*10^16 | 60 | 0 |
- 合計獲得報酬は以下の通り毎秒計算した場合と一致
175 35 60
備考
-
Synthetixのステークもほぼ同じコード(HopがSynthetix等のコピーのはず)
-
notifyRewardAmount
コード
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); }
-
reward
をrewardsDuration
で割ってrewardRate
を設定する- 未分配の報酬がある場合はその値が
reward
に加算された値でrewardRate
が算出される
- 未分配の報酬がある場合はその値が
- 終了時刻は
block.timestamp
+rewardsDuration
に更新される
-
参考
Discussion