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