昨天我們把 [13, 13] 寫死在測試裡,確實看到多領。但「要重複放 13」是我先告訴 Foundry 的。這次不寫死第二個 epoch,讓它自己選。
還有一件事:把 fuzz 設成跑 24 次,是不是真的有 24 組輸入進到測試?
沿用 Day 12 的 BSC 區塊 27,830,139 快照,測的還是當時的 LevelReferralControllerV2。先在 epoch 13 賺到獎勵、領一部分,等獎勵繼續解鎖,然後呼叫 claimMultiple。
第一個 epoch 固定是 13,第二個讓 Foundry 在 12 和 13 之間選。這份快照的準備流程只在 13 賺到獎勵,拿一堆沒有獎勵的 epoch 來試,測不到這個問題。
等待時間也交給 Foundry,在 0 到 5 小時之間選。時間不同,解鎖的獎勵就不同;我沒有直接改合約裡存的獎勵數字。另外讓它選領款地址,以及由原本帳戶或另一個同樣賺過獎勵的帳戶呼叫。領款地址會變,但合約查的是呼叫者 msg.sender 的獎勵,兩件事不能混在一起。
第二次領款前先記下可領量,領款後看實際拿到多少。重複的 epoch 只能算一次。測試怎麼把輸入送進合約,摘幾行來看:
vm.warp(block.timestamp + bound(uint256(rawHours), 0, 5) * 1 hours);
uint256 secondEpoch = bound(uint256(rawEpoch), 12, 13);
uint256[] memory epochs = new uint256[](2);
epochs[0] = EPOCH;
epochs[1] = secondEpoch;
vm.prank(caller);
CONTROLLER.claimMultiple(epochs, recipient);
我把 fuzz 設成 24 次、seed 設成 0x123456。清掉舊反例的快取後,其中一次跑出來是這樣:
[PASS] testFuzz_Fixed(uint8,address,uint8,bool) (runs: 24)
[FAIL: paid more than distinct epochs allow: 8096335669924762955 > 7286702102932286660]
testFuzz_Vulnerable(uint8,address,uint8,bool) (runs: 1)
args=[139, 0x0000000000000000000000000000000000007253, 52, false]
epoch pair 13 13
被攻擊版在第 1 次就 FAIL。args 裡的 139 不是 epoch 139;測試會用 bound(rawEpoch, 12, 13) 把它換成 13。52 也會換成再等 4 小時,所以最後送進 claimMultiple 的是 [13, 13]。false 表示這次用原本的帳戶。
呼叫前,不重複計算 epoch 13 的話,還能領 7.2867 LVL;合約卻付了 8.0963 LVL。多的約 0.8096 LVL,就是之前已經領過的那部分。修補版在相同輸入範圍下跑滿 24 次,沒有出現這個失敗。
我清掉快取再跑一遍,同一個 seed 這次找到的是 rawEpoch=3、rawHours=91,跑到第 2 次才 FAIL。換算後仍是再等 1 小時、送入 [13, 13]。反例的原始數字不一定一樣,先看最後送進合約的是什麼。
假設只想測 epoch 12 和 13,可以把產生的數字壓進範圍,也可以把範圍外的數字丟掉:
// 把任何產生的數字映射到 12 或 13
uint256 epoch = bound(rawEpoch, 12, 13);
// 不在 12 到 13 之間就丟掉這組輸入
vm.assume(rawEpoch >= 12 && rawEpoch <= 13);
兩個測試用同一份快照、同一個 seed,最多允許拒絕 24 組輸入。結果:
[PASS] testFuzz_BoundSingleEpoch(uint256) (runs: 24)
[FAIL: `vm.assume` rejected too many inputs (24 allowed)]
testFuzz_AssumeSingleEpoch(uint256) (runs: 0)
bound 跑完了 24 次;assume 一組有效輸入都沒留下。它不是不能用,像排除零地址這種少數不需要的輸入就很適合。但從整個 uint256 範圍裡等到 12 或 13,幾乎只會一直丟掉輸入。
bound 的 24 次也不是 24 種 epoch,最後仍只有 12、13 兩種。這次有找到多付的反例;修補版跑滿 24 次,不能當成所有輸入都安全的證明。
明天把「同一份獎勵不能領兩次」寫成明確的測試條件,並把這次的反例留下來,之後改合約時還能重跑。
assume Cheatcode