# Last person to withdraw his tokens might not be able to do this, in Crowdsale (edge case)

Crowdsale finalize() can run before endTime when commitmentsTotal == totalTokens-1, yet commitEth still accepts one more token, leaving the last withdrawer unable to claim.

- Payout: Audit contest
- Severity: high
- Program: Sushi
- Found by: gpersoon
- Disclosed: 2021-11-05
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2021-09-sushimiso-findings/issues/15
- Library page: https://bugbountylibrary.com/r/c4%3A2021-09-sushimiso%3AH-03
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:solidity technique:race-window technique:source-review vuln:accounting-error vuln:business-logic

**Target:** Crowdsale.sol auction contract (commitEth/commitTokens/finalize/withdrawTokens)
**Root cause:** _addCommitment only checks marketInfo.endTime, not auctionEnded() or status.finalized, so commits are accepted after finalize() when auctionEnded() is true early via the +1 token rounding in _getTokenAmount.
**Steps:**
1. Deploy Crowdsale with totalTokens N; have commitmentsTotal reach N-1 before endTime.
2. auctionEnded() returns true because _getTokenAmount(commitmentsTotal+1) >= totalTokens; auctionSuccessful() also true.
3. Admin calls finalize(): distributes funds/unsold tokens, sets status.finalized=true.
4. Attacker calls commitEth() before endTime; calculateCommitment allows exactly 1 more token; _addCommitment passes since only endTime is checked.
5. Contract now holds N-1 tokens for N commitments; last withdrawTokens() call reverts on _safeTokenPayment due to missing token, and finalize() cannot be re-called to recover revenue.
**How found:** Code reading of auctionEnded()'s +1 rounding versus _addCommitment's missing finalized/auctionEnded check, spotting the off-by-one edge case where finalize precedes endTime.
**Exploitation:** Attacker waits for finalize() then submits a minimal commitEth() before endTime to buy the final token, permanently stranding one token and the associated revenue; last legitimate withdrawer cannot claim.
**Insight:** When a state-transition guard (auctionEnded) uses rounding like commitmentsTotal+1, check every other entry point that gates on time alone; finalize-before-endTime leaves a window where commits still succeed.

