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。