fix: forward AuctionManager bid revenue + guard WRNFT finalize ordering#440
Conversation
…lize ordering Two follow-up fixes from the multi-lens audit of PR #385. H-5 (AuctionManager): pre-PR, every consumed bid forwarded `bid.amount` to `membershipManagerContractAddress` via `processAuctionFeeTransfer`. That fn was deleted in this branch and `updateSelectedBidInformation` now only flips `bid.isActive=false`, stranding the consumed-bid ETH in the contract. The `treasury` immutable was added but never read. This change rewires `updateSelectedBidInformation` to forward `bid.amount` to `treasury` immediately on consumption, matching the previously-removed revenue path. The `membershipManagerContractAddress` immutable and its constructor param are dropped (unused). Bid-cancellation refund is unaffected (`_cancelBid` still refunds the bidder). The currently-stranded ~0.097 ETH (`accumulatedRevenue` below the 0.2 ETH auto-forward threshold) is left to be handled separately; the per-call forward stops new revenue from getting trapped going forward. H-3 (WithdrawRequestNFT): `finalizeRequests` lacked any guard that the share-rate-freeze sentinel had been pushed. If `initializeShareRateFreezeUpgrade` is skipped between proxy upgrade and the first oracle report, the first `finalizeRequests(N)` pushes `(N, rateNow)` as the trace's first entry. Every legacy tokenId then resolves to `rateNow` via `lowerLookup` instead of the `0` sentinel that signals "fall back to live LP rate", silently mispaying every pre-upgrade holder. This change adds `NotInitialized` + a `length() == 0` precondition to `finalizeRequests`, making the upgrade-ordering trap impossible. Tests: - `TestSetup` pushes the sentinel as part of standard setup (matches post-upgrade reality on mainnet). - `WithdrawRequestNFTIntrusive` gains `clearFinalizationRatesForTest()` so the four tests that explicitly exercise the pre-init transition can reset the trace to length 0. - All AuctionManager constructor call-sites updated for the dropped param. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b0732d2. Configure here.
| (bool sent, ) = treasury.call{value: amount}(""); | ||
| if (!sent) revert EtherTransferFailed(); | ||
| emit BidRevenueForwarded(_bidId, treasury, amount); | ||
| } |
There was a problem hiding this comment.
reEnterAuction creates insolvency after bid ETH forwarded
Medium Severity
reEnterAuction re-activates a bid without restoring the ETH that updateSelectedBidInformation already forwarded to treasury. If called after consumption, bid.isActive becomes true again while the contract no longer holds bid.amount, breaking the invariant address(this).balance ≥ Σ(active bid amounts). A subsequent _cancelBid would attempt a refund the contract cannot cover.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b0732d2. Configure here.
…nto seongyun/fix/auction-revenue-wrnft-init-guard


Follow-ups to #385 from the multi-lens audit. Two H-rated findings; minimal-surface fixes.
H-5 · AuctionManager bid revenue trap
Pre-#385, every consumed bid forwarded
bid.amounttomembershipManagerContractAddressviaprocessAuctionFeeTransfer. That function was deleted in this branch andupdateSelectedBidInformationnow only flipsbid.isActive=false, stranding the consumed-bid ETH in the contract. The newtreasuryimmutable was added but never read.Fix: rewire
updateSelectedBidInformationto forwardbid.amount→treasuryimmediately on consumption. Drop the unusedmembershipManagerContractAddressimmutable (and its constructor param). EmitBidRevenueForwardedfor observability.Bid-cancellation refund is unchanged —
_cancelBidstill refunds the bidder. The on-chain invariantaddress(this).balance ≥ Σ(bid.amount for active bids)is preserved.Stranded today (not handled by this PR): ~0.097 ETH in
accumulatedRevenuebelow the 0.2 ETH auto-forward threshold. That can be left in the contract per the audit conversation and handled later if desired.H-3 · WithdrawRequestNFT init-order trap
finalizeRequestshad no guard thatinitializeShareRateFreezeUpgrade()had pushed the sentinel checkpoint. If afinalizeRequests(N)slipped in between the proxy upgrade and the init call, it would push(N, rateNow)as the trace's first entry. Every legacytokenId ≤ Nwould thenlowerLookuptorateNowinstead of the0sentinel that signals "fall back to live LP rate", silently mispaying every pre-upgrade holder.Fix: add
NotInitialized+ a_finalizationRates.length() == 0precondition tofinalizeRequests. The upgrade-ordering trap becomes impossible —finalizeRequestsreverts cleanly until the sentinel is in place.Test changes
TestSetup.solpushes the sentinel as part of standard setup (matches post-upgrade reality). Done in both the fresh-deploy path and the fork-upgrade path.WithdrawRequestNFTIntrusivegainsclearFinalizationRatesForTest()and a matching test-helper_clearFinalizationRatesForTest()so the four tests that explicitly exercise the pre-init transition can reset the trace.new AuctionManager(...)call-sites updated for the dropped constructor param.Test results
forge test --match-contract WithdrawRequestNFTTest→ 79/80 pass (1 fails due to missingMAINNET_RPC_URL, pre-existing).forge test --match-contract AuctionManagerTest→ 21/21 pass.MAINNET_RPC_URL/OP_RPC_URL).Deployment runbook addition
The upgrade timelock batch must call
WithdrawRequestNFT.initializeShareRateFreezeUpgrade()after the new impl is in place. The new guard makesfinalizeRequestsrevert cleanly until this happens, so the failure mode is loud rather than silent.Note
High Risk
Changes treasury ETH routing on bid consumption and gates withdrawal finalization on upgrade ordering—both affect protocol funds and pre-upgrade claim semantics if deployment steps are wrong.
Overview
This PR closes two audit follow-ups: consumed auction bid ETH no longer sits in
AuctionManager, andWithdrawRequestNFT.finalizeRequestscannot run before the share-rate-freeze sentinel exists.AuctionManager: When a bid is consumed via
updateSelectedBidInformation, the contract now sendsbid.amountto thetreasuryimmutable (withBidRevenueForwarded), replacing the removed membership-manager fee path. The unusedmembershipManagerContractAddressimmutable and constructor argument are dropped; allnew AuctionManager(...)sites are updated.WithdrawRequestNFT:
finalizeRequestsreverts withNotInitializedwhile_finalizationRatesis empty, so a finalize between proxy upgrade andinitializeShareRateFreezeUpgrade()cannot seed a real rate as the first checkpoint and break legacy live-rate fallback.Tests / deploy hygiene:
TestSetupand fork setup callinitializeShareRateFreezeUpgrade()by default; intrusive test helpers clear the checkpoint trace so pre-init behavior stays testable.Reviewed by Cursor Bugbot for commit 998a922. Bugbot is set up for automated code reviews on this repo. Configure here.