Skip to main content

Liquidations

Sources:

DobDex liquidations are just sells the validator has flagged with a protocol-mandated discount. There is no separate liquidate(...) entrypoint on the hook or on DobDirectSwap. A holder calls the same swap they always would; the hook detects the asset is in liquidation mode and applies a penalty (penaltyBps) instead of routing through the normal LP-priced exit path.

Enabling liquidation for an asset

The validator (the DobValidatorRegistry owner) calls:

// DobValidatorRegistry — three-arg signature
function setLiquidationParams(address token, uint16 penaltyBps, uint256 cap) external onlyOwner;
function disableLiquidation(address token) external onlyOwner;
function setGlobalLiquidationCap(uint256 cap) external onlyOwner;
ParameterMeaning
tokenThe dobRWA token to put into liquidation mode.
penaltyBpsPenalty in bps. Must be 1..10000 (e.g. 2000 = 20%).
capPer-asset dobRWA cap on cumulative liquidations.

Calling setLiquidationParams emits LiquidationEnabled(token, penaltyBps, cap). disableLiquidation emits LiquidationDisabled(token) and resets the per-asset running counter. There is no per-call isLiquidatable argument — enabled state is implicit in LiquidationData.enabled.

The global cap (globalLiquidationCap) lives on its own setter. The hook checks it on every liquidation swap.

Liquidation flow

1.  Validator     -> registry.setLiquidationParams(token, penaltyBps, cap)
2. Holder -> swap(dobRWA -> USDC) on the V4 pool (with rwaToken in hookData)
3. Hook -> registry.getLiquidationParams(token) (sees enabled = true)
4. Hook -> per-asset cap + global cap checks
revert LiquidationCapExceeded / GlobalLiquidationCapExceeded
5. Hook -> amountOut = amountIn * (10000 - penaltyBps) / 10000
penaltyAmount = amountIn - amountOut
6. Hook -> registry.recordLiquidation(token, amountIn)
7. Hook -> lpRegistry.queryAndFill(token, oraclePrice, penaltyBps, amountOut)
iterates LPs whose minPenaltyBps <= penaltyBps, FIFO/cheapest-first
8. Hook -> emits LiquidationSwap(user, token, amountIn, amountOut, penaltyAmount)
9. Holder -> receives `amountOut` USDC; LPs receive dobRWA at the discounted rate

The penalty is the discount. Because the hook reads the oracle price for staleness only (not for output sizing), the holder's USDC out is amountIn * (1 - penalty) regardless of pool depth.

Why orderly?

Two anti-cascade properties:

  • Per-asset cap (liquidations[token].cap) — once liquidatedAmount + amountIn > cap, the next swap reverts LiquidationCapExceeded. Subsequent liquidations have to wait for the cap to be raised.
  • Global cap (globalLiquidationCap) — same idea across all assets. 0 means unlimited.

These are hard ceilings, not curves; the protocol can pause the cascade by simply leaving the cap unchanged.

LP-side: who fills a liquidation

queryAndFill walks the per-asset backer list (sorted ascending by minPenaltyBps) and skips any LP that:

  • is younger than MIN_BACKING_AGE = 1 hour,
  • has oraclePrice < backing.minOraclePrice,
  • has currentPenaltyBps < backing.minPenaltyBps,
  • has no usdcAllocated - usdcUsed left, or no maxExposure - currentExposure left.

Each filled LP is charged PROTOCOL_FEE_BPS = 150 (1.5%) and credited dobRWA at the protocol-mandated discount:

dobRwaForLP = fillUsdc * 10000 / (10000 - penaltyBps)

Net USDC (after fee) is transferred to the hook, which uses it to pay the seller.

Reserve hold on stop-backing during distress

If an LP calls stopBacking(token) while the asset is currently distressed (liquidation enabled with penaltyBps > 0), RESERVE_BPS = 33% of their unused USDC is held in a ReserveHold for up to RESERVE_WITHDRAWAL_DELAY = 7 days (or until the asset returns to healthy). See Liquidity Nodes for the full mechanism.

This is the only place the 33% reserve applies. It is not a continuous lock on every LP's deposit.

Events you'll see

EventSourceMeaning
LiquidationEnabled(token, penaltyBps, cap)DobValidatorRegistryValidator flagged the asset.
LiquidationDisabled(token)DobValidatorRegistryValidator cleared the flag (counter reset).
GlobalLiquidationCapSet(cap)DobValidatorRegistryGlobal cap updated.
LiquidationRecorded(token, amount, totalLiquidated)DobValidatorRegistryHook recorded a liquidation swap (only the hook can call).
LiquidationSwap(user, token, amountIn, amountOut, penaltyLocked)DobPegHookHook completed the discounted swap.
FillExecuted(lp, token, usdcAmount, dobRwaAmount)DobLPRegistryAn LP was filled.
LPFill(lpUsdcFilled, lpDobRwaOwed, protocolUsdcUsed)DobPegHookAggregated breakdown for the swap.

Comparison with traditional AMM liquidation

AspectTraditional AMMDobDex
Price impactProportional to trade sizeFixed penaltyBps discount
Cascade riskHigh — selling drops the curve, more positions get liquidatedBounded by per-asset and global caps
Seller certaintyUnknown final priceKnown: amountIn * (1 - penalty)
LP returnsIL exposureDiscounted RWA tokens (capped, fee-taxed)
RecoveryCurve may not recoverValidator can disable liquidation when conditions improve

Admin / validator controls

ActionEffect
Increase penaltyBpsLarger discount; attracts more LPs; sellers get less.
Decrease penaltyBpsSmaller discount; fewer LPs may match (minPenaltyBps filter).
Adjust per-asset capHard ceiling on how much can be liquidated for the asset.
Adjust globalLiquidationCapHard ceiling across all assets.
disableLiquidation(token)Returns the asset to the normal LP-priced sell path; per-asset counter resets.
pause() / unpause()Emergency stop on the registry.

All on-chain — every change is fully auditable via the events above.