寫完 Code 絕對不能直接上主網
在過去兩天中,我們順利開發了 ERC20 代幣與去中心化交易所(DEX)的核心合約。對於傳統軟體開發來說,寫完程式碼、跑起來沒報錯,可能就準備打包上線了。
但在 Web3 與智慧合約的世界裡,這是一個極度危險的想法。一旦合約部署上鏈,不僅程式碼難以竄改,裡面更可能鎖倉了價值數百萬美元的資產。只要出現一個未處理的邊界條件,駭客就能輕易掏空資金池。身為資安系的學生,我們必須建立一套嚴謹的「智慧合約生命週期」觀念,而其中最重要的一環,就是撰寫測試腳本。
智慧合約的生命週期
一個成熟且安全的 Web3 專案,從無到有通常會經歷以下五個階段:
Foundry 測試框架:為什麼它對資安如此重要?
在 Day 9 建立環境時我們提過,Foundry 最大的優勢在於「用 Solidity 測試 Solidity」。這讓我們在寫測試腳本時,不需要在 JavaScript 與 Solidity 之間切換語境。
更強大的是,Foundry 內建了 vm(虛擬機操作物件),這是一組被稱為「作弊碼 (Cheatcodes)」的神兵利器。透過 vm,我們可以任意改變區塊時間、強制設定某個地址的餘額、甚至偽造別人的身分(msg.sender)來模擬駭客發起攻擊。
實戰:為 ERC20 代幣撰寫單元測試
讓我們回到前天寫的 MySecureToken 合約。我們要在 Foundry 專案的 test/ 資料夾下,建立一個名為 MySecureToken.t.sol 的測試檔(.t.sol 是 Foundry 測試檔的標準副檔名)。
'''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 引入 Foundry 的標準測試庫
import "forge-std/Test.sol";
// 引入我們要測試的目標合約
import "../src/MySecureToken.sol";
// 測試合約必須繼承 Test
contract MySecureTokenTest is Test {
MySecureToken public token;
// 建立兩個模擬帳戶:老闆與駭客
address owner = address(0x1);
address hacker = address(0x2);
// setUp() 函數會在「每一個」測試函數執行前自動跑一次,用於初始化狀態
function setUp() public {
// vm.prank 可以將下一筆交易的呼叫者 (msg.sender) 偽裝成指定的地址
vm.prank(owner);
token = new MySecureToken(); // 由 owner 部署合約
}
// 測試案例 1:Owner 應該能成功鑄造代幣
function test_MintByOwner() public {
vm.prank(owner); // 偽裝成 owner
token.mint(owner, 1000);
// 斷言 (Assertion):檢查 owner 的餘額是否正確增加了 1000(加上初始鑄造量)
assertEq(token.balanceOf(owner), 1000000 * 10**18 + 1000);
}
// 測試案例 2(資安防護測試):駭客試圖呼叫 mint 應該要被拒絕 (Revert)
function test_RevertWhen_MintByHacker() public {
vm.prank(hacker); // 偽裝成駭客
// vm.expectRevert 宣告下一行程式碼「必定會發生錯誤並退回」,
// 如果下一行沒有出錯,這個測試案例反而會判定為失敗!
vm.expectRevert();
token.mint(hacker, 1000);
}
}
'''
執行測試與解讀報告
寫好測試腳本後,打開終端機,輸入以下指令:
'''
forge test -vv
'''
(-vv 代表詳細輸出模式,會顯示各個測試函數的日誌與變數狀態)
如果程式碼邏輯無誤,你將會看到閃亮亮的綠字報告,顯示兩個測試案例皆以極快的速度 [PASS] 成功通過!這代表我們的權限控制修飾子 onlyOwner 確實在防禦駭客越權鑄幣上發揮了作用。
有了撰寫測試腳本的能力,我們就不再只是「開發者」,而是具備了「合約攻擊者與防禦者」的視角。