Skip to content

test: atomic I3 finalize+lock (Bugbot #470 "Lock finalize not rolled back")#480

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

test: atomic I3 finalize+lock (Bugbot #470 "Lock finalize not rolled back")#480
seongyun-ko merged 1 commit into
seongyun/fuzz/security-upgradesfrom
seongyun/fix/bugbot-470-finalize-atomicity

Conversation

@seongyun-ko

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

Copy link
Copy Markdown
Contributor

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: 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. Production never exposes that window — EtherFiAdmin._finalizeWithdrawals (EtherFiAdmin.sol:427-428) runs finalizeRequests then addEthAmountLockedForWithdrawal in one atomic transaction.

The fix

Route both legs through a single external self-call this._finalizeThenLock(target, lockAmount) in production order (finalize → lock), wrapped in try/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.

  • P1 enforcement observation is preserved (success ⇒ bound held; InsufficientLiquidity revert ⇒ bound genuinely violated).
  • P1 is still positively/adversarially driven every run by the dedicated probeOverLiquidityLock op, 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:

requestsCreated     : 31
requestsFinalized   : 5
requestsClaimed     : 10
finalizeBoundChecks : 5
lockBoundEnforced   : 17

(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-upgrades HEAD:

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: finalizeRequests first, then addEthAmountLockedForWithdrawal, inside a single this._finalizeThenLock self-call wrapped in try/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 ethAmountLockedForWithdrawal without advancing lastFinalizedRequestId) and then re-lock the same id range on a later fuzz step.

P1 checks on successful locks and InsufficientLiquidity reverts are unchanged; adversarial coverage still comes from probeOverLiquidityLock. Test-only change in WithdrawalSolvencyHandler.sol — no src/ changes.

Reviewed by Cursor Bugbot for commit d090237. Bugbot is set up for automated code reviews on this repo. Configure here.

…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).
@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 fde00aa into seongyun/fuzz/security-upgrades Jun 26, 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