在真實的資安稽核(Security Audit)實務中,工具終究只是輔助。駭客往往是從系統架構最邊緣的盲區發動攻擊。身為資安系的學生,我們必須建立一套嚴謹的「SOP」。今天,我們就把前述學到的所有攻擊手法與防禦策略,收斂成一份實務上可以直接拿來照表操課的智慧合約安全稽核清單 (Audit Checklist)。未來當你在部署任何合約前,請務必逐條核對!
一、 權限控制與初始化 (Access Control & Initialization)
這是防禦最基礎但也最容易疏忽的第一道門。門沒鎖好,裡面再安全都沒用。
- 建構子與初始化: 代理合約(Proxy)是否使用了 Initializable 模組?initialize() 函數是否加上了 initializer 修飾子以防止被重複呼叫?
- 可見度審查: 把合約裡所有的 public 與 external 函數全部列出來。問自己:這些函數真的需要對外公開嗎?如果涉及資金或狀態修改,是否已經加上了 onlyOwner 或對應的 Role-Based 權限修飾子?
- 特權階級風險 (Centralization Risk): Owner 的權限是否過大(例如可以隨意凍結用戶資金或無限鑄幣)?實務上,Owner 應該是一個多簽錢包(Multi-sig Wallet)或時間鎖(Timelock)合約,而非開發者的個人帳戶。
二、 資金流動與狀態更新 (Fund Management & State)
牽涉到 Ether 或 ERC20 代幣轉移的函數,是駭客最愛盯上的肥肉。
- 重入攻擊防禦: 只要有向外部地址轉帳的操作,是否嚴格遵守了 Checks-Effects-Interactions (CEI) 模式?是否已經在把錢轉出去之前,先更新了內部的狀態變數(如餘額歸零)?高風險函數是否有加上 nonReentrant 互斥鎖?
- 拒絕服務防禦 (Push vs. Pull): 合約是否會主動發送退款或獎勵給多個用戶?如果是,請立刻改為「讓用戶自己來領取 (Pull)」的設計,避免因為某個接收地址惡意拒收而導致整個合約癱瘓。
- 強制塞錢攻擊 (Forced Ether): 合約的邏輯是否依賴 address(this).balance == 特定數值?(注意:駭客可以透過 selfdestruct 強制把 Ether 塞進你的合約,繞過 receive() 函數,這會打破嚴格的餘額相等假設)。
三、 數學運算與邏輯 (Math & Logic)
這部分需要特別關注邊界值與型別轉換。
- 溢位與下位: 雖然 Solidity 0.8.0 之後內建了檢查,但合約中是否有為了省 Gas 而使用 unchecked 區塊?該區塊內的變數是否絕對不可能被外部操控?
- 除法精度遺失: Solidity 沒有小數點。在計算收益率或手續費時,是否遵守了「先乘後除」的原則?(例如計算 A * (B / C) 時,如果 B < C 結果會變成 0,應該改為 (A * B) / C)。
四、 外部互動與預言機 (External Calls & Oracles)
在 DeFi 的世界裡,不要輕易相信別人傳來的資料。
- 閃電貸與價格操縱: 合約是否依賴單一 DEX(如 Uniswap 單一池)作為價格預言機?是否已經改用 Chainlink 這類去中心化預言機網路來取得防篡改的價格中位數?
- 預言機資料校驗: 呼叫 Chainlink latestRoundData() 後,是否有檢查 price > 0?是否有檢查時間戳記確保資料沒有過期 (Stale Data)?
- 未檢查的回傳值: 當使用低階呼叫 call() 轉帳時,是否有檢查並處理回傳的布林值(require(success))?
五、 密碼學與簽章 (Cryptography & Signatures)
將部分邏輯移到鏈下計算以節省 Gas 時,必須嚴防簽章被惡意利用。
- 重放攻擊防禦: ecrecover 驗證簽章成功後,是否有將該簽章(或 Nonce)標記為「已使用」?
- 跨鏈與跨合約防禦: 簽章的 Hash 內容是否遵循了 EIP-712 標準?是否有把當前區塊鏈的 ChainID 與當前合約的 address 一併納入簽章範圍,防止簽章被拿到另一條鏈上兌現?
稽核工作流總結
至此,Phase 3 的資安攻防訓練正式結業。在實務上,當你寫完一份合約,請依照以下順序進行檢測:
- 單元測試與 Fuzzing(涵蓋 90% 以上的程式碼分支)。
- Slither 靜態掃描(過濾低級語法錯誤與權限漏洞)。
- 對照本篇 Audit Checklist(以人工邏輯重新審視架構)。
- Mythril 符號執行(深度挖掘隱藏路徑)。