---
# System prepended metadata

title: Garrick信仲 | C0051202 KryptoCamp 第 5 屆QC筆試
tags: [Solidity 工程師實戰營第 5 期]

---

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