智能合約安全與審計

拒絕服務漏洞

拒絕服務漏洞讓攻擊者卡住一個合約,使正當使用者無法使用,且往往攻擊者並未直接竊取任何東西。這就是 griefing:目的是破壞或凍結功能,而非牟利。由於鏈上每一個操作都要花 gas、而每個區塊都有硬性的 gas 上限,一個能被推入去做無上限或受外部控制工作的合約,可被弄到對所有人都回滾,有時還會把裡頭的資金永久鎖死。

幾種模式反覆出現。一個對任何人都能增長的陣列做無上限迭代的迴圈——向每位參加者付款、或遍歷一份使用者清單——終究會需要超過一個區塊所允許的 gas,屆時任何碰到那個迴圈的交易都無法成功。拒絕服務式回滾發生在付款迴圈中有單一收款方是一個總是回滾的合約時(例如它的 receive 函式會丟出例外),擋住整批的完成,這正是當年 King of the Ether 的缺陷。其他變體包括仰賴一個攻擊者能使其失敗的特定外部呼叫,或關鍵路徑上的意外回滾。

標準防禦是「拉取優於推送」模式:與其讓合約在一個迴圈內把資金推給一份收款方清單,不如讓每位使用者在各自的交易中拉取自己的餘額,如此一個壞人只能擋住自己。更廣義地說,避免對使用者可控的資料結構做無上限迭代,絕不讓單一外部呼叫的失敗報廢一個共用流程,並把關鍵函式設計成沒有任何單一參與者能挾持整個系統。為迴圈規模設上限、隔離失敗、讓提領各自獨立。

// VULNERABLE: one reverting recipient blocks all
for (uint i = 0; i < bidders.length; i++) {
    bidders[i].transfer(refunds[bidders[i]]); // can revert
}

// SAFE: pull pattern
function withdrawRefund() external {
    uint256 r = refunds[msg.sender];
    refunds[msg.sender] = 0;
    (bool ok, ) = msg.sender.call{value: r}("");
    require(ok);
}

讓使用者各自拉取資金,而非推送給所有人。

經典解法是拉取優於推送:絕不要在一筆交易裡用迴圈付款給所有人,因為一個惡意或會回滾的收款方就能讓整批回滾。改讓每位使用者各自獨立提領自己的餘額。

又稱
DoSgriefing