# Accounts will not be liquidated when they are meant to.

Perennial's liquidate() reads collateral before closeAll debits close fees, so fee debit underflows and reverts, blocking liquidation of underwater accounts.

- Payout: Contest reward
- Severity: medium
- Program: Perennial
- Credited: 0xGoodess
- Disclosed: 2023-05-05
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-05-perennial-judging/issues/132
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-05-perennial%3AM-12
- Tags: domain:web3 impact:dos impact:funds-loss kind:contest platform:sherlock sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:dos vuln:liquidation

**Target:** Collateral.sol liquidate() in Perennial collateral contract
**Root cause:** totalCollateral is fetched before product.closeAll(), which debits closePosition fees from the account, so the fee computed from the stale (higher) balance exceeds the actual post-close balance and debitAccount underflows.
**Steps:**
1. Configure ETH-long market: maintenance=5%, minCollateral=100 USDC, liquidationFee=20%, ETH=$1000.
2. User opens $10000 (10 ETH) position with 500 USDC collateral.
3. ETH price rises to $6000; maintenance=60000*5%=$3000 > 500 USDC collateral, so account is liquidatable.
4. Watcher calls liquidate(account, product): totalCollateral=500 USDC.
5. product.closeAll(account) closes position and debits ~10 USDC makerFee; actual balance=490 USDC.
6. collateralForFee=max(3000,100)=3000; fee=min(500,3000*20%)=500.
7. debitAccount(account,500) subtracts 500 from 490 -> underflow revert; liquidation fails.
**How found:** Code reading of liquidate(): noticed totalCollateral is read before closeAll() and reused for the fee, while closeAll debits close fees, implying the fee debit can exceed the remaining balance.
**Exploitation:** Attacker opens an extremely high-leverage position (e.g. 120x vs 20x max) and waits for price to move ~1/liquidationFee (6x here); every liquidation attempt reverts on underflow, leaving the bad debt unliquidated and the protocol exposed.
**Escalation:** Any price move exceeding 1/liquidationFee makes liquidation permanently revert, letting users hold positions far beyond max leverage and accumulate bad debt.
**Insight:** When a function reads a balance, then calls an external/internal routine that mutates that balance, then debits based on the stale value, check whether the mutation (fees, settlements) can push the debit past the real balance and revert the whole path — a DoS on liquidation/withdrawal logic.
