Liquidation is the load-bearing wall of every lending protocol. When it works, nobody notices. When it fails — or when it works too well in the wrong direction — it can collapse the entire structure. The challenge is not implementing basic liquidation logic; that part is straightforward. The challenge is designing a system that remains safe when price moves are fast, oracle updates are slow, liquidator bots are competitive, and dozens of positions are crossing the threshold simultaneously.
This article walks through the full mechanical and architectural picture: how liquidations work at the contract level, why health factor math matters more than it looks, how liquidation bonuses create hidden tradeoffs, how cascades form and propagate, what oracles actually do to cascade timing, and how isolated versus cross-margin architectures distribute — or concentrate — the resulting risk.
How Liquidations Work Mechanically
Crypto lending protocols use automated smart contracts to issue loans backed by digital assets. To mitigate counterparty risk, these systems require overcollateralization — borrowers must lock up collateral worth more than the loan they are requesting.
The flow is straightforward in the normal case:
- A borrower deposits collateral and receives a debt position denominated in a different asset.
- The protocol tracks the ratio of collateral value to debt value in real time using oracle prices.
- When that ratio deteriorates past a defined threshold, the position becomes eligible for liquidation.
- Liquidators are typically automated bots running specialized algorithms that constantly monitor the health factors of open loans. When a position becomes undercollateralized, these bots race to trigger the liquidation function within the protocol smart contract. The liquidator repays a portion or the entirety of the borrower’s debt and receives an equivalent value of collateral, plus a bonus.
Aave V3 runs an open liquidation market — anyone can call liquidationCall() and seize collateral at a discount (typically 5–10%). Compound V3 uses an absorb mechanism rather than open liquidation. These are architectural choices with profound risk consequences, covered in a later section.
The distinguishing legal frame is worth being explicit about early. When collateral_value * loan_to_value_ratio < borrow_value, enough collateral exists to cover the loan, preventing bad debt if liquidated in time. Insolvency — when collateral_value < borrow_value — means insufficient collateral remains, leading to bad debt even after liquidation. The gap between “liquidatable” and “insolvent” is the window a protocol depends on. Designing for that window is most of the job.
The Health Factor Model
The health factor is the single number that drives everything in a lending protocol’s risk engine.
Health Factor reflects position solvency and indicates how close a position is to liquidation. It is calculated as eligible collateral value / debt value.
The Aave formulation adds per-asset liquidation thresholds as weights:
HF = Σ(collateral_i * liquidationThreshold_i) / totalBorrowValue
For example, if you supply $10,000 in ETH with an 80% liquidation threshold and borrow $6,000 in GHO, your health factor would be 1.333. Maintaining a health factor above 1 is crucial to avoid liquidation.
The health factor increases when debt is repaid or collateral value rises. It decreases as interest accrues, collateral value decreases, or new debt is taken. Positions become eligible for liquidation when their health factor drops below 1.0.
Here is a Solidity implementation of the core health factor calculation, following the weighted multi-collateral approach:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IPriceOracle {
/// @notice Returns the price of `asset` in USD, 8 decimals
function getPrice(address asset) external view returns (uint256 price, uint256 updatedAt);
}
struct AssetConfig {
uint16 liquidationThresholdBps; // e.g. 8000 = 80%
uint8 decimals;
}
struct UserPosition {
address[] collateralAssets;
uint256[] collateralAmounts;
address[] borrowedAssets;
uint256[] borrowedAmounts;
}
contract HealthFactorCalculator {
uint256 public constant HEALTH_FACTOR_PRECISION = 1e18;
uint256 public constant BPS_DENOMINATOR = 10_000;
uint256 public constant PRICE_DECIMALS = 1e8;
uint256 public constant MAX_ORACLE_STALENESS = 3600; // 1 hour
IPriceOracle public immutable oracle;
mapping(address => AssetConfig) public assetConfigs;
error StalePrice(address asset, uint256 updatedAt);
error ZeroPrice(address asset);
constructor(address _oracle) {
oracle = IPriceOracle(_oracle);
}
/// @notice Calculates the health factor for a given position.
/// @dev Returns HEALTH_FACTOR_PRECISION (1e18) == HF of 1.0.
/// Returns type(uint256).max if there is no debt.
function calculateHealthFactor(UserPosition calldata position)
external
view
returns (uint256 healthFactor)
{
uint256 totalWeightedCollateral = _getWeightedCollateralValue(position);
uint256 totalDebtValue = _getTotalDebtValue(position);
if (totalDebtValue == 0) return type(uint256).max;
healthFactor = (totalWeightedCollateral * HEALTH_FACTOR_PRECISION) / totalDebtValue;
}
/// @notice Returns true when the position is eligible for liquidation.
function isLiquidatable(UserPosition calldata position)
external
view
returns (bool)
{
uint256 hf = this.calculateHealthFactor(position);
return hf < HEALTH_FACTOR_PRECISION;
}
// ─── Internal helpers ───────────────────────────────────────────────────
function _getWeightedCollateralValue(UserPosition calldata position)
internal
view
returns (uint256 weightedValue)
{
for (uint256 i; i < position.collateralAssets.length; ++i) {
address asset = position.collateralAssets[i];
uint256 amount = position.collateralAmounts[i];
AssetConfig memory cfg = assetConfigs[asset];
uint256 price = _getSafePrice(asset);
// Normalise to 18 decimals before applying threshold
uint256 valueUSD = (amount * price) / (10 ** cfg.decimals * PRICE_DECIMALS / 1e18);
weightedValue += (valueUSD * cfg.liquidationThresholdBps) / BPS_DENOMINATOR;
}
}
function _getTotalDebtValue(UserPosition calldata position)
internal
view
returns (uint256 totalDebt)
{
for (uint256 i; i < position.borrowedAssets.length; ++i) {
address asset = position.borrowedAssets[i];
uint256 amount = position.borrowedAmounts[i];
AssetConfig memory cfg = assetConfigs[asset];
uint256 price = _getSafePrice(asset);
totalDebt += (amount * price) / (10 ** cfg.decimals * PRICE_DECIMALS / 1e18);
}
}
function _getSafePrice(address asset)
internal
view
returns (uint256 price)
{
(uint256 rawPrice, uint256 updatedAt) = oracle.getPrice(asset);
if (block.timestamp - updatedAt > MAX_ORACLE_STALENESS)
revert StalePrice(asset, updatedAt);
if (rawPrice == 0)
revert ZeroPrice(asset);
return rawPrice;
}
}
The staleness check on line 72 is not cosmetic. A price that is even 30 minutes stale during a volatile session is materially different from the current market price, and acting on it can produce liquidations at the wrong moment — or miss them entirely.
Liquidation Bonus Design and Its Tradeoffs
The liquidation bonus (sometimes called the liquidation discount or spread) is the incentive that makes the whole system work. Liquidators are not public servants; they are profit-seeking actors. Without a bonus, they have no reason to execute liquidations.
Aave and Compound use a fixed spread liquidation mechanism, otherwise known as a liquidation bonus or liquidation discount factor. The discount is typically 5–15%.
The fixed-bonus model has a hard failure mode. The bad debt incurred by Aave in November 2022 was not due to excess volatility in CRV/USDC price activity on that day, but rather a fundamental flaw in the liquidation logic which triggered a toxic liquidation spiral on the platform. The mechanism: when a position is deep underwater and the bonus is fixed, each partial liquidation removes more collateral value than it clears debt. The position’s health factor can actually worsen with each liquidation step, making bad debt accumulation deterministic once the spiral starts.
Aave V4 addresses this with a variable bonus model. Instead of a fixed close factor, liquidators repay only enough debt to restore the position to a Target Health Factor set at the Spoke level. The bonus paid to liquidators scales with the position’s health factor. Lower health factors offer higher bonuses, creating a Dutch-auction style incentive that prioritizes the riskiest positions.
Positions with lower health factors, which pose more risk to the protocol, now offer a higher liquidation bonus. This encourages liquidators to act quickly on the riskiest positions, which reduces the chance of bad debt accumulating. In a market downturn, when liquidations spike, this mechanism makes sure that the most dangerous positions are cleared first.
The following Solidity snippet demonstrates both the fixed-bonus and variable-bonus approaches:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title LiquidationIncentive
/// @notice Demonstrates fixed vs variable liquidation bonus calculation
library LiquidationIncentive {
uint256 constant PRECISION = 1e18;
uint256 constant BASE_BONUS_BPS = 500; // 5% base bonus
uint256 constant MAX_BONUS_BPS = 1500; // 15% max bonus
uint256 constant BPS_DENOMINATOR = 10_000;
// ─── Fixed bonus (Aave V3 style) ────────────────────────────────────────
/// @notice Returns collateral to seize for a fixed bonus.
/// @param debtToRepay Debt being repaid, in USD (18 dec)
/// @param bonusBps Protocol-configured bonus in basis points
/// @return collateralToSeize Collateral value to send to liquidator
function fixedBonusSeize(
uint256 debtToRepay,
uint256 bonusBps
) internal pure returns (uint256 collateralToSeize) {
// collateral = debtRepaid * (1 + bonus)
collateralToSeize = debtToRepay * (BPS_DENOMINATOR + bonusBps) / BPS_DENOMINATOR;
}
// ─── Variable bonus (Aave V4 / Dutch-auction style) ─────────────────────
/// @notice Computes a dynamically scaled bonus based on health factor.
/// @dev As HF → 0, bonus → MAX_BONUS_BPS.
/// As HF → 1, bonus → BASE_BONUS_BPS.
/// Linear interpolation between the two.
/// @param healthFactor Current HF, scaled to 1e18 (1e18 == 1.0)
/// @return bonusBps Effective bonus in basis points
function variableBonus(uint256 healthFactor)
internal
pure
returns (uint256 bonusBps)
{
// Clamp: HF should be in [0, 1e18) to be liquidatable
if (healthFactor >= PRECISION) return BASE_BONUS_BPS;
if (healthFactor == 0) return MAX_BONUS_BPS;
// Linear interpolation:
// bonus = MAX - (MAX - BASE) * (HF / PRECISION)
uint256 bonusRange = MAX_BONUS_BPS - BASE_BONUS_BPS;
uint256 scaled = (bonusRange * healthFactor) / PRECISION;
bonusBps = MAX_BONUS_BPS - scaled;
}
/// @notice Computes the maximum debt that can be repaid to restore
/// the position to targetHF, rather than liquidating a fixed %.
/// @param totalDebt Total debt in USD (18 dec)
/// @param weightedCollateral Threshold-weighted collateral in USD
/// @param targetHF Target health factor post-liquidation (1e18 == 1.0)
/// @param bonusBps Liquidation bonus in bps
/// @return maxRepay Maximum debt that may be repaid
function maxRepayForTargetHF(
uint256 totalDebt,
uint256 weightedCollateral,
uint256 targetHF,
uint256 bonusBps
) internal pure returns (uint256 maxRepay) {
// Derived from:
// targetHF = (weightedCollateral - repay*(1+bonus)) / (totalDebt - repay)
// Solving for repay:
// repay = (targetHF * totalDebt - weightedCollateral)
// / (targetHF - (1 + bonus))
//
// Note: denominator is negative when targetHF < 1+bonus (healthy case).
// This function should only be called when HF < 1.
uint256 bonusFactor = PRECISION + (PRECISION * bonusBps / BPS_DENOMINATOR);
uint256 numerator = targetHF * totalDebt / PRECISION - weightedCollateral;
uint256 denominator = targetHF - bonusFactor;
// Guard: if denominator would underflow, cap to 100% of debt
if (denominator == 0 || bonusFactor >= targetHF) {
return totalDebt;
}
maxRepay = (numerator * PRECISION) / denominator;
// Cap at total debt
if (maxRepay > totalDebt) maxRepay = totalDebt;
}
}
The variableBonus function captures the core insight: a position at HF = 0.5 represents far more protocol risk than one at HF = 0.99 and should receive a proportionally larger incentive to attract liquidators urgently.
A secondary design tradeoff: small at-risk positions can dodge liquidation even when their health factor is critical, because transaction gas costs often outweigh the incentives for liquidators, leaving these ‘dust’ positions untouched. Protocols must account for this floor — small bad positions accumulate over time if there is no economic incentive to clear them. Aave V4’s approach of restoring to a target health factor (rather than taking a fixed percentage) also helps here: the simplified handling of dust debt and collateral makes the system more reliable. In V3, small leftover debt positions can be difficult to manage and can accumulate over time. V4’s design allows liquidators to more easily fully clear positions, preventing these inefficiencies.
Bad Debt Accumulation
Bad debt is what accrues when a position crosses from “liquidatable” into “insolvent.” Insolvency occurs when collateral_value < borrow_value, meaning insufficient collateral remains, leading to bad debt even after liquidation.
If bad debt accumulates rapidly during a cascading event, the protocol may become insolvent, leaving lenders unable to withdraw their deposited assets.
Where does the bad debt go? Different protocols make different choices.
Bad debt that exceeds collateral coverage falls on Aave’s Safety Module, where AAVE stakers underwrite the protocol in exchange for staking yield. The Safety Module has been tapped twice historically (the 2020 BLY incident and the 2022 CRV bad debt).
The November 2022 CRV incident is the canonical case study. The lending platform Aave V2 incurred bad debt resulting from a major liquidation event involving a single user who had borrowed close to $40M of CRV tokens using USDC as collateral. The position became too large to liquidate cleanly in available on-chain liquidity; every liquidation attempt moved the market, reducing the collateral value faster than the debt was cleared.
Two structural conditions produce bad debt:
-
Speed failure: during high volatility, network congestion or slow price oracles can delay liquidations, increasing protocol exposure to bad debt. The position was liquidatable when last checked, but by the time the transaction executes, it has slid into insolvency.
-
Liquidity failure: The collateral asset has insufficient on-chain liquidity to absorb the liquidation at anything near the oracle price. The liquidator can only execute at a deep discount, which doesn’t cover the debt. This is particularly dangerous for assets where liquidation mechanisms or oracles fail to respond promptly to market movements.
Liquidation Cascade Mechanics
A liquidation cascade is the domino effect that happens when multiple liquidations occur simultaneously.
The mechanism is reflexive: heavy liquidation activity can push prices lower, triggering further liquidations. Each executed liquidation increases sell pressure on the collateral asset. That sell pressure reduces the collateral’s price. Lower prices push more positions below their liquidation threshold. Those positions get liquidated, creating more sell pressure. Repeat.
Volatility is a defining feature of cryptocurrency markets and poses significant challenges to the security of DeFi protocols. Sharp price swings can lead to rapid changes in the value of collateral, increasing the likelihood of liquidations and, in extreme cases, causing cascading liquidations.
The cascade has five discrete phases:
Phase 1 — Trigger: A significant price drop pushes a cluster of positions below HF = 1. The cluster size is a function of how many users borrowed near the maximum LTV and how correlated their collateral assets are.
Phase 2 — Liquidator Race: Liquidators are automated bots that constantly monitor health factors. When a position becomes undercollateralized, these bots race to trigger the liquidation function. On-chain, this is a priority gas auction or MEV competition. The collateral is sold on-chain, typically into a DEX.
Phase 3 — Market Impact: Each sale moves the on-chain price. Large-scale liquidations increase sell pressure, pushing asset prices down, and amplifying financial shocks. The impact is proportional to liquidation volume relative to available DEX liquidity.
Phase 4 — Threshold Breach of Neighboring Positions: The price drop from Phase 3 pushes previously-healthy positions across the liquidation threshold. This is the cascade mechanism — positions that were at HF = 1.05 when the cascade began are now at HF = 0.97.
Phase 5 — Bad Debt Formation: Positions that enter the cascade late — because they are large, because gas prices made them unprofitable to clear, or because oracle updates lagged the actual price — can slip from “liquidatable” to “insolvent” without ever being touched. Price volatility increases, and the risk of cascading liquidations grows, particularly in DeFi protocols reliant on sufficient market depth to liquidate positions effectively.
Real examples of cascade scale: during the March 2023 market crash, Compound saw 5,678 liquidations totaling $234 million in a single day. During fast collateral price drops (e.g., March 2020, May 2022, August 2024), liquidations cascade and pile bad debt onto the protocol.
The Role of Price Oracles in Cascade Timing
Oracles are where the abstract risk becomes concrete. Oracle manipulation and stale pricing data present another vulnerability. Lending protocols depend on oracle networks to fetch offchain asset prices and deliver them onchain.
Oracle design choices affect cascade dynamics in several specific ways:
Update frequency and latency: Time delays between oracle updates and critical operations provide additional security buffers. Rather than immediately acting on fresh oracle data, platforms can wait for confirmation across multiple update cycles. This delay makes manipulation attacks more expensive to execute while providing minimal impact on user experience for most operations. But this same delay means oracle prices can lag actual market prices during a fast crash, creating the gap where positions slip into insolvency before the protocol registers it.
TWAP vs. spot: Time-weighted average prices resist manipulation but introduce deliberate lag. Some protocols use TWAPs to resist flash-loan–driven price swings. Under normal conditions this is protective. Under a genuine crash it means the oracle-reported price is always above the actual traded price, which delays liquidations by design and produces bad debt when the crash is fast enough.
Single-source vulnerability: A lending market on L2 with Uniswap v3 TWAP as a sanity-check and a single push oracle as primary can fail when a thin pool gets nudged during two consecutive blocks, your TWAP lags just enough on a volatile day, and liquidations cascade.
Oracle manipulation as active attack vector: Price manipulation becomes more feasible during volatile periods, as attackers exploit thin liquidity or delayed price updates to trigger undeserved liquidations or arbitrage opportunities. The attack pattern is well-documented: push prices on a thin venue, let oracles propagate the distorted value, and harvest liquidations at scale.
The November 2024 USDe incident illustrates the tail risk of single-point oracle reliance. Ethena’s synthetic USD (USDe) experienced a sharp pricing anomaly on Binance: an internal pricing feed briefly showed USDe at $0.65. That single mispriced tick propagated into liquidation engines and cross-venue trading logic, contributing to what some analyses describe as one of the largest liquidation cascades in crypto history.
Best practice oracle architecture for lending protocols:
- Aggregated feeds: Use at least two independent oracle sources (e.g., Chainlink + Pyth) and require consensus within a defined deviation band before acting.
- Staleness enforcement: Revert or fall back if the oracle has not been updated within
MAX_STALENESSseconds. - Pyth confidence intervals: Use the confidence value as a cap on liquidation execution — do not liquidate when uncertainty is high.
- L2 sequencer uptime check: if the sequencer wobbles, fail-safes may not trigger because you never wired the L2 uptime feed. Always check the
sequencerUptimeFeedbefore consuming prices on L2. - Lending liquidations particularly benefit from delayed execution based on sustained price movements rather than momentary spikes.
Protocol-Level Circuit Breakers
Circuit breakers act as the immune system of DeFi protocols. They detect threats and activate defenses automatically, giving the protocol time to stabilize and users time to make informed decisions.
Circuit breakers can be triggered by price changes, liquidity withdrawals, oracle deviations, or unusual trading volumes. Common types include trading halt circuits, withdrawal limits, price deviation protections, and transaction size restrictions.
The core circuit breaker patterns for liquidation systems are:
Price deviation halt: Pause liquidations (or new borrows) when the oracle-reported price deviates from a TWAP or secondary oracle by more than a configured threshold. Automated circuit breakers implement pre-defined conditions (e.g., price deviation exceeding X%, liquidity dropping below Y%) that automatically trigger a pause or limit functionality to prevent further exploits.
Per-block liquidation cap: Limit the total value of collateral that can be liquidated in a single block or epoch. This throttles cascade propagation by preventing any single block from generating enough sell pressure to push the next wave of positions into liquidation.
Governance pause: governance-controlled pauses allow the chain’s governance (token holders or a multisig) to pause specific protocol functions (e.g., borrowing, liquidations) if an oracle attack is suspected or confirmed.
Withdrawal limits: Cap the speed at which liquidity providers can exit during stress events to prevent simultaneous bank-run dynamics.
The critical design tension: circuit breakers pause, allowing borrowers time to deposit additional collateral and giving market liquidity a chance to stabilize. However, circuit breakers must be designed carefully to ensure they do not inadvertently trap users or prevent necessary debt clearing during prolonged market downturns.
A circuit breaker that fires during a genuine 40% market drop and prevents liquidations for 24 hours doesn’t stop bad debt — it defers and amplifies it. The right design triggers on manipulation-shaped signals (sudden, large, brief deviations) not on prolonged genuine market moves.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title LiquidationCircuitBreaker
/// @notice Implements a per-block liquidation volume cap and price
/// deviation check before allowing liquidations to proceed.
contract LiquidationCircuitBreaker {
uint256 public constant MAX_PRICE_DEVIATION_BPS = 1000; // 10%
uint256 public constant BPS_DENOMINATOR = 10_000;
/// @notice Max USD value liquidatable in a single block (18 dec)
uint256 public maxLiquidationPerBlock;
/// @notice Tracks liquidation volume in the current block
uint256 public liquidationVolumeThisBlock;
uint256 public lastBlockSeen;
address public governance;
bool public paused;
IPriceOracle public primaryOracle;
IPriceOracle public secondaryOracle; // TWAP or alternative feed
error CircuitBreakerTripped(string reason);
error NotGovernance();
modifier onlyGovernance() {
if (msg.sender != governance) revert NotGovernance();
_;
}
constructor(
address _primaryOracle,
address _secondaryOracle,
uint256 _maxLiquidationPerBlock,
address _governance
) {
primaryOracle = IPriceOracle(_primaryOracle);
secondaryOracle = IPriceOracle(_secondaryOracle);
maxLiquidationPerBlock = _maxLiquidationPerBlock;
governance = _governance;
}
/// @notice Called before every liquidation. Reverts if any circuit trips.
function checkAndAccumulate(
address collateralAsset,
uint256 liquidationValueUSD
) external {
if (paused) revert CircuitBreakerTripped("protocol paused");
_checkPriceDeviation(collateralAsset);
_checkBlockVolumeCap(liquidationValueUSD);
}
/// @notice Emergency pause by governance
function pause() external onlyGovernance { paused = true; }
function unpause() external onlyGovernance { paused = false; }
/// @notice Governance can adjust the per-block cap
function setMaxLiquidationPerBlock(uint256 newMax) external onlyGovernance {
maxLiquidationPerBlock = newMax;
}
// ─── Internal ────────────────────────────────────────────────────────────
function _checkPriceDeviation(address asset) internal view {
(uint256 primaryPrice,) = primaryOracle.getPrice(asset);
(uint256 secondaryPrice,) = secondaryOracle.getPrice(asset);
if (primaryPrice == 0 || secondaryPrice == 0)
revert CircuitBreakerTripped("zero price");
uint256 deviation;
if (primaryPrice >= secondaryPrice) {
deviation = ((primaryPrice - secondaryPrice) * BPS_DENOMINATOR) / secondaryPrice;
} else {
deviation = ((secondaryPrice - primaryPrice) * BPS_DENOMINATOR) / primaryPrice;
}
if (deviation > MAX_PRICE_DEVIATION_BPS)
revert CircuitBreakerTripped("price deviation exceeds threshold");
}
function _checkBlockVolumeCap(uint256 liquidationValueUSD) internal {
if (block.number != lastBlockSeen) {
// New block: reset volume counter
liquidationVolumeThisBlock = 0;
lastBlockSeen = block.number;
}
liquidationVolumeThisBlock += liquidationValueUSD;
if (liquidationVolumeThisBlock > maxLiquidationPerBlock)
revert CircuitBreakerTripped("per-block liquidation cap reached");
}
}
Note that the per-block counter in _checkBlockVolumeCap uses storage writes on every call, which is expensive. A production implementation would use an event-based accumulator with off-chain monitoring for less critical limits, reserving on-chain enforcement for hard caps only.
Isolated vs. Cross-Margin Systems
Architecture determines how risk propagates through a protocol. The central question is: when one position goes bad, can it contaminate others?
Cross-Margin
Cross margin uses the entire account balance to back all open positions. The platform continuously reallocates equity across the book, so winning trades support losing ones. That design improves capital efficiency and keeps more strategies live during normal volatility.
The cascade failure mode is predictable: during a broad market selloff, if multiple positions move against the trader simultaneously, the shared collateral can be consumed faster than any individual position’s losses would suggest. A trader cannot limit the damage of one position without closing it or switching it to isolated mode. If equity falls below combined maintenance margin falls below combined maintenance margin, the entire account faces liquidation simultaneously.
Cascade amplification in cross-margin: a falling asset price reduces collateral value across multiple positions at once. Every position using that asset as collateral becomes eligible for liquidation at the same time. This floods the liquidation queue with simultaneous orders, competes for liquidator capacity, and suppresses recovery prices.
Isolated Margin
Isolated margin caps the loss of any single position at its allocated margin. A liquidation in one position does not affect others. The trade-off is reduced capital efficiency — the same account needs more total collateral to run the same set of positions.
Cascade resistance in isolated margin: a price drop triggers liquidations only in positions that used the affected asset as collateral for that specific trade. Other positions are unaffected by default. The protocol’s overall solvency is less vulnerable to correlated asset moves.
Protocol Design Choices
For protocols that support both modes, the choice of default matters enormously. A cross-margin default with opt-in isolation exposes unsophisticated users to cascade risk they did not explicitly accept. An isolated default with opt-in cross-margin places the burden of understanding the risk on the user who wants more capital efficiency.
From an audit perspective: cross-margin systems require explicit cascade analysis. The key questions are whether the liquidation mechanism can process the queue faster than prices move, whether the insurance fund is sized for correlated liquidations rather than independent ones, and whether social loss (loss mutualization) has a defined trigger and cap.
Liquidation System Audit Checklist
Health factor and collateral valuation
- Health factor formula is correctly implemented and matches the specification
- Collateral valuation uses oracle prices that cannot be manipulated in a single transaction
- Liquidation threshold and collateral factor are distinct values with the correct relationship
- Health factor is checked atomically with every operation that changes collateral or debt
Liquidation incentive design
- Liquidation bonus is large enough to cover gas costs on the target network at peak congestion
- Liquidation bonus is small enough that it does not become a significant drain on the insurance fund
- Dutch auction or similar mechanism is used to prevent liquidation bonus extraction by MEV searchers
- Partial liquidation is supported to allow undercollateralized positions to be brought back to health incrementally
Bad debt handling
- The protocol has an explicit definition of bad debt (collateral value < debt value)
- Bad debt accrual mechanism is defined — who absorbs it and when
- Insurance fund size is sufficient to cover bad debt from plausible correlated liquidation events
- Social loss (mutualization) has a defined trigger, scope, and cap
Cascade risk
- Isolated margin is the default, or cascade risk between positions is explicitly analyzed
- The liquidation queue capacity is analyzed under conditions where multiple large positions become eligible simultaneously
- Circuit breakers pause new borrowing (but not repayments or liquidations) during extreme price volatility
- The protocol has been stress-tested against historical volatility events (e.g., -50% in 24 hours)
Oracle dependency
- Liquidation prices are not read from spot AMM reserves
- Oracle staleness during network congestion does not create a window where undercollateralized positions cannot be liquidated
- The sequencer uptime feed is checked before liquidations on L2 deployments
Liquidator economics
- Liquidation is profitable at the minimum viable position size on the target network
- The protocol does not depend on a single liquidator — the mechanism works with any participant
- Liquidation is not blocked by reentrancy or callback attacks from malicious collateral tokens