# 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