# BidderRegistry can be drained by reentering BidderRegistry::openBid()

BidderRegistry.openBid() sends ETH to bidder before updating state; reentrancy plus re-storing an opened commitment lets a malicious bidder drain the contract.

- Payout: Contest reward
- Severity: high
- Program: Primev
- Credited: philmnds
- Disclosed: 2025-08-27
- Write-up quality: 7/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_primev_oct2024.pdf#3.1.7
- Library page: https://bugbountylibrary.com/r/cantina%3Aprimev_oct2024%3A3.1.7
- Tags: domain:web3 impact:funds-loss kind:contest platform:cantina sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:reentrancy

**Target:** BidderRegistry / PreconfManager smart contracts
**Root cause:** openBid() performs an external value transfer to the bidder before setting bidState.state, and storeUnopenedCommitment() lacks a check preventing re-storing an already-opened commitment, so the isOpened guard can be reset.
**Steps:**
1. Deploy MaliciousProvider with steal() calling PreconfManager.storeUnopenedCommitment() then openCommitment().
2. Deploy MaliciousBidder whose receive() calls MaliciousProvider.steal() while BidderRegistry has balance.
3. Register MaliciousProvider on ProviderRegistry and MaliciousBidder on BidderRegistry, fund bidder with x.
4. Store commitment via storeUnopenedCommitment() with bid > x so availableAmount < bid triggers external call.
5. Call PreconfManager.openCommitment() to trigger reentrancy loop and drain BidderRegistry.
**How found:** Noticed openBid() makes external call before state update (classic reentrancy), then checked the isOpened guard and found storeUnopenedCommitment() does not verify prior storage, allowing the guard to be reset.
**Exploitation:** Malicious bidder's receive() reenters via storeUnopenedCommitment() to reset isOpened, then openCommitment() re-triggers openBid()'s external call repeatedly, each time receiving bid - availableAmount until the contract is drained in one transaction.
**Insight:** When a reentrancy guard relies on a flag set elsewhere, verify the setter can't be re-invoked to clear it; here the 'already opened' flag was resettable via an unguarded store function, turning a one-shot guard into a loop.
