# 第五屆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價格

**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 期`