# 第四屆QC筆試考題
**及格線:6題/10題**
## Solidity 基礎
**1. 如何在部屬合約時,指定 owner,以程式碼舉例**
```solidity=
contract exam {
constructor() ERC721("KryptoNFT", "KT") {
owner = msg.sender;
}
}
```
**2. ERC721 合約部屬至 goerli,寫上部屬的合約地址**
回答寫在這裡
```
https://goerli.etherscan.io/verifyContract
```
**3. 試寫出兩種錯誤處理(require, revert),並解釋兩種的不同**
回答寫在這裡
```solidity
modifier onlyOwner() {
require(msg.sender == owner)
//require包含條件判斷
_;
}
modifier onlyOwner(){
if(msg.sender !== owner){
revert()
//revert不包含條件判斷
}
}
```
**4. 試寫出 transfer, call 的不同**
- transfer 會限制 gas 花費量 2,300,可防止重入攻擊、call 可以做更多的事
回答寫在這裡
```
transfer僅能夠將錢轉過去,但是call除了可以轉錢過去,還可以呼叫另外一個合約的function
```
```solidity=
// call
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
// transfer
_to.transfer(msg.value);
```
**5.承上題,transfer 與 ERC20 的 transfer 有何不同?**
回答寫在這裡
```solidity
//ERC20的transfer
transfer(address _to, uint256 _value)
//發送數量為_value的Token到地址_to,這裡的value可指的是代幣數量
//ERC721的transfer
transfer(address _to, uint256 _tokenId)
//這裏的tokenId是一組唯一的ID
```
**6. 以下哪個選項為較安全產生隨機數?**
- A:block.timestamp
- B:block.hash
- C:Chainlink VRF 預言機
回答寫在這裡
```
C
```
## Solidity 進階
**1. 請問以下是Solidity允許的數值?(複選)**
- A. 0x1cb89a3833bc070
- B. 129348349684596843
- C. 0.1
回答寫在這裡
```
BC
```
**3. 說明 proxy 概念,如何更新智能合約?**
回答寫在這裡
```
使用proxy會部署三個約:
實例合約:這個合約可以被升級
代理合約:使用者直接對該合約操作,該合約會包含實例合約
代理合約管理員:代理合約的擁有者,管理員可以執行Upgradeability Proxy更新實例合約的位置
```
**4. 合約裡的 receive() 用途是?**
回答寫在這裡
```solidity
receive() external payable {
emit Received(msg.sender, msg.value);
//receive是用來收錢的,function中不需要給任何參數
}
```
**5. 做 Dapp 以下是不需要的?**
- A. ethers.js
- B. RPC Provider
- C. 智能合約 ABI
- D. 智能合約地址
回答寫在這裡
```
以上都需要
```
**6. 說明 EOA 與 Contract Address 是如何產生的**
回答寫在這裡
```
EOA錢包位址是根據private key經過算法產生的
Contract address則是根據上傳者的EOA錢包位置加上nonce參數後透過算法產生的
```
**7. 承上題,兩者有何不同?要如何區分?**
回答寫在這裡
```
EOA錢包裡面不會有程式碼;有程式碼的一定是contract
```
**8. 實作 ERC20 質押某代幣,timelock(固定鎖倉期,自定義), reward (回饋該代幣)**
回答寫在這裡
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract BasicBank {
// 質押 Staking Token代幣
IERC20 public stakingToken;
// 全部質押數量
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;
constructor(IERC20 _stakingToken) {
stakingToken = _stakingToken;
}
// 存款
function deposit(uint256 _amount) external {
// 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 {
// 檢查:餘額需要大於 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();
}
// 計算利息
function getReward(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 getAllReward() public view returns (uint256){
uint256 N = depositOf[msg.sender].length;
uint256 allRewards;
for (uint256 i = 0; i < N; i++) {
allRewards += getReward(i);
}
return allRewards;
}
}
```
## Solidity 資安相關
**1. 標註程式碼哪幾行可能有資安問題,並說明何種資安問題,並提出解法**
```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;
bool internal isLock;
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 用lock變數控制
require(islock == false, "you want to reentry! no way!");
islock = true;
require(balance[msg.sender] >= amount, "Account balance is not enough");
//所有的msg.sender都應該要加上不可以是0地址的modifier
balance[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
//用call因為沒有瓦斯費限制所以容易被溢位攻擊,建議改使用transfer
islock = false;
require(success, "Transfer failed.");
}
function withdrawFee() external {
(bool success, ) = msg.sender.call{value: fee}("");
require(success, "Transfer failed.");
fee = 0;
}
}
```
```
解法附在備註上面
```
**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
//這裡修改比歷成3分之1
}
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;
bool internal isLock;
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 用lock變數控制
require(islock == false, "you want to reentry! no way!");
islock = true;
require(balance[msg.sender] >= amount, "Account balance is not enough");
//所有的msg.sender都應該要加上不可以是0地址的modifier
balance[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
//用call因為沒有瓦斯費限制所以容易被溢位攻擊,建議改使用transfer
islock = false;
require(success, "Transfer failed.");
}
function withdrawFee() external {
(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 是多少數字?**
```
要用fundry跑才能知道預測gas fee,但是我的fundry怪怪的,本題passs
```
**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);
}
}
```
請描述你是如何破解的,詳細寫下步驟
```
1. 發現到addPoints這一個function 中可以傳入的變數型別是int這意味著可以傳入負數的_points進去
2. 假設我今天先傳入一個最大的負數,他可以通過require(_points <= 10)的檢查條件
3. 當我第二次再傳入負數進去,他就會溢位變成正非常大的數
4. 我可以領錢了
```
###### tags: `Solidity 工程師實戰營第 4 期`