MasterChef在SushiSwap中处于核心地位,用户可以通过它进行流动性挖矿。
MasterChef在SushiSwap中处于核心地位,用户可以通过它进行流动性挖矿。MasterChef中包含两个主要的数据结构:UserInfo和PoolInfo
1.1、UserInfo
struct UserInfo {
uint256 amount; // How many LP tokens the user has provided.
uint256 rewardDebt; // Reward debt. See explanation below.
}
amount是用户质押的LPToken数量,rewardDebt代表用户已经获取的奖励数。
struct PoolInfo {
IERC20 lpToken; // Address of LP token contract.
uint256 allocPoint; // How many allocation points assigned to this pool. SUSHIs to distribute per block.
uint256 lastRewardBlock; // Last block number that SUSHIs distribution occurs.
uint256 accSushiPerShare; // Accumulated SUSHIs per share, times 1e12. See below.
}
lpToken是ERC20标准代币,SushiSwap最初的LPToken是Uniswap的流动性,Uniswap质押后生成的流动性其实是UniswapPair的代币,SushiSwap将UniswapPair的地址设置到pool里,就可以将Uniswap的流动性进行质押操作。后来SushiSwap完成了一次迁移,LPToken就从Uniswap的流动性代币变成了SushiSwap的流动性代币。
allocPoint是质押池的分配比例,lastRewardBlock是上一次分配奖励的区块数。
accSushiPerShare是质押一个LPToken的全局收益,用户依赖这个计算实际收益,原理很简单,用户在质押LPToken的时候,会把当前accSushiPerShare记下来作为起始点位,当解除质押的时候,可以通过最新的accSushiPerShare减去起始点位,就可以得到用户实际的收益。
1.3、其他数据结构
SushiToken public sushi;
// Dev address.
address public devaddr;
// Block number when bonus SUSHI period ends.
uint256 public bonusEndBlock;
// SUSHI tokens created per block.
uint256 public sushiPerBlock;
// Bonus muliplier for early sushi makers.
uint256 public constant BONUS_MULTIPLIER = 10;
// The migrator contract. It has a lot of power. Can only be set through governance (owner).
IMigratorChef public migrator;
// Info of each pool.
PoolInfo[] public poolInfo;
// Info of each user that stakes LP tokens.
mapping(uint256 => mapping(address => UserInfo)) public userInfo;
// Total allocation poitns. Must be the sum of all allocation points in all pools.
uint256 public totalAllocPoint = 0;
// The block number when SUSHI mining starts.
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是开始区块
constructor(
SushiToken _sushi,
address _devaddr,
uint256 _sushiPerBlock,
uint256 _startBlock,
uint256 _bonusEndBlock
) public {
sushi = _sushi;
devaddr = _devaddr;
sushiPerBlock = _sushiPerBlock;
bonusEndBlock = _bonusEndBlock;
startBlock = _startBlock;
}
MasterChef初始化传入sushi代币的地址(值为0x6b3595068778dd592e39a122f4f5a5cf09c90fe2),开发者地址(值为0xe94b5eec1fa96ceecbd33ef5baa8d00e4493f4f3 ),每个块分配sushi的数量(值为100*1e18),奖励结束区块(值为10850000)以及开始区块(值为10750000)
function add(
uint256 _allocPoint,
IERC20 _lpToken,
bool _withUpdate
) public onlyOwner {
if (_withUpdate) {
massUpdatePools();
}
uint256 lastRewardBlock =
block.number > startBlock ? block.number : startBlock;
totalAllocPoint = totalAllocPoint.add(_allocPoint);
poolInfo.push(
PoolInfo({
lpToken: _lpToken,
allocPoint: _allocPoint,
lastRewardBlock: lastRewardBlock,
accSushiPerShare: 0
})
);
}
代码很简单,生成一个poolInfo然后加入数组中,然后更新totalAllocPoint,其中_allocPoint是指这个池质押挖矿的比例。比如totalAllocPoint是10000,_allocPoint是100,每个区块一共挖100个,那么每个区块这个池分配到的就是100*(100/10000) = 1
本方法只有管理员可以执行,因为有onlyOwner这个modifier
function set(
uint256 _pid,
uint256 _allocPoint,
bool _withUpdate
) public onlyOwner {
if (_withUpdate) {
massUpdatePools();
}
totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(
_allocPoint
);
poolInfo[_pid].allocPoint = _allocPoint;
}
目前来看只能修改质押挖矿的分配比例,也是只有管理员可以执行。
function setMigrator(IMigratorChef _migrator) public onlyOwner {
migrator = _migrator;
}
function migrate(uint256 _pid) public {
require(address(migrator) != address(0), "migrate: no migrator");
PoolInfo storage pool = poolInfo[_pid];
IERC20 lpToken = pool.lpToken;
uint256 bal = lpToken.balanceOf(address(this));
lpToken.safeApprove(address(migrator), bal);
IERC20 newLpToken = migrator.migrate(lpToken);
require(bal == newLpToken.balanceOf(address(this)), "migrate: bad");
pool.lpToken = newLpToken;
}
管理员会先设置迁移器,然后针对单个质押池进行迁移。迁移流程先对迁移器进行授权(safeApprove),后面执行由migrator控制,migrator会返回一个新的LPToken,然后重置质押池。下面看看sushi的migrator是怎么操作的:
function migrate(IUniswapV2Pair orig) public returns (IUniswapV2Pair) {
require(msg.sender == chef, "not from master chef");
require(block.number >= notBeforeBlock, "too early to migrate");
require(orig.factory() == oldFactory, "not from old factory");
address token0 = orig.token0();
address token1 = orig.token1();
IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1));
if (pair == IUniswapV2Pair(address(0))) {
pair = IUniswapV2Pair(factory.createPair(token0, token1));
}
uint256 lp = orig.balanceOf(msg.sender);
if (lp == 0) return pair;
desiredLiquidity = lp;
//用户的流动性还给Uniswap
orig.transferFrom(msg.sender, address(orig), lp);
//Uniswap把质押的代币交易对给到sushiswap的pair
orig.burn(address(pair));
//sushiswap给用户发放流动性
pair.mint(msg.sender);
desiredLiquidity = uint256(-1);
return pair;
}
正如上文所述,sushiswap一开始借助的是uniswap的流动性,因此上面的lpToken传过来的其实是UniswapPair,然后通过UniswapPair拿到具体的交易对里的两个token,然后在sushi中创建SushiSwapPair(都是IUniswapV2Pair接口的实现类),然后将用户在Uniswap的流动性赎回(先转给Uniswap,然后调用burn,注意这里burn的对象是pair,这样Uniswap会把两个质押token还到SushiSwapPair的地址),最后调用SushiSwapPair的mint给用户增发SushiSwapPair的流动性,从而完成用户流动性的迁移。
function updatePool(uint256 _pid) public {
PoolInfo storage pool = poolInfo[_pid];
if (block.number <= pool.lastRewardBlock) {
return;
}
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (lpSupply == 0) {
pool.lastRewardBlock = block.number;
return;
}
uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
uint256 sushiReward =
multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
totalAllocPoint
);
sushi.mint(devaddr, sushiReward.div(10));
sushi.mint(address(this), sushiReward);
pool.accSushiPerShare = pool.accSushiPerShare.add(
sushiReward.mul(1e12).div(lpSupply)
);
pool.lastRewardBlock = block.number;
}
首先会计算质押池lpToken的数量,如果为0,就只更新lastRewardBlock。否则会先计算一个乘数multiplier
// Return reward multiplier over the given _from to _to block.
function getMultiplier(uint256 _from, uint256 _to)
public
view
returns (uint256)
{
if (_to <= bonusEndBlock) {
return _to.sub(_from).mul(BONUS_MULTIPLIER);
} else if (_from >= bonusEndBlock) {
return _to.sub(_from);
} else {
return
bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add(
_to.sub(bonusEndBlock)
);
}
}
这里的计算是为了兼容bonusEndBlock,如果是to小于bonusEndBlock,说明质押池完全处于奖励挖矿阶段,会乘以一个倍数BONUS_MULTIPLIER,如果from大于bonusEndBlock,说明质押池完全没参与奖励挖矿,所以简单的to-from就可以了。最后一个else是处理质押池部分参与奖励挖矿,部分是结束后的常规挖矿。
multiplier的计算是从lastRewardBlock到当前区块的奖励区块数,获取multiplier后开始计算这一段时间的sushiReward
uint256 sushiReward =
multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
totalAllocPoint
);
multiplier*sushiPerBlock是总的sushi奖励,pool.allocPoint/totalAllocPoint是当前质押池的分配比例。
接下来给开发者地址分配10%的sushiReward作为手续费,然后总的sushiReward分配给当前质押池。
然后计算一下accSushiPerShare进行累加。
最后更新lastRewardBlock。
function pendingSushi(uint256 _pid, address _user)
external
view
returns (uint256)
{
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][_user];
uint256 accSushiPerShare = pool.accSushiPerShare;
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (block.number > pool.lastRewardBlock && lpSupply != 0) {
uint256 multiplier =
getMultiplier(pool.lastRewardBlock, block.number);
uint256 sushiReward =
multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(
totalAllocPoint
);
accSushiPerShare = accSushiPerShare.add(
sushiReward.mul(1e12).div(lpSupply)
);
}
return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt);
}
前面所有的逻辑都在更新当前质押池的最新收益,逻辑和updatePool类似,但是不执行mint,仅仅是逻辑上计算。最后一行通过通过用户质押的ammount乘以accSushiPerShare,得到理论上用户一共获得的sushi数量,然后减去用户实际已经获得的sushi数量rewardDebt,就是剩余还未获得的数据。
function deposit(uint256 _pid, uint256 _amount) public {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];
updatePool(_pid);
if (user.amount > 0) {
uint256 pending =
user.amount.mul(pool.accSushiPerShare).div(1e12).sub(
user.rewardDebt
);
safeSushiTransfer(msg.sender, pending);
}
pool.lpToken.safeTransferFrom(
address(msg.sender),
address(this),
_amount
);
user.amount = user.amount.add(_amount);
user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12);
emit Deposit(msg.sender, _pid, _amount);
}
先更新了质押池收益,然后计算用户未获得的sushi收益(如果用户之前已经质押了),将这些收益转到用户账户。然后将用户的LPToken转移给质押池,最后更新用户质押的LPToken数量,将最新的amount*accSushiPerShare设置为rewardDebt,这一步操作其实就是设置了一个用户奖励的起始点位,而上面的pendingSushi的计算恰恰依赖这个起始点位。
function withdraw(uint256 _pid, uint256 _amount) public {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];
require(user.amount >= _amount, "withdraw: not good");
updatePool(_pid);
uint256 pending =
user.amount.mul(pool.accSushiPerShare).div(1e12).sub(
user.rewardDebt
);
safeSushiTransfer(msg.sender, pending);
user.amount = user.amount.sub(_amount);
user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12);
pool.lpToken.safeTransfer(address(msg.sender), _amount);
emit Withdraw(msg.sender, _pid, _amount);
}
先更新了质押池收益,然后计算用户未获得的sushi收益,将这些收益转到用户账户,然后更新rewardDebt,最后把LPToken还给用户。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!