Liquidations
Sources:
Dobhooks/contracts/src/DobValidatorRegistry.sol— liquidation flag and parameters.Dobhooks/contracts/src/DobPegHook.sol—_beforeSwapliquidation path.Dobhooks/contracts/src/DobLPRegistry.sol—queryAndFillLP matching.
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;
| Parameter | Meaning |
|---|---|
token | The dobRWA token to put into liquidation mode. |
penaltyBps | Penalty in bps. Must be 1..10000 (e.g. 2000 = 20%). |
cap | Per-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) — onceliquidatedAmount + amountIn > cap, the next swap revertsLiquidationCapExceeded. Subsequent liquidations have to wait for the cap to be raised. - Global cap (
globalLiquidationCap) — same idea across all assets.0means 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 - usdcUsedleft, or nomaxExposure - currentExposureleft.
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
| Event | Source | Meaning |
|---|---|---|
LiquidationEnabled(token, penaltyBps, cap) | DobValidatorRegistry | Validator flagged the asset. |
LiquidationDisabled(token) | DobValidatorRegistry | Validator cleared the flag (counter reset). |
GlobalLiquidationCapSet(cap) | DobValidatorRegistry | Global cap updated. |
LiquidationRecorded(token, amount, totalLiquidated) | DobValidatorRegistry | Hook recorded a liquidation swap (only the hook can call). |
LiquidationSwap(user, token, amountIn, amountOut, penaltyLocked) | DobPegHook | Hook completed the discounted swap. |
FillExecuted(lp, token, usdcAmount, dobRwaAmount) | DobLPRegistry | An LP was filled. |
LPFill(lpUsdcFilled, lpDobRwaOwed, protocolUsdcUsed) | DobPegHook | Aggregated breakdown for the swap. |
Comparison with traditional AMM liquidation
| Aspect | Traditional AMM | DobDex |
|---|---|---|
| Price impact | Proportional to trade size | Fixed penaltyBps discount |
| Cascade risk | High — selling drops the curve, more positions get liquidated | Bounded by per-asset and global caps |
| Seller certainty | Unknown final price | Known: amountIn * (1 - penalty) |
| LP returns | IL exposure | Discounted RWA tokens (capped, fee-taxed) |
| Recovery | Curve may not recover | Validator can disable liquidation when conditions improve |
Admin / validator controls
| Action | Effect |
|---|---|
Increase penaltyBps | Larger discount; attracts more LPs; sellers get less. |
Decrease penaltyBps | Smaller discount; fewer LPs may match (minPenaltyBps filter). |
Adjust per-asset cap | Hard ceiling on how much can be liquidated for the asset. |
Adjust globalLiquidationCap | Hard 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.