SUSHI的源码及方案解析一(早期的流动性挖矿部分)
前言
我的思路是这样的,如果把设备按功能分成不同的群体,那么群体内设备与设备之间通过token来完成信息的交互,这里最简单的例子就是基于预言机来完成信息的报价和购买,我们在 《关于预言机nest的源码解析及克隆》
里面已经对nest预言机部分的源码和克隆进行了解析,有兴趣的可以看一下。
接下来还有两个非常重要的内容需要解决。第一个是token是怎么来的,另一个是token之间是如何进行交互的。关于token是怎么来的,我们考虑的是模仿SUSHI的流动性挖矿来解决,而token之间是如何交易的,则模仿uniswap来完成。
本文首先对流动性挖矿(SUSHI)进行方案的解析。
特别说明,本文基于的是SUSHI早期的源码,那个时候交易所部分还没出来。
流动性挖矿(SUSHI)概述
流动性挖矿(SUSHI)很多人都知道开发者一个月赚了几千万的事情。我简单的从技术角度说一下。
SUSHI模仿YAM进行流动性挖矿,但创造性的用uniswap的令牌来完成。
主要的功能分四部分:
1.管理员建立uniswap令牌池
2.用户将自己的uniswap令牌(LP)存入令牌池
3.根据不同的令牌池权重和不同数量的用户LP存入币实时计算该用户应该获得的SUSHI
4.用户通过取功能获得SUSHI,以及存入的uniswap令牌。
5.将用户的LP令牌转移到SUSHI的去中心化交易所。
所以说核心函数其实就那么几个:
1.SUSHI的挖矿接口函数;
2.建立令牌池及修改令牌池(管理员权限)
3.用户存、取LP;
4.用户查看及取SUSHI;
5.将用户的LP令牌转移到SUSHI的去中心化交易所
Token.sol
这个里面erc20部分大部分内容来自于openzeppelin,而// SushiToken with Governance.部分主要来自于yam。
大家理解为一个简单的可以挖矿的erc20币即可。
这里面的关键代码就一句:
contract PITAYAToken is ERC20("PITAYAswap", "PITAYA"), Ownable {
这里的PITAYAToken是我自己改的名字。后面的PITAYAswap是项目的名字,PITAYA是货币符号。
也就是说,用这段代码克隆的同学,就改这三个地方即可。
chef.sol
这个里面没有什么要改的,但要理解。
这个里面看似合约很多,但其实主要关注的只有一个,MasterChef。
我们对照着前言里面提到的几个函数说明一下。
contract MasterChef is Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; // Info of each user. struct UserInfo { uint256 amount; // How many LP tokens the user has provided. uint256 rewardDebt; // Reward debt. See explanation below. } // Info of each pool. //每一个矿池的数据结构 struct PoolInfo { IERC20 lpToken; // Address of LP token contract.uniswap的令牌地址 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.sushi的配额 } // The SUSHI TOKEN! //sushi的token地址 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 = 2; // The migrator contract. It has a lot of power. Can only be set through governance (owner). //迁移地址,当sushi的去中心化交易所完成后进行的迁移 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; event Deposit(address indexed user, uint256 indexed pid, uint256 amount); event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); constructor( SushiToken _sushi, address _devaddr, uint256 _sushiPerBlock, uint256 _startBlock, uint256 _bonusEndBlock ) public { sushi = _sushi; devaddr = _devaddr; sushiPerBlock = _sushiPerBlock; bonusEndBlock = _bonusEndBlock; startBlock = _startBlock; } function poolLength() external view returns (uint256) { return poolInfo.length; } // Add a new lp to the pool. Can only be called by the owner. // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do. //增加矿池,管理员权限 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 })); } // Update the given pool's SUSHI allocation point. Can only be called by the owner. //设置矿池参数,管理员权限 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; } // Set the migrator contract. Can only be called by the owner. //设置迁移地址,管理员权限 function setMigrator(IMigratorChef _migrator) public onlyOwner { migrator = _migrator; } // Migrate lp token to another lp contract. Can be called by anyone. We trust that migrator contract is good. //进行迁移 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; } // 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) ); } } // View function to see pending SUSHIs on frontend. //获得矿池信息 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); } // Update reward vairables for all pools. Be careful of gas spending! //更新所有矿池信息 function massUpdatePools() public { uint256 length = poolInfo.length; for (uint256 pid = 0; pid < length; ++pid) { updatePool(pid); } } // Update reward variables of the given pool to be up-to-date. //更新某个矿池信息 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(20)); sushi.mint(address(this), sushiReward); pool.accSushiPerShare = pool.accSushiPerShare.add(sushiReward.mul(1e12).div(lpSupply)); pool.lastRewardBlock = block.number; } // Deposit LP tokens to MasterChef for SUSHI allocation. //存入LP 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); } // Withdraw LP tokens from MasterChef. //撤出令牌 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); } // Withdraw without caring about rewards. EMERGENCY ONLY. //无条件撤出令牌,用于矿池出现严重问题的时候 function emergencyWithdraw(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; pool.lpToken.safeTransfer(address(msg.sender), user.amount); emit EmergencyWithdraw(msg.sender, _pid, user.amount); user.amount = 0; user.rewardDebt = 0; } // Safe sushi transfer function, just in case if rounding error causes pool to not have enough SUSHIs. //安全转移sushi function safeSushiTransfer(address _to, uint256 _amount) internal { uint256 sushiBal = sushi.balanceOf(address(this)); if (_amount > sushiBal) { sushi.transfer(_to, sushiBal); } else { sushi.transfer(_to, _amount); } } // Update dev address by the previous dev. //设置管理员信息 function dev(address _devaddr) public { require(msg.sender == devaddr, "dev: wut?"); devaddr = _devaddr; } }
重新编译和布置sushi的智能合约
说明一下,这部分的内容当时笔记做的不太好,大家当参考即可。
另外,由于我当时做相关开发的时候,sushi网站的源码并没有开源,所以用的是banana的网站源码。也就是说, sushi智能合约源码+banana网站源码
一 Token的安装
Token的安装比较简单
记得在最后面把名字改一下就行。(建议用yuno的,对比banana),用的是banana的。
contract PITAYAToken is ERC20(“PITAYAswap”,”PITAYA”), Ownable {o:p
修改为Pitaya PITAYAToken (火龙果)
Token:0x9527bf8828a991537558f33d291271b206376412o:p
二 chief
chief则需要修改很多的内容,而且是一次性完成的。(用的banana)
A 需要修改这个BONUS_MULTIPLIER,变为2(表示最开始的发现是之后的2倍,sushi默认是10倍)
B 一次性需要完成的是下面几个
SushiToken地址,
devaddr地址(治理员,建议跟ower不能是一个,0x404331C629D53B500Ac06b596eaF533423A413a7)
sushiPerBlock(每块创建的寿司代币数量,40暂定,输入40000000000000000000)
bonusEndBlock(块结束时间,区块号,在这个后面就开始变少了,我们把这个时间设定为3个月左右10846048+600000=11446048)
也就是说,慢了10%。也就是说,真正持续的时间大概是三个月零3天。
startBlock(开始块的时间,开始才出口,预计设计为后天,10846048,12号九点)
Ps:devaddr的作用主要是用来分成的,每挖一次都要给额外的10%,也可以用来提前偷摸挖矿,但咱们不干这个。
Chief:0x6a6db5fe904366023a0c0a9e2cea29ed8226b415o:p
三 建立后需要完成的两件事情
建立后需要完成的两件事情,注意对比yuno和banana。
第一个是Token合约的转换权限(transferownership),换成chief合约地址。
第二个是chef合约里面建立pool0和1(add函数,就是y.js里面的前几个列表,根据banana来就行,区别是改banana的第一个),剩余的慢慢来。Set函数可以修改对应的值。
Pool0:2,0xb23b4b10099690ff7e9ebe16d94c25124f9c4d07
true
重新编译和运行banana的网站源码
基于banana的修改网站的代码,还有图片。这里记得要用yuno的对比改一下
一 修改
a var chefAddress = “0x6a6db5fe904366023a0c0a9e2cea29ed8226b415”;//改
b var tokenAddress = “0x9527bf8828a991537558f33d291271b206376412”;//改o:p
c [“0xb23b4b10099690ff7e9ebe16d94c25124f9c4d07”,
“UNISWAP NEST/ETH”, “https://uniswap.info/pair/0xb23b4b10099690ff7e9ebe16d94c25124f9c4d07”,
2, 0, 0],//改两个
*function getUniswapPrice() {
- var ctx0 =
new web3.eth.Contract(uniswapABI, pools[0][0]); - var ctx1 =
new web3.eth.Contract(uniswapABI, uni1);
var ctx2 = new
web3.eth.Contract(uniswapABI, “0xb23b4b10099690ff7e9ebe16d94c25124f9c4d07”);//改一个。
d
http-server
二 第一次使用
进入到so部分,然后第一次设置会显示需要授权。
授权后就可以存入,建议存入少量。
存入后刷新页面,几秒钟到几分钟在staked token就可以出现了。
然后再取出。几次就ok了。
三 启动测试与运行
http-serve
四 正常运行后的第三阶段
等稳定了后,做两件事情。(暂时不做,kimchi做了这个,yuno,hotdog都没有做这部分).特别说明,所谓的稳定是在测试网测试过,搞清楚executeTransaction怎么用。
A 建立timelock,时间设定为delay(7天),管理员admin为我自己。
B 将chef合约的owner更改为timelock地址(这里还有很多问题需要解决起码还没搞清楚executeTransaction怎么用)
C等完全都完事了,要部署GovernorAlpha,将timelock与挂接GovernorAlpha。就正式开启自治。
五自治后运行的事情
SushiSwap(SUSHI)发起流动性迁移提案#QmXm9T7,投票期限为 1 天。Timelock的管理者(本人)将部署所有必需的智能合约 Migrator、SushiSwapFactory(UniswapFactory)、SushiMaker 以及 SushiBar,并设置「setMigrator」功能来调用时间锁智能合约,将在 48 小时后完成迁移。一旦迁移完成,SushiSwap 将会借助自有的 AMM,把收取的交易费用的 1/6 分配给 SUSHI 代币持有者。
注意事项
一 分红从哪里来的。
按照sushi的逻辑就是,用sushi进行分红,可以分25%,但这个东西是从后面的交易所得来的。而后期进行转换的时候,则需要最开始的一段时间过去才行。
二 可能存在的风险
慢雾创始人余弦发文分析,SushiSwap 仿盘项目 KIMCHI (泡菜) 项目方确实拥有任意铸币的权限,只是如果项目方要任意铸币,至少需要等待2天时间。对接泡菜的平台可以观测泡菜厨师的devaddr地址是否变更为泡菜厨师的当前owner地址。
三 Migrator.sol
就是从旧的工厂合约迁移到新的工厂合约里面去,里面有一个时间点,少于这个时间点是不可以的。
四 部分源码说明
Solidity中当签名不匹配任何的函数方法时,将会触发回退函数。比如,当你调用address.call(bytes4(bytes32(sha3(“thisShouldBeAFunction(uint,bytes32)”))),
1, “test”)时,EVM实际尝试调用地址中的thisShouldBeAFunction(),当这个函数不存在,会触发fallback函数。
call函数转账的使用方法是:地址.call.value(转账金额)()o:p
五 令牌分红的本质
Uniswap 中的 LP Token,对于每一个交易对,Uniswap 合约会给流动性提供者发行相应的 LP Token,LP Token 是日后流动性提供者赎回本金的凭证,当交易产生手续费时,手续费会自动打到
Pool 中,此时如果价格不变的话,相当于单位 LP Token 所能赎回的本金变多了,通过这种方法完成分红。
六 布置合约的说明
A 布置UniswapV2Factory.sol合约,其中那个地址就是我自己的地址(管理者)
B 布置migrator(千万注意没有那个s),将UniswapV2Factory地址传输过去。还有一个旧地址,参考sushiswap里面的migraotr的oldfactory。布置将chef的owner转为timelock
C Timelock.sol、migrator和GovernorAlpha.sol是一体的,用于治理。o:p
D MasterChef里面有一个migrate合约,如果恶意放到不同的合约地址则有可能转走所有的币(在MasterChef里面的),Migrator合约的主要功能就是将uniswap里面的币转换为自己的池子里面并发新的币。这个里面需要注意的是,迁移的时间是需要大于一个值(这个值可以设计为很久),这样当set的时候,就可以由owner运行,而owner可以设计为可以换为另外一个,到时候则切换为GovernorAlpha投票进行即可
七 部分解析
- timelock,governoralpha,migrator不着急做,合一起就是用治理委员会完成迁移。但现在还没有启用。
- MasterChef的owner给了timelock,Timelock的管理员(admin)可以设置一次,之后就锁死了,目前设置为某一个人;pendingAdmin目前还没有设置(为0)
- 目前最主要的方案就是masterchef里面的迁移,这个需要在一个规定时间后(比如是3个月,在migrator的初始化里面设置),由masterchef的owner进行。因为这种权限太危险,所以要把这个owner权限给timelock,同时设定timelock的admin为自己(pendingAdmin待定)。
4.特别提示:
等正式运行稳定了了,到了一定的规模,就将GovernorAlpha的权限转给timelock。在此之前一定要进行足够的风险提示。
总结
我的开发项目是智能设备自治,其中有一个很关键的部分就是,物联网设备之前如何进行有价的信息交换。本文首先对流动性挖矿(SUSHI)进行方案的解析,进而解决token是怎么来的问题。
关于如何具体的怎么增加矿池修改矿池的流程,当时笔记就没再记录详细,就没写。而且相关的文件都是半年前写的,很多内容未做校对,错误很多,因此在这里,大家仅做参考。