智能合約一旦部署上鏈就無法輕易修改,而且直接掌管真金白銀,因此一個看似微小的邏輯錯誤,往往等同把金庫鑰匙公開放在網路上。2016 年的 The DAO 事件正是如此:一行順序寫錯的程式碼,讓攻擊者搬走約 360 萬顆 ETH(當時約 6,000 萬美元),甚至逼得以太坊硬分叉、催生出今天的 Ethereum Classic。理解常見漏洞的成因與防禦手法,是任何想碰 Web3 開發的人的必修課。
📖 學
重入攻擊 reentrancy 與 The DAO
重入攻擊的核心,是「外部呼叫」在合約更新自己狀態之前就把控制權交給了對方。想像一個提款函式:它先用 call 把 ETH 送給使用者,然後才把該使用者的餘額設為 0。問題在於,當這筆 ETH 送達對方合約時,會觸發對方的 fallback/receive 函式,而攻擊者在裡面「再一次」呼叫提款——此時餘額還沒歸零,合約以為他仍有錢,於是再送一次。如此遞迴,直到金庫被榨乾。
The DAO 就是死在這個模式上,對應到 SWC Registry 編號 SWC-107。這個事件的教訓不是「別用外部呼叫」,而是「呼叫外部之前,先把自己的帳算清楚」。
Checks-Effects-Interactions 與 reentrancy guard
防禦重入的黃金守則叫 Checks-Effects-Interactions(CEI),把函式邏輯嚴格排成三段順序:先 Checks(檢查權限與條件,例如 require 餘額足夠)、再 Effects(更新所有內部狀態,例如把餘額扣掉)、最後才 Interactions(對外部做呼叫或轉帳)。只要狀態在轉帳前就更新完畢,任何重入的回呼看到的都是「已扣款」的世界,自然無利可圖。
第二道防線是 reentrancy guard(重入鎖),例如 OpenZeppelin 的 ReentrancyGuard。它用一個狀態變數當旗標,在函式進入時鎖上、離開時解鎖,只要加上 nonReentrant modifier,重入呼叫會直接被 revert。CEI 是設計原則,guard 是保險絲,兩者搭配最穩。
整數溢位、存取控制與 tx.origin
整數溢位/下溢曾是經典漏洞:當 uint8 的 255 再加 1 會回捲成 0,0 - 1 會變成最大值,攻擊者可藉此憑空製造餘額。好消息是 Solidity 0.8 版之後,算術運算內建溢位檢查,超出範圍會自動 revert;若為了省 gas 想關掉,需明確用 unchecked { } 區塊,反而變成刻意的選擇。
存取控制缺失則低級卻致命:忘記在關鍵函式(如提領、鑄幣、自毀)加上 onlyOwner 之類的權限修飾子,等於任何人都能呼叫。2017 年 Parity 多簽錢包的鉅額凍結事件,根源之一就是初始化函式沒有妥善保護。tx.origin 釣魚是另一種陷阱:用 tx.origin 判斷身分時,tx.origin 永遠是最初發起交易的人,若使用者被誘導呼叫惡意合約,惡意合約再轉呼叫你的合約,tx.origin 仍是使用者本人,權限判斷就被繞過。判斷呼叫者身分請一律用 msg.sender。
預言機操縱、閃電貸與 delegatecall
閃電貸 flash loan 讓人在「同一筆交易」內無抵押借出鉅額資金,只要交易結束前還清即可。它本身是中性工具,但配合脆弱的價格預言機就成了利器:攻擊者借入大筆資金,在流動性偏低的 DEX 上瞬間拉抬或砸盤某代幣價格,讓依賴該「即時現貨價」的借貸協議誤判抵押品價值,進而超額借走資產,最後一次性還清閃電貸。2022 年 Mango Markets 被以此手法搬走約 1.17 億美元。防禦靠 TWAP(時間加權平均價) 與多來源預言機,讓價格無法在單一區塊內被瞬間扭曲。
delegatecall 風險在於它「借用別人的程式碼,但在自己的儲存空間上執行」。若目標地址可被操控,或代理合約與邏輯合約的儲存佈局(storage layout)不對齊,攻擊者就能改寫代理合約的關鍵變數(如 owner)。亂數不可預測性則提醒我們:鏈上一切公開,用 block.timestamp、blockhash 當隨機源都可被礦工/驗證者預測或操縱,需要安全亂數時應改用 Chainlink VRF 這類可驗證隨機方案。
🧠 記
- 重入(SWC-107):外部呼叫早於狀態更新 → The DAO 損失 360 萬 ETH。
- CEI 順序:Checks → Effects → Interactions;搭配
nonReentrant鎖。 - 溢位:Solidity 0.8+ 內建檢查,關掉才需
unchecked。 - 權限:關鍵函式要加
onlyOwner;判斷身分用msg.sender,別用tx.origin。 - 閃電貸+預言機:單一區塊操縱現貨價 → 用 TWAP + 多來源預言機。
- delegatecall / 亂數:注意 storage 對齊;鏈上亂數不可信,用 VRF。
✍️ 實踐
今天挑一份公開的簡易 Solidity 合約(或自己寫一個含提款功能的),逐行檢查三件事:提款函式是否遵守 CEI 順序、有沒有把外部轉帳放到狀態更新之後、關鍵函式是否都有權限修飾子。接著到 SWC Registry 挑三個編號(SWC-107 重入、SWC-101 溢位、SWC-115 tx.origin),把每一項的「攻擊範例」與「修復範例」對照讀完,並用一句話寫下你自己的防禦口訣。
🔗 延伸學習
- SWC Registry — Smart Contract Weakness Classification — 智能合約弱點的標準分類目錄,重入、溢位、tx.origin 都有編號與範例。
- OpenZeppelin — ReentrancyGuard 文件 — 業界最常用的重入鎖實作與用法說明。
- ethereum.org — Smart contract security — 官方整理的安全設計原則、常見弱點與稽核資源。
- Wikipedia — The DAO — 事件始末與以太坊硬分叉的完整脈絡。
💬 問 AI
把下面這段貼給 AI,請它幫你把一份合約做重入自檢:
我有一份 Solidity 合約,請幫我以 Checks-Effects-Interactions 原則逐一檢查每個會做外部呼叫或轉帳的函式:指出哪裡把 interaction 放在 effect 之前、是否需要加 nonReentrant、以及是否誤用了 tx.origin 或缺少 onlyOwner。請針對每個問題給出修正前後的程式碼對照。