# KrypoCamp 第三週作業 ### 作業目的 部署可質押(staking)且有回饋(reward)的智能合約 ### 基礎作業 1. 透過 Remix JavaScript VM 部署合約 2. 部署任一自訂 ERC20 Token 3. 部署質押合約 - Deposit(存入) - Reward(回饋) - TimeLock(固定鎖倉期) #### ERC20 ``` solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract StakingToken is ERC20 { uint256 private _coin = 50000; constructor() ERC20("Jason Staking Token", "JST") {} function mint() public { _mint(msg.sender, _coin ); } } ``` #### Bank ``` solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract BaseBank { //質押代幣ERC20的interface IERC20 private _stakingToken; //質押的總數量 uint256 private _totalSupply = 0; //個人質押數量 mapping( address => uint256 ) private _balanceOf; //鎖質押 10 秒 uint256 private _lockBaseTime = 10 seconds; //鎖質押的開始時間 mapping( address => uint256 ) private _lockTimeStart; //鎖質押的結束時間 mapping( address => uint256 ) private _lockTimeEnd; //回饋獎勵 uint256 private _rewardRate = 1; //個人回饋的總獎勵 mapping( address => uint256 ) private _rewardOf; //建構子 constructor( IERC20 stakingToken ) { _stakingToken = stakingToken; } //存入多少代幣 function deposit( uint256 coin ) public { //轉給合約多少代幣 _stakingToken.transferFrom( msg.sender, address(this), coin ); //加總合約代幣 _totalSupply += coin; //加總合約的個人代幣 _balanceOf[ msg.sender ] += coin; //設定個人存入的開始時間 _lockTimeStart[ msg.sender ] = block.timestamp; //設定個人存入的結束時間 _lockTimeEnd[ msg.sender ] = block.timestamp + _lockBaseTime; } function withdraw( uint256 coin ) external { //判斷取出時間是否大於存入的結束時間 require( block.timestamp >= _lockTimeEnd[ msg.sender ] , "lock time..." ); //判斷個人目前在合約的代幣是否比取出的代幣多 require( _balanceOf[ msg.sender ] >= coin, "withdraw too much" ); //加總個人的回饋獎勵 _rewardOf[ msg.sender ] += getReward(); //從合約轉移代幣給個人 _stakingToken.transfer( msg.sender, coin ); //合約的代幣減少總量 _totalSupply -= coin; //個人的合約代幣減少總量 _balanceOf[ msg.sender ] -= coin; } //回饋方式 function getReward() public view returns ( uint256 ) { uint256 reward = block.timestamp - _lockTimeEnd[ msg.sender ]; return reward * _rewardRate * _balanceOf[ msg.sender ]; } //查看個人目前總回饋量 function getTotalRewardOf( address owner ) public view returns ( uint256 ) { return _rewardOf[ owner ]; } //查看目前合約代幣總量 function getTotalSupply() public view returns( uint256 ){ return _totalSupply; } //查看目前個人在合約的代幣量 function getBalanceOf( address owner ) public view returns( uint256 ){ return _balanceOf[ owner ]; } } ``` > 每次存入代幣會重新設定鎖倉時間,接下來的進階會解決此問題 --- ### 進階作業 1. 透過 remix Injected Web3 部署至 Rinkeby 2. 部署任一自訂 ERC20 Token 3. 部署質押合約 - Deposit(存入) - Reward(回饋) - TimeLock(根據鎖倉期給予不同回饋加乘) #### ERC20 ``` solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract StakingToken is ERC20 { uint256 private _coin = 50000; constructor() ERC20("Jason Staking Token", "JST") {} function mint() public { _mint(msg.sender, _coin ); } } ``` #### Bank ``` solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract AdvancedBank { //質押代幣ERC20的interface IERC20 private _stakingToken; //質押的總數量 uint256 private _totalSupply = 0; //個人質押數量 mapping( address => uint256 ) private _balanceOf; //鎖質押 10 秒 uint256 private _lockBaseTime = 10 seconds; // 質押資訊 struct Deposit { uint256 coin; uint256 startTime; uint256 endTime; } mapping(address => Deposit[]) private _depositOf; //回饋獎勵 uint256 private _rewardRate = 1; //個人回饋的總獎勵 mapping( address => uint256 ) private _rewardOf; //建構子 constructor( IERC20 stakingToken ) { _stakingToken = stakingToken; } //存入多少代幣 function deposit( uint256 coin ) public { //轉給合約多少代幣 _stakingToken.transferFrom( msg.sender, address(this), coin ); //加總合約代幣 _totalSupply += coin; //加總合約的個人代幣 _balanceOf[ msg.sender ] += coin; //設定個人存入的資料 _depositOf[ msg.sender ].push( Deposit( { coin: coin, startTime: block.timestamp, endTime: block.timestamp + _lockBaseTime } ) ); } function withdraw( uint256 idx ) external { // memory 傳遞值 // storage 傳遞指標 Deposit[] storage deposits = _depositOf[ msg.sender ]; uint256 coin; //判斷個人目前在合約的代幣是否比取出的代幣多 require( idx < deposits.length, "withdraw too much" ); //判斷取出時間是否大於存入的結束時間 require( block.timestamp >= deposits[idx].endTime , "lock time..." ); coin = deposits[idx].coin; //從合約轉移代幣給個人 _stakingToken.transfer( msg.sender, coin ); //合約的代幣減少總量 _totalSupply -= coin; //個人的合約代幣減少總量 _balanceOf[ msg.sender ] -= coin; //提領指定編號的定存後,將資料移除,避免無限擴增 uint256 lastOne = deposits.length - 1; deposits[ idx ] = deposits[ lastOne ]; deposits.pop(); } //回饋方式 function getReward( address owner, uint256 idx ) public view returns ( uint256 ) { Deposit[] storage deposits = _depositOf[ owner ]; require( idx < deposits.length, "withdraw too much" ); uint256 reward = block.timestamp - deposits[idx].endTime; return reward * _rewardRate * deposits[idx].coin; } //回饋方式 function getRewards( address owner ) public view returns ( uint256 ) { uint256 sum = 0; for( uint i = 0 ; i < _depositOf[ owner ].length ; i++ ) { sum += getReward( owner, i ); } return sum; } //查看個人存入資訊 function getDepositOf( address owner, uint idx ) public view returns ( uint256 coin, uint256 startTime, uint256 endTime ){ Deposit[] storage deposits = _depositOf[ owner ]; require( idx < deposits.length, "withdraw too much" ); return ( deposits[idx].coin, deposits[idx].startTime, deposits[idx].endTime ); } //查看個人目前總回饋量 function getTotalRewardOf( address owner ) public view returns ( uint256 ) { return getRewards( owner ); } //查看目前合約代幣總量 function getTotalSupply() public view returns( uint256 ){ return _totalSupply; } //查看目前個人在合約的代幣量 function getBalanceOf( address owner ) public view returns( uint256 ){ return _balanceOf[ owner ]; } } ``` > function getRewards( address owner ) public view returns ( uint256 ) > 如果存入量大,用for迴圈會消耗大量gas ### 終極作業 1. 透過 remix Injected Web3 部署至 Rinkeby 2. 部署任兩種自訂 ERC20 Token(一個質押,一個回饋) 3. 部署質押合約 - Deposit(存入) - Reward(回饋另一個 Token) - TimeLock(根據鎖倉期給予不同回饋加乘) 4. 利用範例 AMM 交易所進行 Reward Token 的 ETH 兌換 5. Rinkeby Testnet Network - Rewards Token: 0xCF84692c39d9Dd72e3286042C07484C2c296A263 - Exchange : 0x2526a943c571f058C6a4B6e298157D9c3fe5E6bb > 第三週作業最後的部分,大家可以到 Exchange 換 Rewards Token,然後再繳交 HackMD 的時候附上自己的錢包地址即可。 #### ERC20 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract StakingToken is ERC20 { constructor() ERC20("Staking Token", "ST") {} // 1 wei -> 1 coin function mint() external payable { _mint(msg.sender, msg.value); } } contract RewardsToken is ERC20 { constructor() ERC20("Rewards Token", "ST") { _mint(msg.sender, 100 * 1e18); } } ``` #### Bank ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract FinalBank { // 質押代幣 IERC20 public stakingToken; // 獎勵代幣 IERC20 public rewardsToken; // 質押數量 uint256 public totalSupply; // 個人質押數量 mapping(address => uint256) public balanceOf; // 鎖倉時間 uint256 public lockDuration = 10 seconds; // 質押資訊 struct Deposit { uint256 amount; uint256 start; uint256 end; } mapping(address => Deposit[]) public depositOf; // 獎勵 uint256 public rewardRate = 1; // uint256 public lastUpdateTime; 錯誤,只記錄到單人 mapping(address => uint256) public lastUpdateTime; mapping(address => uint256) public rewardOf; constructor(IERC20 _stakingToken, IERC20 _rewardsToken) { stakingToken = _stakingToken; rewardsToken = _rewardsToken; } // 計算獎勵 function earned() public view returns (uint256) { uint256 duration = block.timestamp - lastUpdateTime[msg.sender]; return balanceOf[msg.sender] * duration * rewardRate + rewardOf[msg.sender]; } // 更新獎勵 modifier updateReward() { rewardOf[msg.sender] = earned(); lastUpdateTime[msg.sender] = block.timestamp; _; } // 定存 function deposit(uint256 _amount) external updateReward { stakingToken.transferFrom(msg.sender, address(this), _amount); totalSupply += _amount; balanceOf[msg.sender] += _amount; depositOf[msg.sender].push( Deposit({ amount: _amount, start: block.timestamp, end: block.timestamp + lockDuration }) ); } // 解除定存 function withdraw(uint256 _depositId) external updateReward { Deposit[] storage deposits = depositOf[msg.sender]; uint256 amount = deposits[_depositId].amount; require(_depositId < deposits.length, "Deposit ID does not exist"); require(block.timestamp >= deposits[_depositId].end, "withdraw too soon"); stakingToken.transfer(msg.sender, amount); totalSupply -= amount; balanceOf[msg.sender] -= amount; // remove deposit uint lastOne = deposits.length - 1; deposits[_depositId] = deposits[lastOne]; deposits.pop(); } // 提取獎勵 function getReward() external updateReward { uint reward = rewardOf[msg.sender]; rewardOf[msg.sender] = 0; rewardsToken.transfer(msg.sender, reward); } } ``` #### Exchange ```solidity= //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "hardhat/console.sol"; contract Exchange is ERC20 { IERC20 public token; constructor(IERC20 _token) ERC20("Exchange", "EX") { token = _token; } function addLiquidity( uint256 minLiquidity, uint256 maxTokens, uint256 deadline ) public payable returns (uint256) { uint256 liquidity = totalSupply(); if (liquidity > 0) { uint256 ethReserve = address(this).balance - msg.value; uint256 tokenReserve = token.balanceOf(address(this)); uint256 tokenAmount = (msg.value * tokenReserve) / ethReserve + 1; uint256 liquidityMinted = (msg.value * liquidity) / ethReserve; require( maxTokens >= tokenAmount && liquidityMinted >= minLiquidity, "LMT" ); _mint(msg.sender, liquidityMinted); token.transferFrom(msg.sender, address(this), tokenAmount); return totalSupply(); } else { uint256 initialLiquidity = address(this).balance; _mint(msg.sender, initialLiquidity); token.transferFrom(msg.sender, address(this), maxTokens); return totalSupply(); } } function getInputPrice( uint256 inputAmount, uint256 inputReserve, uint256 outputReserve ) internal pure returns (uint256) { require(inputReserve > 0 && outputReserve > 0, "LIO"); uint256 inputAmountWithFee = inputAmount * 997; uint256 numerator = inputAmountWithFee * outputReserve; uint256 denominator = (inputReserve * 1000) + inputAmountWithFee; return numerator / denominator; } function ethToTokenTransferInput(uint256 minTokens) public payable returns (uint256) { uint256 ethSold = msg.value; uint256 tokenReserve = token.balanceOf(address(this)); uint256 inputReserve = address(this).balance - ethSold; uint256 tokensBought = getInputPrice( ethSold, inputReserve, tokenReserve ); token.transfer(msg.sender, tokensBought); require(tokensBought >= minTokens, "LMT"); return tokensBought; } } ``` ### 延伸讀物 - [Lootex DAO - TimeLockNonTransferablePool](https://bscscan.com/address/0x750fc63264c0d08472387da13e39428be4a7892d#code) - [Hakka - HakkaRewardsVesting](https://etherscan.io/address/0xF4D1F9674c8e9f29A69DC2E6f841292e675B7977#code) MetaMask : 0xf8789AB568eC6155Eb3F9056144346590dbF14cc