# Underwater borrower can evade liquidations indefinitely by compensating loan

Underwater borrower front-runs liquidate() by calling compensate() to rotate debt/credit position IDs, making the old debt position repaid and unliquidatable.

- Payout: Contest reward
- Severity: medium
- Program: Size
- Credited: flacko
- Disclosed: 2025-04-03
- Write-up quality: 7/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_size_february2025.pdf#3.1.1
- Library page: https://bugbountylibrary.com/r/cantina%3Asize_february2025%3A3.1.1
- Tags: domain:web3 impact:funds-loss kind:contest platform:cantina sev:medium tech:evm tech:solidity technique:race-window vuln:business-logic vuln:liquidation

**Target:** Size.sol credit marketplace liquidate/compensate actions
**Root cause:** compensate() lets the borrower repay a debt position using a credit position, creating fresh debt/credit positions and marking the old debt position repaid with zero future value. No check that the borrower's collateralization ratio improves after compensation.
**Steps:**
1. Deposit collateral and borrow via `_sellCreditMarket(bob, alice, RESERVED_ID, 100e6, 365 days, false)` creating debtPositionId and creditPositionId.
2. Drop price via `_setPrice(2e18)` so `size.isUserUnderwater(bob)` returns true.
3. Borrower calls `size.compensate(CompensateParams({creditPositionWithDebtToRepayId: creditPositionId, creditPositionToCompensateId: RESERVED_ID, amount: type(uint256).max}))`.
4. Old debtPosition.futureValue becomes 0 and is marked REPAID.
5. Liquidator's `_liquidate(liquidator, debtPositionId)` reverts with `LOAN_NOT_LIQUIDATABLE`.
6. Borrower remains underwater but unliquidatable; repeat each time liquidation is attempted.
**How found:** Read the compensate() flow in Size.sol#L407-L415 and noticed the only validation is tenor range; realized compensation zeroes the old debt position's future value, so a liquidator targeting that ID reverts. Combined with mainnet front-running feasibility noted in README.
**Exploitation:** Borrower monitors mempool on Ethereum mainnet and front-runs any liquidate() call by invoking compensate() on their own debt position, rotating to new position IDs. Liquidator's tx reverts against the now-repaid position. Only constraint is new tenor must stay within market risk config (min 1 hour for WETH/USDC), so borrower can postpone liquidation until the final hour of the tenor window.
**Insight:** When a protocol offers a borrower-initiated 'refinance/rotate' action that mints new position IDs, check whether it invalidates the IDs liquidators target and whether it enforces a collateralization-ratio improvement; position-ID rotation is a liquidation-evasion primitive.
