test(invariant): fix Cursor Bugbot findings (I10 membership mgr + I12/I13 non-vacuity)#474
Merged
seongyun-ko merged 1 commit intoJun 15, 2026
Conversation
Two Medium findings from Cursor Bugbot:
1. ValidatorStateMachine (I10): the fork suite re-deployed LiquidityPool and
WithdrawRequestNFT passing address(membershipManagerInstance) as the
membership-manager immutable, but on a realistic fork that V0-typed var is
never populated (only membershipManagerV1Instance is, from
deployed.MEMBERSHIP_MANAGER()). The new impls therefore got a zero membership
manager, diverging from the production bootstrap. Fixed to use the real
deployed MEMBERSHIP_MANAGER constant for both the LP and WRN constructors.
2. RewardsDistributor (I12/I13): the suite logged coverage counters but never
HARD-asserted that finalize/claim actually happened, so the ghost flags could
stay false and the invariants pass vacuously. Root causes found & fixed:
(a) no targetSelector -> the engine also fuzzed the handler's view getters,
wasting the call budget; added an explicit action-only selector list.
(b) added an afterInvariant() non-vacuity gate (finalize_ok / claim_ok /
replay_revert > 0 and total ETH paid > 0).
(c) made doClaim self-contained (it already finalizes via _establishRoot; now
also runs an inline replay-rejection and counts the finalize), so a single
call exercises finalize+claim+replay -- keeping the non-vacuity gate
satisfiable even when Foundry shrinks a failing run to a 1-call sequence.
Verified: rewards suite 256x16384 calls, 0 reverts, non-vacuous (finalize_ok=23,
claim_ok=9, replay_revert=16), stable across 3 seeds; I10 fork suite still passes
with the membership-manager fix. src untouched.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Address Cursor Bugbot findings on #470
Cursor Bugbot flagged 2 Medium issues on the merged invariant suites. Both are real; both fixed here.
src/untouched — test-only.1. I10
ValidatorStateMachine— unset membership manager on forkThe fork suite re-deployed
LiquidityPoolandWithdrawRequestNFTpassingaddress(membershipManagerInstance)as the membership-manager immutable. On a realistic mainnet fork that V0-typed var is never populated — onlymembershipManagerV1Instanceis (fromdeployed.MEMBERSHIP_MANAGER()). So the new impls gotaddress(0), diverging from the production bootstrap and mis-authing any membership-routed path.Fix: use the real deployed
MEMBERSHIP_MANAGERconstant for both the LP and WRN constructors. (The contract already inheritsDeployed.)2. I12/I13
RewardsDistributor— invariants lacked non-vacuityThe suite logged coverage counters but never hard-asserted that finalize/claim actually happened, so the ghost flags could stay false and the invariants pass without exercising the merkle-delay or payout logic. Worse, I found the run was actually vacuous: with no
targetSelector, the engine also fuzzed the handler's public view getters and wasted the call budget —setPending/finalize/claimnever fired.Fixes:
targetSelectoradded — restrict fuzzing to the 7 action functions so every call advances the state machine.afterInvariant()non-vacuity gate — assertfinalize_ok,claim_ok,replay_revertall> 0and total ETH paid> 0.doClaim— it already finalizes via_establishRoot(now counted) and runs an inline replay-rejection, so a single call exercises finalize + claim + replay. This keeps the non-vacuity gate satisfiable even when Foundry shrinks a failing run to a 1-call subsequence (the shrink-masking trap).Verification
finalize_ok=23, claim_ok=9, replay_revert=16), stable across 3 seeds.16×384 calls, 0 reverts).Base:
seongyun/fuzz/security-upgrades(the release line these suites were merged into via #468).Note
Low Risk
Changes are confined to invariant/fork tests and handlers; production
src/is untouched.Overview
Test-only fixes for two invariant suites flagged after merge: fork setup now matches production membership wiring, and the rewards fuzz run is forced to exercise the paths I12/I13 actually defend.
I10
ValidatorStateMachine: When re-deployingLiquidityPoolandWithdrawRequestNFTon a mainnet fork, the suite now passes the realMEMBERSHIP_MANAGERfromDeployedinstead ofmembershipManagerInstance, which stays unset on a realistic fork and had been wiringaddress(0)into the new impls.I12/I13
RewardsDistributor: Fuzzing is limited withtargetSelectorto the seven handler actions so calls are not wasted on view getters.afterInvariant()hard-requires at least one successful finalize, claim, and replay-revert plus non-zero total ETH paid. The handler incrementsfinalize_okinside_establishRoot, anddoClaimperforms an inline replay attempt so a single shrunk call sequence can still satisfy the non-vacuity gates.Reviewed by Cursor Bugbot for commit 1174666. Bugbot is set up for automated code reviews on this repo. Configure here.