在傳統網路安全裡,DoS(Denial-of-Service,拒絕服務)代表「讓系統忙到無法服務他人」。
在區塊鏈上,同樣有這類攻擊,只是形式不同——攻擊者不一定要癱瘓整條鏈,而是讓特定合約或功能無法被正常使用。
今天,我們要看看在智能合約中,DoS 攻擊是如何出現的,以及我們能如何預防。
🧠 什麼是鏈上 DoS 攻擊?
在區塊鏈環境下,DoS 通常指:
攻擊者透過特定輸入、交易順序或大量操作,導致合約執行失敗、Gas 用盡,或讓某功能永久卡住(例如提款、回圈、投票等)。
由於智能合約不可修改、執行需付費(Gas),設計不良就可能被惡意利用成「邏輯死鎖」。
⚙️ 常見類型一覽表
類型 攻擊原理 範例說明
Gas 耗盡型 /利用回圈或大量資料導致交易超出 Gas 限制/大量提款或迴圈遍歷全名單
狀態鎖定型 /攻擊者使狀態卡住,使其他人無法操作/投票永遠未結束、鎖倉永不釋放
依賴外部呼叫型/外部呼叫(call/send/transfer)失敗造成revert/攻擊者刻意讓fallback revert,導致整個函式中斷
競態 DoS(front-run/revert/攻擊者透過搶先交易或干擾邏輯,讓其他人無法成功執行交易/出價競標中,攻擊者不斷小幅提高價以阻斷他人
🧩 範例 1:提款清單導致 Gas 耗盡
mapping(address => uint) public balance;
address[] public users;
function withdrawAll() public {
for (uint i = 0; i < users.length; i++) {
address user = users[i];
uint amount = balance[user];
if (amount > 0) {
balance[user] = 0;
payable(user).transfer(amount);
}
}
}
問題:
• 每次呼叫 withdrawAll() 都要遍歷整個使用者名單。
• 隨著使用者增多,Gas 成本急劇上升。
• 到最後會超出 block gas limit,導致整個交易失敗。
結果:沒有人能再成功提領,合約陷入「永久卡死」狀態。
這就是典型的 Gas DoS。
改進方式:
• 不要一次性遍歷所有人。
• 改成每個人自己提領(pull over push):
function withdraw() public {
uint amount = balance[msg.sender];
require(amount > 0, "No balance");
balance[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
✅ 優點:
每人自行提領 → 單次交易成本固定 → 不會卡死整體。
⚠️ 範例 2:外部呼叫 Revert DoS
function payout(address[] memory recipients) public {
for (uint i = 0; i < recipients.length; i++) {
recipients[i].call{value: 1 ether}("");
}
}
若其中一個 recipients[i] 是惡意合約,它的 fallback 函式若強制 revert:
整個迴圈都會失敗,導致所有合法收款者也拿不到錢。
解法:
改用可忽略錯誤的低層呼叫,並記錄結果:
(bool success, ) = recipients[i].call{value: 1 ether}("");
if (!success) {
failedRecipients.push(recipients[i]);
}
✅ 優點:
惡意地址不會影響其他人;
可在後續 retry 或人工審查時補發。
💣 範例 3:依賴外部輸入的競態 DoS
舉例:一個競標系統。
function bid() public payable {
require(msg.value > highestBid);
if (highestBidder != address(0)) {
highestBidder.call{value: highestBid}(""); // 退回前一名
}
highestBidder = msg.sender;
highestBid = msg.value;
}
攻擊手法:
• 攻擊者成為最高出價者後,在 fallback 函式中 revert()。
• 當下次有人出更高價時,call() 會失敗整個交易 → 新出價永遠失敗。
結果:攻擊者永久佔據第一名,其他人無法競標。
解法:
• 改用「提領式退款」模式(withdraw pattern)。
• 保留退款額度記錄,讓前一名自行取回:
mapping(address => uint) pendingReturns;
function bid() public payable {
require(msg.value > highestBid);
pendingReturns[highestBidder] += highestBid;
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdraw() public {
uint amount = pendingReturns[msg.sender];
require(amount > 0);
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
🧠 預防重點清單
• ❌ 避免遍歷大量動態資料(用 mapping + 單筆操作代替)。
• ✅ 使用 pull 模式(withdraw pattern),減少外部依賴。
• ✅ 記錄失敗而非直接 revert,例如用事件或陣列追蹤。
• ✅ 限制每筆交易能修改的狀態量,降低 Gas 被拖垮風險。
• ⚠️ 警惕外部呼叫(call / delegatecall)失敗的副作用。
• 🧰 若需高安全合約,可使用 OpenZeppelin ReentrancyGuard + PullPayment 模式。
區塊鏈的 DoS 攻擊不像傳統網路那麼顯眼,卻更「安靜致命」。
只要你的合約有一個無限迴圈、或依賴不可信外部呼叫,就可能讓所有人「卡住」資金。
寫合約時應該習慣反問自己:
「這段邏輯若有 1000 個使用者會怎樣?」
「這個函式失敗時,其他人還能繼續嗎?」
只要能回答這兩個問題,基本就能避免大部分 DoS 風險。