# Provider can withdraw funds before commitments are opened

Primev ProviderRegistry counts commitments only at openCommitment, so a provider can store a commitment, unstake, withdraw after the delay, then open it—escaping slashing.

- Payout: Contest reward
- Severity: medium
- Program: Primev
- Credited: amaron
- Disclosed: 2025-08-27
- Write-up quality: 6/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_primev_oct2024.pdf#3.2.9
- Library page: https://bugbountylibrary.com/r/cantina%3Aprimev_oct2024%3A3.2.9
- Tags: domain:web3 impact:funds-loss kind:contest platform:cantina sev:medium tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** ProviderRegistry.sol staking/commitment contract
**Root cause:** commitmentsCount is incremented only in openCommitment, while provider validity is checked only in storeUnopenedCommitment; withdraw's pending-commitment guard therefore misses stored-but-unopened commitments.
**Steps:**
1. Call `storeUnopenedCommitment` while still a valid provider (validity check passes).
2. Immediately call `unstake`, setting `withdrawalRequests[msg.sender] = block.timestamp`.
3. Wait until `block.timestamp >= withdrawalRequests[msg.sender] + withdrawalDelay`.
4. Call `withdraw`: `providerPendingCommitmentsCount == 0` passes because commitmentsCount was never incremented.
5. After funds are withdrawn, call `openCommitment` (no validity check) to activate the commitment.
**How found:** Code reading of the commitment lifecycle: noticed commitmentsCount is only updated in openCommitment while the validity check lives in storeUnopenedCommitment, creating a window where stored commitments are invisible to withdraw's guard.
**Exploitation:** Provider stores a commitment, unstakes before opening it, waits out withdrawalDelay, withdraws stake since commitmentsCount is still zero, then opens the commitment—leaving no slashable stake for misbehavior.
**Insight:** When a protocol tracks pending obligations with a counter, check every state transition that creates an obligation—here storeUnopenedCommitment creates one but only openCommitment increments the counter, so the exit guard can be bypassed by ordering calls.
