test(fuzzing): guard YS invariant against unreachable empty-while-vesting state - #771
Conversation
…ting state InvariantTestYS Rule 14/15 (startVestingTime <= endVestingTime) started failing in nightly-fuzz once main advanced to include the YS system. Root cause is the acknowledged Finding 1: setFirstDepositTimestamp() moves startVestingTime without clearing a stale endVestingTime, reachable only when a vault hits totalSupply == 0 while a vest is still active and is then re-entered. That state is unreachable in production (deprecated vaults never fully drain, let alone mid-vest) and the source is intentionally WONTFIX. The prior handler workaround let the bad state happen then repaired it with a best-effort vestYield that reverts under the contract's own guards, so it silently left the invalid state and the invariant caught it — a flaky failure. Replace repair with prevention: the YS withdraw handlers now refuse to drain the vault to zero while vestingGains > 0 (vestYield already refuses to vest into an empty vault), so totalSupply == 0 always implies vestingGains == 0 and Rule 14/15 cannot be violated without weakening what they check. Remove the now-dead repair function from both the Foundry and Medusa handlers and document the rationale in the fuzzing README. Verified: FOUNDRY_PROFILE=nightly forge test --match-path test/fuzzing/InvariantTestYS.sol --fuzz-seed 0x2 -> 51 passed, 0 failed (previously 2 failed on the same seed and depth). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR replaces a best-effort vesting-state repair in the YS fuzz handlers with a withdrawal clamp intended to prevent an acknowledged, production-unreachable empty-vault state.
Confidence Score: 4/5The PR appears safe to merge, with a non-blocking fuzz-coverage gap around full withdrawals whose requested time advance completes the active vest. The new guard stabilizes the targeted invariant and is shared by both fuzzers, but its pre-warp state check can reduce or skip a withdrawal that would be valid when the actual call executes after vest completion. Files Needing Attention: test/fuzzing/handlers/TellerHandler.sol Important Files Changed
Sequence DiagramsequenceDiagram
participant F as Fuzzer
participant H as TellerHandler
participant A as YS Accountant
participant T as YS Teller
F->>H: withdrawYS(full shares, timeDelta)
H->>A: read vestingState()
A-->>H: "vestingGains > 0"
H->>H: clamp shares or skip
H->>H: warp by timeDelta
Note over H,A: Vest may now be complete
H->>A: sync vested assets
H->>T: withdraw(clamped shares)
T->>A: updateExchangeRate()
A-->>T: realize completed vest
Reviews (1): Last reviewed commit: "test(fuzzing): guard YS invariant agains..." | Re-trigger Greptile |
|
|
||
| uint256 actorShares = vaultYS.balanceOf(actor); | ||
| shareAmount = bound(shareAmount, 1, actorShares); | ||
| shareAmount = _clampYSWithdrawForActiveVest(shareAmount); |
There was a problem hiding this comment.
Clamp uses pre-warp vest state
withdrawYS clamps the amount before applying timeDelta, so a vest that completes during the requested warp is still treated as active. The handler consequently leaves dust—or skips a one-share withdrawal entirely—instead of exercising the valid completed-vest full-withdrawal path.
Knowledge Base Used: CI, Testing, and Fuzzing
What
Fixes the recurring nightly-fuzz failure in
test/fuzzing/InvariantTestYS.sol— Rule 14/15 (startVestingTime <= endVestingTime), which began failing oncemainadvanced to include the yield-streaming system.Root cause (not a regression)
The acknowledged Finding 1 (
setFirstDepositTimestampbug):setFirstDepositTimestamp()setsstartVestingTimewithout clearing a staleendVestingTime, combined with_updateExchangeRate()not clearingvestingGainsattotalSupply == 0. The bad state (start > endwithvestingGains > 0) is reachable only when a vault empties tototalSupply == 0while a vest is still active and is then re-entered.That state is unreachable in production (deprecated vaults never fully drain, let alone mid-vest), so the source is intentionally WONTFIX. The nightly went red because the prior handler workaround let the bad state happen and then tried to repair it with a best-effort
vestYield— which reverts under the contract's own guards, silently leaving the invalid state for the invariant to catch (a flaky failure that surfaced once the seedless nightly campaign found the sequence).Change (test-only: prevention over repair)
withdrawYS,bulkWithdrawYS) now refuse to drain the YS vault to zero whilevestingGains > 0— leaving dust or skipping.vestYieldalready refuses to vest into an empty vault, so this makestotalSupply == 0always implyvestingGains == 0, and Rule 14/15 can no longer be violated without weakening what they check for real regressions.fixVestingStateAfterFirstDeposit) from both the Foundry and Medusa handlers.Covers both the Foundry and Medusa YS suites (shared
TellerHandler). Net −150 / +52 lines.Verification
Same seed (
0x2) and depth (256×500) as the failing nightly run #66.🤖 Generated with Claude Code