← All postsEngineering

Designing a multi-chain liquidation engine

Three chains. One liquidation queue. Zero double-spends. Here's how we built it, and the corner case that almost shipped.

Manibabu Pippalla
CTO
6 min read
Designing a multi-chain liquidation engine

Liquidation across one chain is a solved problem. Aave does it. Compound does it. We did it on Base in our first month. Liquidation across THREE chains, where the same collateral can in principle be referenced on any of them, that's the problem that almost shipped a double-spend in our staging environment. Here's how we caught it, and what we built instead.

The double-spend that almost happened. User borrows USDC on Base against fAAPL collateral. Bridges fAAPL to Arbitrum. Borrows again against the same fAAPL. Bridges back. The naive design, a separate liquidation queue per chain, doesn't see the cross-chain leverage until it's too late. Health factor on each chain individually looks fine. Aggregate health factor is underwater.

We caught it in staging two days before launch. Reverted the chain-isolated design. Rebuilt around a single global queue.

Single global queue, multi-chain triggers. Every position contributes to a single global health factor, regardless of which chain holds the collateral. The queue lives on Base (where settlement is fastest), but liquidation triggers can fire from any chain. The flow:

  1. Per-chain health monitor watches LTV against a 30-second TWAP, per asset.
  2. When any chain's monitor flags below 1.05, it submits a candidate event to Base.
  3. Base aggregates candidate events into the global queue, ordered by underwater severity.
  4. 12-hour grace period kicks off. Borrower can top up collateral on any chain.
  5. If grace expires, liquidator on Base wins the auction and gets cross-chain seizure rights.

The 12-hour grace. Most lending protocols liquidate the moment health drops below 1. We hold for twelve hours. Long enough for a borrower to wake up, see a webhook, and top up. We chose this because our typical user is a fintech with a real ops team, not a retail wallet. Speed of liquidation isn't the goal. Avoiding wrongful liquidation is. Health-factor webhooks fire at 1.3, 1.15, and 1.05, so the borrower sees three escalations before the timer starts.

Most lending protocols liquidate the moment health drops below 1. We hold for twelve hours.

Corner cases worth knowing:

  • Bridge in-flight. A position bridging at the moment a liquidation triggers is held in a frozen state on the destination chain until the source attestation lands. We hold for up to 60 seconds.
  • Oracle TWAP gap. During a halt or a venue outage, the TWAP can stale. We pause new liquidations if any feed is older than 90 seconds.
  • Partial fill. Liquidators can take up to 50% of a position per call to avoid wiping borrowers on a temporary dislocation. The grace timer resets if the remainder returns to health.

What we got wrong the first time. We initially priced the liquidation auction by gas-on-Base alone. Liquidators on Arbitrum couldn't compete. We rewrote the auction to publish the rate on Base but accept claims from any chain, with the gas differential subsidized from the liquidation insurance fund. Net effect: more liquidator competition, tighter spread to the borrower, lower realized losses.

The lesson. When you build cross-chain, every assumption about local cost has to become a global one.

Written by Manibabu Pippalla, CTO
More posts →