# Garrick信仲 | C0051202 KryptoCamp 第 5 屆QC筆試 **及格線:6題/10題** ## Solidity 基礎 **1. 如何在部屬合約時,指定 owner,以程式碼舉例** ```solidity= contract exam { address private owner; constractor(){ owner = msg.sender;//部署合約時指定owner } } ``` constractor在部署時只會執行一次,所以在裡面指定owner **2. 請列出 ERC721 safeMint & mint 兩者的差異?** 參考網址:https://docs.openzeppelin.com/contracts/3.x/api/token/erc721 ``` TokenId必須存在,如果 to是智能合約,則必須實作IERC721Receiver.onERC721Received達到安全移轉 safeMint方法從程式中可以看到也是調用了Mint,但是額外增加了_checkOnERC721Received的判斷,判斷對方的地址是否為黑洞地址,導致資產無法被轉走的永久損失 ``` **3. 試寫出對於操作 ERC20 代幣時,safeTransfer 的功能以及需要使用的時機** ``` # 如果交易的token在沒有返回值的狀況下會退回交易,這樣造成了一個bug, 如果要防止這樣的狀況就可以使用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(count != 3, "The number is not right!" ) if(count != 3){ revert("The number is not right!") } ``` **5. 試寫出 transfer, call 的不同** ``` - transfer: - 會限制 gas 花費 2,300,可防止重入攻擊 - call: - 沒有 Gas 限制 - 可自定義 gas limit 等,可執行更複雜的邏輯 - 會回傳 boolean 來判斷交易是否成功 - 如果失敗會回傳 false,不會終止執行(不會自動回滾) - call 較為底層,使用須小心重入攻擊 transfer: 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不支援浮點數所以不選C ``` **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.js是可選的協助開發工具,還有web3.js或是wagmi可以替代 ``` **5. 說明 EOA 與 Contract Address 是如何產生的** ``` -EOA(Externally Owned Account) : 私鑰先使用 ECDSA(橢圓曲線數位簽章算法) 計算出公鑰,接著對公鑰作出 Keccak-256雜湊,接著取其雜湊最右邊的 40 位元組,並以 16 進制字串方式表達該 40 位元組,然後在字串前面加上 "0x" 字串後,該字串即為該私鑰所對應之乙太坊地址(Ethereum Address)。 -Contract Address(合約地址): 錢包地址+nonce ``` **6. 承上題,兩者有何不同?要如何區分?** ``` EOA:個人錢包 Contract Address:有程式碼、不能主動發起交易 ``` ## 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; } } ``` ## Solidity 合約應用 **1. 部署一個 ERC721 合約並開源合約,mint 一個 NFT 後轉移到以下地址 `0x6e24f0fF0337edf4af9c67bFf22C402302fc94D3` 請留下 transfer 的 tx hash** 部署在Goerli測試鏈上 ``` # 回答區 tx hash: 0x1120dbbeb84ee26057129fdc6595e66d72c18433aa51f240f9b482cdedae2ef2 ``` Contract Address: `0x613A6c22a91b9bc034140F096D43241A09922A6f` **2. 請部署一個能從 Chainlink 取得 ETH 價格的合約到 goerli 測試網上並開源合約** 範例網址 : https://solidity-by-example.org/defi/chainlink-price-oracle/ 合約連結:[https://goerli.etherscan.io/address/0x84B94347518B6E9841fcd01Ad242A2EE2A18Aa7E](https://goerli.etherscan.io/address/0x84B94347518B6E9841fcd01Ad242A2EE2A18Aa7E) **3. 以下是一份名單,共有八個地址,請利用 merkle tree 原理製作出一個 root 以及當 address1 要證明自己在 leaves 中時所需要提交的 proof** ``` 名單 address1: '0xdab15510af1425ba57499C2284cf420001A24D00' address2: '0xA2F7B4eA63be89464bE01FB074d981F5917f53ef' address3: '0x4197b82771654C0cE9049925845a8F942b58ccD0' address4: '0x1b9024CFB1409c13f3B2ee422e9c196442c699E1' address5: '0xEa36d9a9d90b7aFA41404CeCa0c06F9d3A75A8fa' address6: '0x5ea8023bB1cca8aF07bcA9edB8FCE8b8a84C8B3f' address7: '0x4bCae98Ab9912694af894D82658517782203a1dE' address8: '0x2834A1487A841930b8b5b3C5812FB526A0189339' ``` ``` # 回答區 root 0x484a13e504a9fb9b8dae4c8cb1a48506c2690a11fa9e82a619089f9352996508 proof [ '0x88b344beb364e890c0b1d54bf02d3d480fc501d5cf27cc7787abe3e5cbbe7f39', '0x6d4bafed986e6393196d0b7c8fd7ac980df9589c51fb42a5888303052853fa20', '0xb7731fa2737ab4b6c77798c52aa500cf15d60965362df47e8b4832ef7a7bac09' ] # 解題過程... 將名單放到和程式同個路徑下的whitelist檔案中並修改程式中getProof裡的地址後執行產生root和proof #製作root和proof程式碼 const { MerkleTree } = require("merkletreejs"); const keccak256 = require("keccak256"); const whitelistJSON = require("./whitelist"); // 透過 merkletreejs 套件產生 merkle tree function getMerkle(whiteList) { const leafs = whiteList.map((addr) => keccak256(addr)); return new MerkleTree(leafs, keccak256, { sortPairs: true }); } const whitelistMerkleTree = getMerkle(whitelistJSON); // console.log("merkle", whitelistMerkleTree); // 取得 merkle tree 的 root const root = whitelistMerkleTree.getRoot(); console.log("root", bufferToBytes32(root)); // 取得 proof function getProof(address) { const leaf = keccak256(address); return whitelistMerkleTree.getProof(leaf).map((p) => bufferToBytes32(p.data)); } console.log("proof", getProof("0xdab15510af1425ba57499C2284cf420001A24D00")); // // 驗證 proof // function verify(address) { // const leaf = keccak256(address); // const proof = getProof(address); // return whitelistMerkleTree.verify(proof, leaf, root); // } // console.log("verify", verify("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4")); // 將 buffer 轉成 bytes32 function bufferToBytes32(buffer) { return "0x" + buffer.toString("hex").padStart(64, "0"); } ``` **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 期`