test: atomic I3 finalize+lock (Bugbot #470 "Lock finalize not rolled back")#480
Merged
seongyun-ko merged 1 commit intoJun 26, 2026
Conversation
…ock finalize not rolled back") Bugbot (Medium): in WithdrawalSolvencyHandler.finalizeRequests the lock (addEthAmountLockedForWithdrawal) and finalize (finalizeRequests) ran as two separate calls. A committed lock followed by a finalize revert orphaned the lock: ethAmountLockedForWithdrawal stayed bumped while lastFinalizedRequestId did not advance, so a later fuzz step could re-lock the SAME id range and move extra ETH out of the LP. That diverges from production (EtherFiAdmin._finalizeWithdrawals, EtherFiAdmin.sol:427-428), which runs finalizeRequests then addEthAmountLockedForWithdrawal in ONE atomic transaction. Fix: route both legs through a single external self-call (this._finalizeThenLock) in production order (finalize -> lock), wrapped in try/catch. The shared frame gives all-or-nothing semantics: if either leg reverts, BOTH state changes roll back, so no committed lock can outlive a failed finalize and no id range is ever locked twice. P1 enforcement observation is preserved (success => bound held; InsufficientLiquidity revert => bound genuinely violated) and is still positively driven by probeOverLiquidityLock. Verified on mainnet fork: suite 7/7 pass, non-vacuity gates all positively satisfied (requestsCreated=31, finalized=5, claimed=10, finalizeBoundChecks=5, lockBoundEnforced=17).
|
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.
What this is
Addresses the one remaining open Bugbot finding on #470 — "Lock finalize not rolled back" (Medium) — by making the I3 handler's finalize+lock atomic, exactly like production. Test-only; no
src/changes.The finding
In
WithdrawalSolvencyHandler.finalizeRequests, the lock (addEthAmountLockedForWithdrawal) and the finalize (finalizeRequests) ran as two separate calls. A successful lock followed by a finalize revert orphaned the lock:ethAmountLockedForWithdrawalstayed bumped whilelastFinalizedRequestIddid not advance, so a later fuzz step could re-lock the same id range and move extra ETH out of the LP. Production never exposes that window —EtherFiAdmin._finalizeWithdrawals(EtherFiAdmin.sol:427-428) runsfinalizeRequeststhenaddEthAmountLockedForWithdrawalin one atomic transaction.The fix
Route both legs through a single external self-call
this._finalizeThenLock(target, lockAmount)in production order (finalize → lock), wrapped intry/catch. The shared call frame gives all-or-nothing semantics: if either leg reverts, both state changes roll back — no committed lock can outlive a failed finalize, and no id range is ever locked twice.InsufficientLiquidityrevert ⇒ bound genuinely violated).probeOverLiquidityLockop, so the non-vacuity gate never depends on the fuzzer stumbling onto an over-bound range here.Verification
Re-ran the full suite on a mainnet fork — 7/7 pass, with the non-vacuity gates all positively satisfied:
(Fork run done via a local anvil fork node pinned to block 25368000 to dodge a forge-1.5.1 anvil-IPC regression on this machine; the suite itself is unmodified beyond this handler.)
Status of the other #470 Bugbot findings
For the record, the other six are already resolved at the current
seongyun/fuzz/security-upgradesHEAD:MEMBERSHIP_MANAGERdeployed constant (commit 1174666), with an inline comment covering exactly this.afterInvariantgate + deterministic lifecycle seed are already present (commits 9018e8f / c4cc526).This PR closes the last one.
Note
Low Risk
Test-only invariant handler change; production contracts are untouched.
Overview
The I3 withdrawal solvency fuzz handler no longer runs lock and finalize as separate steps. It now mirrors
EtherFiAdmin._finalizeWithdrawals:finalizeRequestsfirst, thenaddEthAmountLockedForWithdrawal, inside a singlethis._finalizeThenLockself-call wrapped intry/catch.That gives production-like all-or-nothing behavior on the fork: if either leg reverts, both roll back, so the handler cannot leave an orphaned lock (bumped
ethAmountLockedForWithdrawalwithout advancinglastFinalizedRequestId) and then re-lock the same id range on a later fuzz step.P1 checks on successful locks and
InsufficientLiquidityreverts are unchanged; adversarial coverage still comes fromprobeOverLiquidityLock. Test-only change inWithdrawalSolvencyHandler.sol— nosrc/changes.Reviewed by Cursor Bugbot for commit d090237. Bugbot is set up for automated code reviews on this repo. Configure here.