iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Security

從 Detect 到 Proof:智慧合約審計工具實戰系列 第 14 篇

Day 14|錢領對了 帳卻記錯了

  • 分享至 

  • xImage
  •  

Day 13 的 fuzz 已經找到 [13, 13] 多付的反例。這個結果用到一個先寫好的判斷:同一個 epoch 出現兩次,可領獎勵仍只能算一次。今天把這句話和 Day 12 看到的 claimed 記帳錯誤,整理成可以重跑的檢查條件。

「不能重複領」要檢查哪件事

這幾條條件都用 BSC 區塊 27,830,139 的 Level Finance 快照,先讓同一個帳戶在 epoch 13 賺到獎勵、領過一部分,再等剩下的獎勵解鎖。比較的是呼叫前後的狀態;換了帳戶、時間或操作順序,都要重新測。

性質 檢查條件 本篇的輸入
一次呼叫不能多付 paid <= 呼叫前各個不重複 epoch 的 claimable 總和 claimMultiple([13, 13]);收款地址的 LVL 餘額差是 paid
已領紀錄不能被覆蓋 claimedAfter == claimedBefore + paid 單一 epoch 的 claimMultiple([13]);讀同一帳戶的 users(13)
沒有新獎勵不能再付 第一次領完,不推進時間,再呼叫 [13] 時 secondPaid == 0 同一帳戶連續呼叫兩次;每次陣列都只有一個 13

第一條是 Day 13 fuzz 已經使用的付款上界。它只看這次付多少,抓得到 [13, 13] 多付,卻看不出 Day 12 那個 [13] 測試為何「付款 PASS、帳記錯」。第二條檢查記帳,第三條用固定的第二次呼叫,確認記帳錯誤會不會真的再次付款。這三條不等於證明所有領款路徑都安全。

把付款上界寫進測試

Day 13 會讓第二個 epoch 在 12 和 13 之間變動,計算上界時不能把重複的 13 加兩次:

uint256 allowed = CONTROLLER.claimable(EPOCH, caller);
if (secondEpoch != EPOCH) {
    allowed += CONTROLLER.claimable(secondEpoch, caller);
}
uint256 before = LVL.balanceOf(recipient);
vm.prank(caller);
CONTROLLER.claimMultiple(epochs, recipient);
uint256 paid = LVL.balanceOf(recipient) - before;
assertLe(paid, allowed, "paid more than distinct epochs allow");

allowed 在呼叫前讀,paid 則看收款地址的餘額差,因為收款地址不一定是呼叫者。這段 assertion 沒有新發現;Day 14 的工作是把 Day 13 的 fuzz 反例固定為回歸測試:rawEpoch=139 映射成 13、rawHours=52 映射成再等 4 小時,收款地址 0x7253。固定送進合約的條件 [13, 13],不依賴下一次 fuzz 抽到同樣的原始輸入。

固定 [13, 13] 呼叫前可領上界 實際付出
被攻擊版:FAIL 7.2867 LVL 8.0963 LVL
修補版:PASS 7.2867 LVL 7.2867 LVL

這個固定測試跟 Day 13 找到的是同一條失敗路徑,沒有把它算成新的漏洞。

付款對了,記帳仍可能錯

Day 12 已觀察到:只傳 [13] 時,被攻擊版付了正確的 4.0482 LVL,但之前領過的 0.8096 LVL 從 claimed 裡消失了。把當時沒檢查的狀態補成 assertion:

uint256 claimedBefore = _claimed();
uint256 available = CONTROLLER.claimable(EPOCH, address(this));
uint256 paid = _claimMultiplePaid(1);
uint256 claimedAfter = _claimed();
assertEq(paid, available, "single-epoch payout changed");
assertEq(claimedAfter, claimedBefore + paid, "claimed did not accumulate payout");

付款檢查兩版都 PASS;記帳檢查在被攻擊版 FAIL(claimed 變成 4.0482,而不是 0.8096 + 4.0482),在修補版 PASS。被攻擊版用 claimed = reward 覆蓋已領紀錄,修補版用 claimed += reward。

還可以用第三條條件檢查後果:保持同一個帳戶和時間,連續呼叫兩次 claimMultiple([13])。第一次兩版都付 4.0482 LVL;第二次被攻擊版又付了 0.8096 LVL,修補版付 0。這是固定的兩次呼叫,不是 Day 15 要做的隨機操作序列。

三條檢查都在本機快照執行;修補版用 vm.etch 把後來經驗證的 runtime 放到快照中的舊實作地址,沒有改動 BSC 鏈上的合約。被攻擊版的預期失敗仍留在套件裡,測試輸出是 3 PASS、3 FAIL。

參考資料


上一篇
Day 13 | Fuzz Test
系列文
從 Detect 到 Proof:智慧合約審計工具實戰 共 14 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言