# Deleveragable Positions Cannot Be Fully Liquidated

Fully liquidating a deleveragable position reverts because total_risk becomes zero, tripping assert_healthy_or_healthier and leaving insolvent positions open.

- Payout: Contest reward
- Severity: medium
- Program: Starknet Perpetual
- Credited: handsomegiraffe
- Disclosed: 2025-06-18
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-03-starknet-perpetual#m-01
- Library page: https://bugbountylibrary.com/r/c4%3A2025-03-starknet-perpetual%3AM-01
- Tags: domain:web3 impact:dos kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:business-logic vuln:liquidation

**Target:** Perpetuals contract liquidation / value_risk_calculator.cairo
**Root cause:** assert_healthy_or_healthier panics when tvtr.after.total_risk is zero, which is exactly the state after fully liquidating a deleveragable position (no synthetic exposure remains).
**Steps:**
1. Set up a position with negative synthetic balance (e.g. -SYNTHETIC_BALANCE_AMOUNT) making TV < 0 (deleveragable).
2. Construct a liquidator order that fully offsets the position (base_amount = -BASE, quote_amount = -QUOTE).
3. Call `state.liquidate(operator_nonce, liquidator_signature, liquidated_position_id, liquidator_order, actual_amount_base_liquidated: BASE, actual_amount_quote_liquidated: QUOTE, actual_liquidator_fee: FEE, liquidated_fee_amount: INSURANCE_FEE)`.
4. Observe panic `POSITION_NOT_HEALTHY_NOR_HEALTHIER` because tvtr.after.total_risk == 0.
**How found:** Read the spec (liquidatable when TV<TR, deleveragable when TV<0) and traced the liquidation path into assert_healthy_or_healthier, noticing the zero-total_risk panic branch triggers precisely when a deleveragable position is fully closed.
**Insight:** When auditing liquidation/health-check asserts, test the terminal state where exposure and risk go to zero — guards that reject zero risk often block the exact full-close path liquidators need.
