# 第四屆QC筆試考題 **及格線:6題/10題** ## Solidity 基礎 **1. 如何在部屬合約時,指定 owner,以程式碼舉例** ```solidity= contract exam { address private owner; constructor() { owner = msg.sender; } } ``` **2. ERC721 合約部屬至 goerli,寫上部屬的合約地址** 回答寫在這裡 ```solidity= 0xC186a835605BdA7e8cBADe2eC02A910A1C9f0592 ``` **3. 試寫出兩種錯誤處理(require, revert),並解釋兩種的不同** 回答寫在這裡 ```text= 1. require() 用來檢查較不嚴重的錯誤,可以退回為使用到的 gas。 2. revert() 跟 require() 基本上相同,但是 revert 沒有包括狀態檢查。 3. 還有第三種 assert() 用來檢查較嚴重的錯誤,會像以前一樣拿走所有 gasLimit 的手續費。 ``` **4. 試寫出 transfer, call 的不同** 回答寫在這裡 ```text= transfer: - 如果異常會轉賬失敗,拋出異常 - transfer 會限制 gas 花費量 2,300,可防止重入攻擊 call: - 如果異常會轉賬失敗,僅會返回false,不會終止執行 - 没有gas限制 - 比較底層,使用上需要比較小心 ``` ```solidity= // call (bool sent, bytes memory data) = _to.call{value: msg.value}(""); // transfer _to.transfer(msg.value); ``` **5.承上題,transfer 與 ERC20 的 transfer 有何不同?** 回答寫在這裡 ```text= - ERC20 transfer 是從自己 transfer 到對方,對象是 Token - 原生 transfer 是轉 Eth,對象是 Eth ``` **6. 以下哪個選項為較安全產生隨機數?** - A:block.timestamp - B:block.hash - C:Chainlink VRF 預言機 回答寫在這裡 ```text= # C,隨機數一定要從鏈下數據產生,否則會有資安風險。 ``` ## Solidity 進階 **1. 請問以下是Solidity允許的數值?(複選)** - A. 0x1cb89a3833bc070 - B. 129348349684596843 - C. 0.1 回答寫在這裡 ``` # A, B 皆可 , C不行,solidity 中不允許浮點數 ``` **3. 說明 proxy 概念,如何更新智能合約?** 回答寫在這裡 ```text= # proxy 基本思想我們可以把合約分拆成 Proxy Contract 跟 Logic Contract , 將資料存在代理合約和程式邏輯儲存在邏輯合約中,所以升級的時候, 舊有的資料並不會消失,而是會繼續保留在合約中,而抽象的邏輯就可以隨著升級的合約更新。 透過 delegatecall,上述將執行單位與邏輯單位切割的想法得以實現。 另一角度來說就是進行升級。第一個合約是一個簡單 “proxy”, 用戶可以直接和它交互,並負責將交易轉發到包含實際邏輯的第二個合約。 要理解的關鍵概念是可以在代理或接入點永遠不變的情況下替換邏輯合約。 這兩個合約仍然是不可變的,因為它們的程式碼不能改變, 但是邏輯合約可以簡單地被另一個合約交換。 這就稱為合約被“upgraded”。 ``` **4. 合約裡的 receive() 用途是?** 回答寫在這裡 ``` # 合約中用來接收 ETH, 不需要 function 關鍵字:receive() external payable { ... } 不能有任何的參數,不能返回任何值,必須包含 external 和 payable。 當合約接收 ETH 的時候,receive() 會被觸發。 receive() 最好不要執行太多的邏輯因為如果別人用 send 和 transfer 方法發送 ETH 的話, gas 會限制在 2300,receive() 太複雜可能會觸發 Out of Gas 報錯; 如果用 call 就可以自定義 gas 執行更複雜的邏輯(這三種發送 ETH 的方法我們之後會講到)。 ``` **5. 做 Dapp 以下是不需要的?** - A. ethers.js - B. RPC Provider - C. 智能合約 ABI - D. 智能合約地址 回答寫在這裡 ``` # A, 有別套可以使用例如 web3.js ``` **6. 說明 EOA 與 Contract Address 是如何產生的** 回答寫在這裡 ``` # 可以免費創建EOA,但若要創建合約帳戶則需支付手續費 EOA產生過程: 橢圓顯算法產生 public、private key 把public key 除開頭第一個 byte ,進行 SHA-3 (keccak-256)。 最後面取40個字加上 0x得到。 合約帳戶: EOA 地址 + nounce ``` **7. 承上題,兩者有何不同?要如何區分?** 回答寫在這裡 ``` # EOA - 個人錢包 Contract Address - 有程式碼 ``` **8. 實作 ERC20 質押某代幣,timelock(固定鎖倉期,自定義), reward (回饋該代幣)** 回答寫在這裡 ```solidity= // SPDX-License-Identifier: MIT pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 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; event WithdrawReward(address _account, uint256 reward); constructor(IERC20 _stakingToken, IERC20 _rewardToken) { stakingToken = _stakingToken; rewardToken = _rewardToken; } // 計算利息,公式計算 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{ // 1) 取得目前的總利息 uint256 reward = rewardOf[msg.sender]; // 2) 將利息歸 0 rewardOf[msg.sender] = 0; // 3) 利息用 rewardToken 方式獎勵給 User rewardToken.transfer(msg.sender, reward); // 4) 紀錄事件,使用者已經提領利息 emit WithdrawReward(msg.sender, reward); } } ``` ## 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; } } ``` ``` # 31行 withdrawFee 沒有判斷只有 owner 可以提領 20、25行 舊版 solidity有溢出風險,需要使用 safeMath ``` **2. 試寫出多簽錢包程式碼,調整同意比例(1/3)** 回答寫在這裡 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; contract MultiSigWallet { event Deposit(address indexed sender, uint amount, uint balance); event SubmitTransaction( address indexed owner, uint indexed txIndex, address indexed to, uint value, bytes data ); event ConfirmTransaction(address indexed owner, uint indexed txIndex); event RevokeConfirmation(address indexed owner, uint indexed txIndex); event ExecuteTransaction(address indexed owner, uint indexed txIndex); address[] public owners; mapping(address => bool) public isOwner; uint public numConfirmationsRequired; struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 numConfirmations; } // mapping from tx index => owner => bool mapping(uint256 => mapping(address => bool)) public isConfirmed; Transaction[] public transactions; modifier onlyOwner() { require(isOwner[msg.sender], "not owner"); _; } modifier txExists(uint256 _txIndex) { require(_txIndex < transactions.length, "tx does not exist"); _; } modifier notExecuted(uint256 _txIndex) { require(!transactions[_txIndex].executed, "tx already executed"); _; } modifier notConfirmed(uint256 _txIndex) { require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed"); _; } constructor(address[] memory _owners) { require(_owners.length > 0, "owners required"); for (uint i = 0; i < _owners.length; i++) { address owner = _owners[i]; require(owner != address(0), "invalid owner"); require(!isOwner[owner], "owner not unique"); isOwner[owner] = true; owners.push(owner); } // 需要三分之一的人同意 numConfirmationsRequired = _owners.length/ 3; } receive() external payable { emit Deposit(msg.sender, msg.value, address(this).balance); } function deposit() external payable {} function submitTransaction( address _to, uint256 _value, bytes memory _data ) public onlyOwner { uint256 txIndex = transactions.length; transactions.push( Transaction({ to: _to, value: _value, data: _data, executed: false, numConfirmations: 0 }) ); emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data); } function confirmTransaction(uint256 _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) { Transaction storage transaction = transactions[_txIndex]; transaction.numConfirmations += 1; isConfirmed[_txIndex][msg.sender] = true; emit ConfirmTransaction(msg.sender, _txIndex); } function executeTransaction(uint _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { Transaction storage transaction = transactions[_txIndex]; require( transaction.numConfirmations >= numConfirmationsRequired, "cannot execute tx" ); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}( transaction.data ); require(success, "tx failed"); emit ExecuteTransaction(msg.sender, _txIndex); } function revokeConfirmation(uint _txIndex) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) { Transaction storage transaction = transactions[_txIndex]; require(isConfirmed[_txIndex][msg.sender], "tx not confirmed"); transaction.numConfirmations -= 1; isConfirmed[_txIndex][msg.sender] = false; emit RevokeConfirmation(msg.sender, _txIndex); } function getOwners() public view returns (address[] memory) { return owners; } function getTransactionCount() public view returns (uint) { return transactions.length; } function getTransaction(uint _txIndex) public view returns ( address to, uint value, bytes memory data, bool executed, uint numConfirmations ) { Transaction storage transaction = transactions[_txIndex]; return ( transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations ); } } ``` **3. 標註程式碼哪幾行可能有資安問題,並說明何種資安問題,並提出解法** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract AuthorizeDepositContract is ReentrancyGuard { using SafeMath for uint256; uint256 public fee; mapping(address => uint256) public balance; 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 { (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } } ``` ``` # 26行要判斷 owner ``` **4. 此[合約](https://goerli.etherscan.io/address/0x03C928FFF7609849Ce3d7428804Fd7dE4BE3a643#code) 呼叫 mint 預估 Gas 為多少?請寫下你預估 Gas 的詳細步驟、預估 Gas 是多少數字?** ``` # 回答區 https://www.evm.codes/ 我會用這判斷 ``` **5. 這是一個績點奪獎金遊戲,請找出漏洞在哪,讓你可以跳過原本的通關條件領光合約裡所有的錢** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; contract KryptoGame { mapping(address => int256) public playerPoints; uint256 public pointstoWin = 1e10; uint256 public prize; bool public status; address public winner; address payable public owner; constructor() payable { owner = payable(msg.sender); status = true; prize += msg.value; } modifier onlyowner() { require(msg.sender == owner, "You are not owner"); _; } function getPrizePool() public view returns (uint) { return address(this).balance; } function addPoints(int256 _points) public { require(status == true, "Game is over."); require(_points <= 10, "Only allow to add less than 10 points!"); playerPoints[msg.sender] += _points; } function winTheGame() public { require(uint256(playerPoints[msg.sender]) >= pointstoWin, "Not yet."); winner = msg.sender; status = false; payable(msg.sender).transfer(address(this).balance); } function BOMB() public onlyowner { selfdestruct(owner); } } ``` 請描述你是如何破解的,詳細寫下步驟 ###### tags: `Solidity 工程師實戰營第 4 期`