Skip to content

test(invariant): fix Cursor Bugbot findings (I10 membership mgr + I12/I13 non-vacuity)#474

Merged
seongyun-ko merged 1 commit into
seongyun/fuzz/security-upgradesfrom
seongyun/fix/bugbot-470
Jun 15, 2026
Merged

test(invariant): fix Cursor Bugbot findings (I10 membership mgr + I12/I13 non-vacuity)#474
seongyun-ko merged 1 commit into
seongyun/fuzz/security-upgradesfrom
seongyun/fix/bugbot-470

Conversation

@seongyun-ko

@seongyun-ko seongyun-ko commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

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 fork

The fork suite re-deployed LiquidityPool and WithdrawRequestNFT passing address(membershipManagerInstance) as the membership-manager immutable. On a realistic mainnet fork that V0-typed var is never populated — only membershipManagerV1Instance is (from deployed.MEMBERSHIP_MANAGER()). So the new impls got address(0), diverging from the production bootstrap and mis-authing any membership-routed path.

Fix: use the real deployed MEMBERSHIP_MANAGER constant for both the LP and WRN constructors. (The contract already inherits Deployed.)

2. I12/I13 RewardsDistributor — invariants lacked non-vacuity

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 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/claim never fired.

Fixes:

  • targetSelector added — restrict fuzzing to the 7 action functions so every call advances the state machine.
  • afterInvariant() non-vacuity gate — assert finalize_ok, claim_ok, replay_revert all > 0 and total ETH paid > 0.
  • Self-contained 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

  • Rewards suite: 256 runs × 16,384 calls, 0 reverts, genuinely 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 (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-deploying LiquidityPool and WithdrawRequestNFT on a mainnet fork, the suite now passes the real MEMBERSHIP_MANAGER from Deployed instead of membershipManagerInstance, which stays unset on a realistic fork and had been wiring address(0) into the new impls.

I12/I13 RewardsDistributor: Fuzzing is limited with targetSelector to 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 increments finalize_ok inside _establishRoot, and doClaim performs 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.

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.
@seongyun-ko seongyun-ko marked this pull request as ready for review June 15, 2026 18:30
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@seongyun-ko seongyun-ko merged commit cf3eda9 into seongyun/fuzz/security-upgrades Jun 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant