# 第四屆QC筆試考題 **及格線:6題/10題** ## Solidity 基礎 **1. 如何在部屬合約時,指定 owner,以程式碼舉例** ```solidity= contract exam { address owner; constructor(address _owner) { owner = _owner; } } ``` **2. ERC721 合約部屬至 goerli,寫上部屬的合約地址** 回答寫在這裡 ``` solidity # 回答區 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract ShenNFT is ERC721 { // TODO: 改掉 name, symbol constructor() ERC721("ShenNFT", "SN") {} function mint(uint256 tokenId) public { _safeMint(msg.sender, tokenId); } } Goerli ERC721合約網址 https://goerli.etherscan.io/tx/0x1170c3b5d984aca9f3b670403ef623133aa0a7ac71a7ce073e4db6e59f7dd179 Goerli ERC721合約地址 0x72e49de4d61d60e531777d7d4120a1d77e0dd8d8 ``` **3. 試寫出兩種錯誤處理(require, revert),並解釋兩種的不同** 回答寫在這裡 ```solidity! # 回答區 //require require(msg.sender == owner); //revert if(msg.sender != owner){ revert(); } require和revert 用來檢查較不嚴重的錯誤,可以退回為使用到的 gas費用 require和rever基本上差不多,但revert沒有包括狀態檢查 ``` **4. 試寫出 transfer, call 的不同** - transfer 會限制 gas 花費量 2,300,可防止重入攻擊、call 可以做更多的事 回答寫在這裡 ``` # 回答區 transfer 如果轉帳失败,抛出異常 函数原型:<address payable>.transfer(uint256 amount) gas花費量2300 call 如果轉帳失败,只會返回False,不會中止執行 gas花費量沒有限制 ``` ```solidity= // call (bool sent, bytes memory data) = _to.call{value: msg.value}(""); // transfer _to.transfer(msg.value); ``` **5.承上題,transfer 與 ERC20 的 transfer 有何不同?** 回答寫在這裡 ``` # 回答區 ERC20 transfer 是從自己轉帳到對方,而轉移的是token,ERC721的概念也是一樣,轉移的是NFT transfer 是轉ETH、BTC等等原生幣種 ``` **6. 以下哪個選項為較安全產生隨機數?** - A:block.timestamp - B:block.hash - C:Chainlink VRF 預言機 回答寫在這裡 ``` # 回答區 Chainlink VRF 預言機 ``` ## Solidity 進階 **1. 請問以下是Solidity允許的數值?(複選)** - A. 0x1cb89a3833bc070 - B. 129348349684596843 - C. 0.1 回答寫在這裡 ``` # 回答區 A. 0x1cb89a3833bc070 經由SSH ``` **3. 說明 proxy 概念,如何更新智能合約?** 回答寫在這裡 ``` # 回答區 proxy概念:合約部署後往往無法再更動,但利用proxy-implementation 的架構,仍保有一定彈性的作法 有兩個合約(a、b)一個是登入的,一個是修改的合約 a Contract (登入合約) b Contract (資料合約) =>只能被 a 呼叫 =>更新合約時,更新 b 的 owner 並通知使用者用新的 a ``` **4. 合約裡的 receive() 用途是?** 回答寫在這裡 ``` # 回答區 合約一定能拿得到幣種,就會用receive()來拿 ``` **5. 做 Dapp 以下是不需要的?** - A. ethers.js - B. RPC Provider - C. 智能合約 ABI - D. 智能合約地址 回答寫在這裡 ``` # 回答區 A. ethers.js 它是用來呼叫ether.js,然操縱錢包的 ``` **6. 說明 EOA 與 Contract Address 是如何產生的** 回答寫在這裡 ``` # 回答區 EOA是由 => 私鑰演算法產生地址,SSH概念 Contract Address是由 => 錢包和nonce所組成的 ``` **7. 承上題,兩者有何不同?要如何區分?** 回答寫在這裡 ``` # 回答區 EOA和Contract Address,如何判斷,他們差在只有一個有合約程式,另一個沒有 EOA是錢包而已 Contract Address 是有自己寫的合約 ``` **8. 實作 ERC20 質押某代幣,timelock(固定鎖倉期,自定義), reward (回饋該代幣)** 回答寫在這裡 ``` solidity # 回答區 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract Bank{ IERC20 public stakingToken; uint public totalSupply; mapping(address => uint256) public balanceOf; //鎖倉時間 uint256 public lockDuration = 5 seconds; mapping(address => uint256) public lockStarts; mapping(address => uint256) public lockEnds; // struct Deposite { // uint256 amount; // uint256 start; // uint256 end; // } //mapping(address => Deposite[]) public depositesOf; //獎勵機制 uint public reward = 1; mapping(address => uint) public rewardToken; constructor(IERC20 _stakingToken){ stakingToken = _stakingToken; } //存入 function deposite(uint _amount) external{ stakingToken.transferFrom(msg.sender, address(this), _amount); //轉移代幣 totalSupply += _amount; balanceOf[msg.sender] += _amount; //鎖倉時間 lockStarts[msg.sender] = block.timestamp; lockEnds[msg.sender] = block.timestamp + lockDuration; // depositesOf[msg.sender].push( // Deposite({ // amount: _amount, // start: block.timestamp, // end: block.timestamp + lockDuration // }) // ); } // //提出 // function withdraw(uint _depositeId) external{ // require(block.timestamp > lockEnds[msg.sender], "withdraw to soon"); //require(_amount <= balanceOf[msg.sender], "insufficient token"); //Deposite[] storage deposites = depositesOf[msg.sender]; //uint256 amount = deposites[_depositeId].amount; //require(_depositeId < deposites.length, "Deposite ID does not exist"); //require(block.timestamp >= deposites[_depositeId].end,"withdraw too soon"); // rewardToken[msg.sender] = getreward(_depositeId); // stakingToken.transfer(msg.sender, amount); //轉移代幣 // totalSupply -= amount; // balanceOf[msg.sender] -= amount; // //remove deposite // uint256 lastDepositeId = deposites.length -1; // deposites[_depositeId] = deposites[lastDepositeId]; // deposites.pop(); // } //獎勵 function getreward(/*uint _depositeId*/) public view returns(uint256){ uint256 duration = block.timestamp - lockStarts[msg.sender]; return duration * reward * balanceOf[msg.sender]; // uint256 start = depositesOf[msg.sender][_depositeId].start; // uint256 amount = depositesOf[msg.sender][_depositeId].amount; //return(amount * (block.timestamp - start) * 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; } } ``` ``` ANS 我看到的資安問題是,如果假如第一次存了錢進去,並且提領出來,他是以Lock去鎖的,可是如果第一次有存進去後,第二次第三次等等,都可以以同樣的msg.sender去執行,這樣第二次之後的錢,都可以無限去領錢,不需要什麼證明 ``` **2. 試寫出多簽錢包程式碼,調整同意比例(1/3)** 回答寫在這裡 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract MultiSigWallet { using SafeMath for uint256; 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; //陣列,他可以設定多少個owner在transact裡面寫成["","","",""] mapping(address => bool) public isOwner; uint public numConfirmationsRequired; struct Transaction { address to; uint value; bytes data; bool executed; uint numConfirmations; } // mapping from tx index => owner => bool mapping(uint => mapping(address => bool)) public isConfirmed; Transaction[] public transactions; modifier onlyOwner() { require(isOwner[msg.sender], "not owner"); _; } modifier txExists(uint _txIndex) { require(_txIndex < transactions.length, "tx does not exist"); _; } modifier notExecuted(uint _txIndex) { require(!transactions[_txIndex].executed, "tx already executed"); _; } modifier notConfirmed(uint _txIndex) { require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed"); _; } constructor(address[] memory _owners) {//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.div(3); } receive() external payable { //如果錢進來可以做多少事情 emit Deposit(msg.sender, msg.value, address(this).balance); } function submitTransaction( address _to, uint _value, bytes memory _data ) public onlyOwner { uint 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(uint _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 ); } } ``` ### 作法 把numConfirmationsRequired取消掉,因為調整比例改成owner的1/3,因此把numConfirmationsRequired使用safemath給除以3 **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; } } ``` ``` # 回答區 ``` ``` solidity function withdrawFee() external { (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } ``` 由於 withdrawFee 沒有限制誰可以領取手續費,因此任何人都可以領走 解法: 使用 Openzeppelin Ownable.sol 來指定權限,讓部署合約的人才能提款 * onlyOwner 可以限制只有部署合約的人才能呼叫該 function ``` solidity import "@openzeppelin/contracts/access/Ownable.sol"; function withdrawFee() external onlyOwner { (bool success, ) = msg.sender.call{value: fee}(""); require(success, "Transfer failed."); fee = 0; } ``` **4. 此[合約](https://goerli.etherscan.io/address/0x03C928FFF7609849Ce3d7428804Fd7dE4BE3a643#code) 呼叫 mint 預估 Gas 為多少?請寫下你預估 Gas 的詳細步驟、預估 Gas 是多少數字?** ``` # 回答區 ``` **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); } } ``` 請描述你是如何破解的,詳細寫下步驟 constructor() payable { owner = payable(msg.sender); status = true; prize += msg.value; } 從payable看到 ###### tags: `Solidity 工程師實戰營第 4 期`