---
# System prepended metadata

title: KrypoCamp 第三週作業

---

# 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