当前位置: 首页 > 工具软件 > Sushi > 使用案例 >

SushiSwap的MasterChef解读--转载

贾兴学
2023-12-01

1、MasterChef的数据结构

MasterChef在SushiSwap中处于核心地位,用户可以通过它进行流动性挖矿。MasterChef中包含两个主要的数据结构:UserInfo和PoolInfo

1.1、UserInfo


   
   
  1. struct UserInfo {
  2. uint256 amount; // How many LP tokens the user has provided.
  3. uint256 rewardDebt; // Reward debt. See explanation below.
  4. //
  5. // We do some fancy math here. Basically, any point in time, the amount of SUSHIs
  6. // entitled to a user but is pending to be distributed is:
  7. //
  8. // pending reward = (user.amount * pool.accSushiPerShare) - user.rewardDebt
  9. //
  10. // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens:
  11. // 1. The pool's `accSushiPerShare` (and `lastRewardBlock`) gets updated.
  12. // 2. User receives the pending reward sent to his/her address.
  13. // 3. User's `amount` gets updated.
  14. // 4. User's `rewardDebt` gets updated.
  15. }

amount是用户质押的LPToken数量,rewardDebt代表用户已经获取的奖励数。

1.2、PoolInfo


   
   
  1. struct PoolInfo {
  2. IERC20 lpToken; // Address of LP token contract.
  3. uint256 allocPoint; // How many allocation points assigned to this pool. SUSHIs to distribute per block.
  4. uint256 lastRewardBlock; // Last block number that SUSHIs distribution occurs.
  5. uint256 accSushiPerShare; // Accumulated SUSHIs per share, times 1e12. See below.
  6. }

lpToken是ERC20标准代币,SushiSwap最初的LPToken是Uniswap的流动性,Uniswap质押后生成的流动性其实是UniswapPair的代币,SushiSwap将UniswapPair的地址设置到pool里,就可以将Uniswap的流动性进行质押操作。后来SushiSwap完成了一次迁移,LPToken就从Uniswap的流动性代币变成了SushiSwap的流动性代币。

allocPoint是质押池的分配比例,lastRewardBlock是上一次分配奖励的区块数。

accSushiPerShare是质押一个LPToken的全局收益,用户依赖这个计算实际收益,原理很简单,用户在质押LPToken的时候,会把当前accSushiPerShare记下来作为起始点位,当解除质押的时候,可以通过最新的accSushiPerShare减去起始点位,就可以得到用户实际的收益。

1.3、其他数据结构


   
   
  1. SushiToken public sushi;
  2. // Dev address.
  3. address public devaddr;
  4. // Block number when bonus SUSHI period ends.
  5. uint256 public bonusEndBlock;
  6. // SUSHI tokens created per block.
  7. uint256 public sushiPerBlock;
  8. // Bonus muliplier for early sushi makers.
  9. uint256 public constant BONUS_MULTIPLIER = 10;
  10. // The migrator contract. It has a lot of power. Can only be set through governance (owner).
  11. IMigratorChef public migrator;
  12. // Info of each pool.
  13. PoolInfo[] public poolInfo;
  14. // Info of each user that stakes LP tokens.
  15. mapping(uint256 => mapping(address => UserInfo)) public userInfo;
  16. // Total allocation poitns. Must be the sum of all allocation points in all pools.
  17. uint256 public totalAllocPoint = 0;
  18. // The block number when SUSHI mining starts.
  19. uint256 public startBlock;

sushi是一个ERC20代币,质押流动性获取的就是这种代币奖励。

devaddr是开发者地址,用于分配sushi奖励的手续费

bonusEndBlock,刚开始sushi是借Uniswap的流动性进行质押的,为了吸引用户,设置了一个奖励值乘数BONUS_MULTIPLIER和一个奖励截止区块bonusEndBlock,在bonusEndBlock前的奖励获得数量都会乘以10,这个区块后会执行迁移,迁移后就没有了加倍奖励,后续这个值也用不到了。

sushiPerBlock,每个区块挖出来的sushi的数量

migrator,迁移工具类,实现原理就是根据UniswapPair创建一个一模一样的SushiSwapPair,然后用用户的UniswapPair流动性赎回交易对(比如USDT/DAI),然后将交易对在SushiSwapPair中添加,获取SushiSwapPair的流动性代币,最后将SushiSwapPair的流动性代币进行质押。

totalAllocPoint是总共分配的点数

startBlock是开始区块

 

2、构造函数


   
   
  1. constructor(
  2. SushiToken _sushi,
  3. address _devaddr,
  4. uint256 _sushiPerBlock,
  5. uint256 _startBlock,
  6. uint256 _bonusEndBlock
  7. ) public {
  8. sushi = _sushi;
  9. devaddr = _devaddr;
  10. sushiPerBlock = _sushiPerBlock;
  11. bonusEndBlock = _bonusEndBlock;
  12. startBlock = _startBlock;
  13. }

MasterChef初始化传入sushi代币的地址(值为0x6b3595068778dd592e39a122f4f5a5cf09c90fe2),开发者地址(值为0xe94b5eec1fa96ceecbd33ef5baa8d00e4493f4f3 ),每个块分配sushi的数量(值为100*1e18),奖励结束区块(值为10850000)以及开始区块(值为10750000)

 

3、添加质押池


   
   
  1. function add(
  2. uint256 _allocPoint,
  3. IERC20 _lpToken,
  4. bool _withUpdate
  5. ) public onlyOwner {
  6. if (_withUpdate) {
  7. massUpdatePools();
  8. }
  9. uint256 lastRewardBlock =
  10. block.number > startBlock ? block.number : startBlock;
  11. totalAllocPoint = totalAllocPoint.add(_allocPoint);
  12. poolInfo.push(
  13. PoolInfo({
  14. lpToken: _lpToken,
  15. allocPoint: _allocPoint,
  16. lastRewardBlock: lastRewardBlock,
  17. accSushiPerShare: 0
  18. })
  19. );
  20. }

代码很简单,生成一个poolInfo然后加入数组中,然后更新totalAllocPoint,其中_allocPoint是指这个池质押挖矿的比例。比如totalAllocPoint是10000,_allocPoint是100,每个区块一共挖100个,那么每个区块这个池分配到的就是100*(100/10000) = 1

本方法只有管理员可以执行,因为有onlyOwner这个modifier

 

4、修改质押池参数


   
   
  1. function set(
  2. uint256 _pid,
  3. uint256 _allocPoint,
  4. bool _withUpdate
  5. ) public onlyOwner {
  6. if (_withUpdate) {
  7. massUpdatePools();
  8. }
  9. totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(
  10. _allocPoint
  11. );
  12. poolInfo[_pid].allocPoint = _allocPoint;
  13. }

目前来看只能修改质押挖矿的分配比例,也是只有管理员可以执行。

 

5、执行迁移


   
   
  1. function setMigrator(IMigratorChef _migrator) public onlyOwner {
  2. migrator = _migrator;
  3. }
  4. function migrate(uint256 _pid) public {
  5. require(address(migrator) != address( 0), "migrate: no migrator");
  6. PoolInfo storage pool = poolInfo[_pid];
  7. IERC20 lpToken = pool.lpToken;
  8. uint256 bal = lpToken.balanceOf(address(this));
  9. lpToken.safeApprove(address(migrator), bal);
  10. IERC20 newLpToken = migrator.migrate(lpToken);
  11. require(bal == newLpToken.balanceOf(address(this)), "migrate: bad");
  12. pool.lpToken = newLpToken;
  13. }

管理员会先设置迁移器,然后针对单个质押池进行迁移。迁移流程先对迁移器进行授权(safeApprove),后面执行由migrator控制,migrator会返回一个新的LPToken,然后重置质押池。下面看看sushi的migrator是怎么操作的:


   
   
  1. function migrate(IUniswapV2Pair orig) public returns (IUniswapV2Pair) {
  2. require(msg.sender == chef, "not from master chef");
  3. require(block.number >= notBeforeBlock, "too early to migrate");
  4. require(orig.factory() == oldFactory, "not from old factory");
  5. address token0 = orig.token0();
  6. address token1 = orig.token1();
  7. IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1));
  8. if (pair == IUniswapV2Pair(address( 0))) {
  9. pair = IUniswapV2Pair(factory.createPair(token0, token1));
  10. }
  11. uint256 lp = orig.balanceOf(msg.sender);
  12. if (lp == 0) return pair;
  13. desiredLiquidity = lp;
  14. //用户的流动性还给Uniswap
  15. orig.transferFrom(msg.sender, address(orig), lp);
  16. //Uniswap把质押的代币交易对给到sushiswap的pair
  17. orig.burn(address(pair));
  18. //sushiswap给用户发放流动性
  19. pair.mint(msg.sender);
  20. desiredLiquidity = uint256( -1);
  21. return pair;
  22. }

正如上文所述,sushiswap一开始借助的是uniswap的流动性,因此上面的lpToken传过来的其实是UniswapPair,然后通过UniswapPair拿到具体的交易对里的两个token,然后在sushi中创建SushiSwapPair(都是IUniswapV2Pair接口的实现类),然后将用户在Uniswap的流动性赎回(先转给Uniswap,然后调用burn,注意这里burn的对象是pair,这样Uniswap会把两个质押token还到SushiSwapPair的地址),最后调用SushiSwapPair的mint给用户增发SushiSwapPair的流动性,从而完成用户流动性的迁移。

6、更新质押池收益


   
   
  1. function updatePool(uint256 _pid) public {
  2. PoolInfo storage pool = poolInfo[_pid];
  3. if (block.number <= pool.lastRewardBlock) {
  4. return;
  5. }
  6. uint256 lpSupply = pool.lpToken.balanceOf(address(this));
  7. if (lpSupply == 0) {
  8. pool.lastRewardBlock = block.number;
  9. return;
  10. }
  11. uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
  12. uint256 sushiReward =
  13. multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
  14. totalAllocPoint
  15. );
  16. sushi.mint(devaddr, sushiReward.div( 10));
  17. sushi.mint(address(this), sushiReward);
  18. pool.accSushiPerShare = pool.accSushiPerShare.add(
  19. sushiReward.mul( 1e12).div(lpSupply)
  20. );
  21. pool.lastRewardBlock = block.number;
  22. }

首先会计算质押池lpToken的数量,如果为0,就只更新lastRewardBlock。否则会先计算一个乘数multiplier


   
   
  1. // Return reward multiplier over the given _from to _to block.
  2. function getMultiplier(uint256 _from, uint256 _to)
  3. public
  4. view
  5. returns (uint256)
  6. {
  7. if (_to <= bonusEndBlock) {
  8. return _to.sub(_from).mul(BONUS_MULTIPLIER);
  9. } else if (_from >= bonusEndBlock) {
  10. return _to.sub(_from);
  11. } else {
  12. return
  13. bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add(
  14. _to.sub(bonusEndBlock)
  15. );
  16. }
  17. }

这里的计算是为了兼容bonusEndBlock,如果是to小于bonusEndBlock,说明质押池完全处于奖励挖矿阶段,会乘以一个倍数BONUS_MULTIPLIER,如果from大于bonusEndBlock,说明质押池完全没参与奖励挖矿,所以简单的to-from就可以了。最后一个else是处理质押池部分参与奖励挖矿,部分是结束后的常规挖矿。

multiplier的计算是从lastRewardBlock到当前区块的奖励区块数,获取multiplier后开始计算这一段时间的sushiReward


   
   
  1. uint256 sushiReward =
  2. multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
  3. totalAllocPoint
  4. );

multiplier*sushiPerBlock是总的sushi奖励,pool.allocPoint/totalAllocPoint是当前质押池的分配比例。

接下来给开发者地址分配10%的sushiReward作为手续费,然后总的sushiReward分配给当前质押池。

然后计算一下accSushiPerShare进行累加。

最后更新lastRewardBlock。

7、查看用户质押收益


   
   
  1. function pendingSushi(uint256 _pid, address _user)
  2. external
  3. view
  4. returns (uint256)
  5. {
  6. PoolInfo storage pool = poolInfo[_pid];
  7. UserInfo storage user = userInfo[_pid][_user];
  8. uint256 accSushiPerShare = pool.accSushiPerShare;
  9. uint256 lpSupply = pool.lpToken.balanceOf(address(this));
  10. if (block.number > pool.lastRewardBlock && lpSupply != 0) {
  11. uint256 multiplier =
  12. getMultiplier(pool.lastRewardBlock, block.number);
  13. uint256 sushiReward =
  14. multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
  15. totalAllocPoint
  16. );
  17. accSushiPerShare = accSushiPerShare.add(
  18. sushiReward.mul( 1e12).div(lpSupply)
  19. );
  20. }
  21. return user.amount.mul(accSushiPerShare).div( 1e12).sub(user.rewardDebt);
  22. }

前面所有的逻辑都在更新当前质押池的最新收益,逻辑和updatePool类似,但是不执行mint,仅仅是逻辑上计算。最后一行通过通过用户质押的ammount乘以accSushiPerShare,得到理论上用户一共获得的sushi数量,然后减去用户实际已经获得的sushi数量rewardDebt,就是剩余还未获得的数据。

8、用户质押LPToken进行挖矿


   
   
  1. function deposit(uint256 _pid, uint256 _amount) public {
  2. PoolInfo storage pool = poolInfo[_pid];
  3. UserInfo storage user = userInfo[_pid][msg.sender];
  4. updatePool(_pid);
  5. if (user.amount > 0) {
  6. uint256 pending =
  7. user.amount.mul(pool.accSushiPerShare).div( 1e12).sub(
  8. user.rewardDebt
  9. );
  10. safeSushiTransfer(msg.sender, pending);
  11. }
  12. pool.lpToken.safeTransferFrom(
  13. address(msg.sender),
  14. address(this),
  15. _amount
  16. );
  17. user.amount = user.amount.add(_amount);
  18. user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div( 1e12);
  19. emit Deposit(msg.sender, _pid, _amount);
  20. }

先更新了质押池收益,然后计算用户未获得的sushi收益(如果用户之前已经质押了),将这些收益转到用户账户。然后将用户的LPToken转移给质押池,最后更新用户质押的LPToken数量,将最新的amount*accSushiPerShare设置为rewardDebt,这一步操作其实就是设置了一个用户奖励的起始点位,而上面的pendingSushi的计算恰恰依赖这个起始点位。

9、解除质押


   
   
  1. function withdraw(uint256 _pid, uint256 _amount) public {
  2. PoolInfo storage pool = poolInfo[_pid];
  3. UserInfo storage user = userInfo[_pid][msg.sender];
  4. require(user.amount >= _amount, "withdraw: not good");
  5. updatePool(_pid);
  6. uint256 pending =
  7. user.amount.mul(pool.accSushiPerShare).div( 1e12).sub(
  8. user.rewardDebt
  9. );
  10. safeSushiTransfer(msg.sender, pending);
  11. user.amount = user.amount.sub(_amount);
  12. user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div( 1e12);
  13. pool.lpToken.safeTransfer(address(msg.sender), _amount);
  14. emit Withdraw(msg.sender, _pid, _amount);
  15. }

先更新了质押池收益,然后计算用户未获得的sushi收益,将这些收益转到用户账户,然后更新rewardDebt,最后把LPToken还给用户。

 类似资料: