# 第五屆QC筆試正式考題解答 Ryan|C0051203 **及格線:6題/10題** ## Solidity 基礎 **1. 如何在部屬合約時,指定 owner,以程式碼舉例** ```solidity= contract exam { address private owner; constructor(){ owner = msg.sender; // asigna owner to who create this contract } } ``` 當「合約部署」時,在 constructor 內宣告 owner = msg.sender constructor 在部署合約時會執行裡面的內容。 **2. 請列出 ERC721 safeMint & mint 兩者的差異?** safeMint可以防止有人將 ERC721 鑄造到不支持 ERC721 傳輸的合約中。所以 ERC721 代幣永遠卡在那裡。如果您確定不會發生這種情況,您可以直接使用_mint以節省 gas 成本。 當接收方為合約賬戶時,可以使用safeXXX方法( safeMint或者safeTransferFrom )進行交易的安全校驗。如果是普通賬戶的話,兩者的執行沒有區別。 **3. 試寫出對於操作 ERC20 代幣時,safeTransfer 的功能以及需要使用的時機** safeTransfer可以安全的處理ERC20轉賬,解決非標準ERC20的問題 當項目方solidity有問題或是不是按照ERC20的標準時,則可以使用openzeppelin提供的SafeERC20提供的safeTransfer方法 ```solidity= import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/SafeERC20.sol"; import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol"; contract TestContract { using SafeERC20 for IERC20; function safeInteractWithToken(uint256 sendAmount) external { IERC20 token = IERC20(address(this)); token.safeTransferFrom(msg.sender, address(this), sendAmount); } } ``` **4. 試寫出兩種錯誤處理(require, revert),並解釋兩種的不同** 回答寫在這裡 require:可以加上條件判斷(狀態檢查),會退回沒使用到的 Gas Fee,通常用來檢查不嚴重的錯誤 revert:與 require 相同,但無法進行狀態檢查 另外補充第三種 `assert()`,用來檢查較為嚴重的錯誤,==不會退回 Gas Fee== 以下是範例語法 ```solidity= require(owner != msg.sender, "You are not owner!"); if (owner != msg.sender) { revert("You are not owner!"); } ``` **5. 試寫出 transfer, call 的不同** - transfer - 會限制 gas 花費 2,300,可防止重入攻擊 - call - 沒有 Gas 限制 - 可自定義 gas limit 等,可執行更複雜的邏輯 - 會回傳 boolean 來判斷交易是否成功 - 如果失敗會回傳 false,不會終止執行(不會自動回滾) - call 較為底層,使用須小心重入攻擊 ```solidity= // call (bool sent, bytes memory data) = _to.call{value: msg.value}(""); // transfer _to.transfer(msg.value); ``` **6.承上題,transfer 與 ERC20 的 transfer 有何不同?** 回答寫在這裡 - ERC20 的 transfer 是轉移 ERC20 的 Token - 原生 transfer 是轉移 Eth **7. 以下哪個選項為較安全產生隨機數?** - A:block.timestamp - B:block.hash - C:Chainlink VRF 預言機 答案是==C== 隨機數必須從鏈下數據產生,否則易有資安風險,被人操控。 ## Solidity 進階 **1. 請問以下是Solidity允許的數值?(複選)** - A. 0x1cb89a3833bc070 - B. 129348349684596843 - C. 0.1 回答寫在這裡 ==答案:A,B== Solidity 並不支援浮點數 **2. 說明 proxy 概念,如何更新智能合約?** proxy 實質上是透過 `Delegate call` 來實現更新合約 紀錄狀態、實際執行動作兩者是分開的 變更「實際執行動作」的合約地址,而完成升級動作 而資料皆是儲存在 Proxy 合約內 **3. 合約裡的 receive() 用途是?** ==接收 ETH== 當合約要接收 ETH時,如果合約有寫 receive ()且msg.data為空時,就會自動觸發receive() **4. 做 Dapp 以下是不需要的?** - A. ethers.js - B. RPC Provider - C. 智能合約 ABI - D. 智能合約地址 答案為:==A== ethers 僅是協助開發的工具,非必須 可用其他第三方函式庫,除了ethers.js 也可使用 web3.js,或是其他套件像是 wagmi 等。 **5. 說明 EOA 與 Contract Address 是如何產生的** - EOA(Externally Owned Account):EOA 私鑰配合演算法(ECDSA)計算出公鑰,再對公鑰做keccak-256 運算,取最後面 40 位元組,並在前面加上0x - Contract Address(合約地址):錢包地址+nonce **6. 承上題,兩者有何不同?要如何區分?** EOA:個人錢包 Contract Address:有程式碼、不能主動發起交易 最大的區別在於 EOA地址沒有程式碼 ## Solidity 資安相關 **1. 標註程式碼哪幾行可能有資安問題,並說明何種資安問題,並提出解法** ```=solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract AuthorizeDepositContract { uint256 public fee; mapping(address => uint256) public balance; bool private _lock = false; modifier lock { require(!_lock); _lock = true; _; _lock = false; } function deposit() external payable { uint256 depositFee = msg.value / 100; balance[msg.sender] += msg.value - depositFee; fee += depositFee; } function withdraw(uint256 amount) external { require(balance[msg.sender] >= amount, "Account balance is not enough"); balance[msg.sender] -= amount; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed."); } function withdrawFee() external lock { (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } } ``` ==withdrawFee== 要限制只能 owner 提款 1. withdrawFee 加上 onlyOwner 2. 設定 modifier onlyOwner ```solidity= modifier onlyOwner { require(msg.sender == owner); _; } function withdrawFee() external lock onlyOwner{ (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } ``` ==withdrawFee== 呼叫此 fn 也可做重送攻擊,雖已限制 owner,但也可再加上限制讓合約變得更保險 以下方法二選一施作即可避免重送攻擊 1. 將 ==fee = 0== 改寫在 msg.sender.call 之前 2. 引用 Openzepplin ReentrancyGuard - 繼承合約`is ReentrancyGuard` - 在withdrawFee function 加上 modifier nonReentrant ```solidity= import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract AuthorizeDepositContract is ReentrancyGuard { function withdrawFee() external lock onlyOwner nonReentrant { (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } } ``` 3. 由於 Solidity 版本為 0.6版,會有 overflow 溢位可能,0.8版以下需==引用 SafeMath Library== ## Solidity 合約應用 **1. 部署一個 ERC721 合約並開源合約,mint 一個 NFT 後轉移到以下地址 `0x6e24f0fF0337edf4af9c67bFf22C402302fc94D3` 請留下 transfer 的 tx hash** ``` 部署至Sepolia測試鏈,因沒有Goerli ETH tx hash: 0xcfcfd3c73fee6290ae0dfe74cf4a0f50996a26aabef60ef000028908def7c0eb ``` **2. 請部署一個能從 Chainlink 取得 ETH 價格的合約到 goerli 測試網上並開源合約** 部署至Sepolia測試鏈,因沒有Goerli ETH 但合約驗證一直不過,所以合約內容貼在下方 合約地址 : 0xE274115564F2cb18e0317E01636f49CBca93990d ``` solidity= //SPDX-License-Identifier: MIT pragma solidity ^0.8.16; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract ChainLinkTest { AggregatorV3Interface internal priceFeed; constructor() { priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306); } function getLatestPrice() public view returns (int) { ( ,int price , , , ) = priceFeed.latestRoundData(); return price; } } ``` Remix 驗證 取得ETH/USD價格 ![](https://hackmd.io/_uploads/SyS3qjBr2.png) **3. 以下是一份名單,共有八個地址,請利用 merkle tree 原理製作出一個 root 以及當 address1 要證明自己在 leaves 中時所需要提交的 proof** ``` 名單 address1: '0xdab15510af1425ba57499C2284cf420001A24D00' address2: '0xA2F7B4eA63be89464bE01FB074d981F5917f53ef' address3: '0x4197b82771654C0cE9049925845a8F942b58ccD0' address4: '0x1b9024CFB1409c13f3B2ee422e9c196442c699E1' address5: '0xEa36d9a9d90b7aFA41404CeCa0c06F9d3A75A8fa' address6: '0x5ea8023bB1cca8aF07bcA9edB8FCE8b8a84C8B3f' address7: '0x4bCae98Ab9912694af894D82658517782203a1dE' address8: '0x2834A1487A841930b8b5b3C5812FB526A0189339' ``` ``` # 回答區 1. root: ... 2. proof: ... # 解題過程... ``` **4. 實作 ERC20 質押某代幣,timelock(固定鎖倉期,自定義), reward (回饋該代幣)** ```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() 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); } } ``` ###### tags: `Solidity 工程師實戰營第 5 期`