On-chain perpetual futures have become the dominant derivative primitive in DeFi. By 2025, on-chain perpetual futures DEXs captured 26% of the crypto-derivatives market, processing $1 trillion in monthly volume via platforms like Hyperliquid and Aster. That scale of capital concentration means every security flaw carries systemic consequence. Decentralized perpetual contract protocols replicate high-leverage derivatives trading on-chain by leveraging shared liquidity pools and oracle-based pricing. Unlike AMM spot trading, perpetual systems involve complex margin accounting, dynamic PnL calculations, and liquidation mechanisms — and minor logic deviations, whether rounding errors or oracle latency, may lead to insolvency at the protocol level or user portfolio wipeout.

This article is a technical deep-dive into the attack surfaces specific to perpetuals and on-chain derivatives protocols. It is intended for smart contract auditors, protocol engineers, and security researchers working at this layer of the stack.


1. The Architecture of On-Chain Perpetuals

Before cataloguing vulnerabilities, it is worth establishing what we are reasoning about. An on-chain perpetual is a synthetic exposure engine. It needs to:

  1. Maintain a mark price (used for PnL and liquidations) tied to an external index price (spot oracle).
  2. Transfer periodic funding payments between longs and shorts to keep mark price near index price.
  3. Enforce margin requirements, liquidating positions that breach a maintenance threshold.
  4. Absorb bad debt via an insurance fund, falling back to socialized losses or auto-deleveraging (ADL) if the fund is exhausted.

Smart contracts, oracles, and funding rates automate pricing, settlement, and risk management transparently on-chain. Every one of those automation points is an attack surface.


2. Funding Rate Manipulation

2.1 How Funding Rates Work

The funding rate is the primary mechanism that tethers the price of the perpetual contract to the spot price of the underlying asset. Funding is typically calculated every 8 hours (or continuously in some vAMM designs) as a function of the premium between mark price and index price:

fundingRate = clamp((markPrice - indexPrice) / indexPrice, -maxRate, +maxRate)

Longs pay shorts when the perpetual trades at a premium; shorts pay longs when it trades at a discount. The incentive arbitrages the mark price back toward index.

2.2 The Attack Vector

Funding rate manipulation is a form of economic griefing — an attacker can move the mark price to generate a favorable funding flow and simultaneously hold a large opposing position that profits from it. The attack is amplified when:

  • The mark price derivation relies on the protocol’s own vAMM price (endogenous) rather than an external index.
  • Position size limits are insufficiently enforced.
  • Funding settlement is infrequent, allowing accumulated imbalances to grow.

A classic pattern: attacker opens a massive long on a low-liquidity perpetual, pushing the mark price above index, then collects funding from shorts for several epochs before unwinding. Traders face unexpected costs from complex funding rate mechanics, counterparty risks associated with exchange platforms, and potential pricing manipulation.

2.3 Vulnerable Solidity Pattern

// VULNERABLE: Mark price derived entirely from vAMM reserves
function getMarkPrice() public view returns (uint256) {
    // Spot reserves of vAMM — directly manipulable by large positions
    return (quoteReserve * PRECISION) / baseReserve;
}

function settleFunding() external {
    uint256 mark = getMarkPrice();
    uint256 index = oracle.getPrice(baseAsset);
    int256 premium = int256(mark) - int256(index);
    int256 fundingRate = premium / int256(index) / 24; // per-hour rate
    // Distribute fundingRate * openInterest across all longs/shorts
    _applyFunding(fundingRate);
}

In this design, an attacker who controls 40% of the vAMM’s open interest can persistently skew getMarkPrice(), earning funding at the expense of every counterparty.

2.4 Mitigations

  • Use a time-weighted average of the premium (TWAP) rather than the instantaneous mark price when calculating funding.
  • Cap the single-epoch funding rate and add a circuit breaker that pauses settlement when the premium exceeds a threshold for more than N consecutive blocks.
  • Source the index price from a robust, manipulation-resistant oracle rather than the protocol’s own liquidity pool.
// SAFER: TWAP-based funding rate
function settleFunding() external {
    uint256 twapMark = twapOracle.getTWAP(address(this), 3600); // 1-hour TWAP
    uint256 index = chainlinkOracle.getPrice(baseAsset);
    int256 premium = int256(twapMark) - int256(index);
    // Clamp the funding rate to protocol-defined bounds
    int256 rawRate = premium * 1e18 / int256(index) / 24;
    int256 fundingRate = _clamp(rawRate, MIN_FUNDING_RATE, MAX_FUNDING_RATE);
    _applyFunding(fundingRate);
}

3. Oracle-Based Liquidation Manipulation

3.1 The Oracle Dependency

Derivatives platforms use the mark price to calculate unrealized PnL and trigger liquidations. This prevents market manipulation — a malicious actor cannot simply crash the price on a single exchange to liquidate other traders, as the mark price remains tethered to the broader global market via secure data inputs. But that invariant only holds if the oracle itself is secure.

Price manipulation via oracles stands as one of the most expensive and notorious attack vectors in Web3. Protocols lost more than $50 million to oracle-manipulation exploits in 2024, even as teams supposedly “learned the lessons” of early DeFi.

3.2 The Mango Markets Template

The Mango Markets attacker massively pumped the illiquid MNGO token on spot markets to drain the perpetuals platform of $115M. The protocol used an on-chain TWAP of a thin market as its index price — a feed that could be sustained at an inflated level for long enough to pass the TWAP window.

TWAPs aim to mitigate brief price manipulation, but unfortunately introduce a new weakness: if a manipulator can maintain a skewed price throughout the entire TWAP calculation period, the resulting average will be as unreliable as the manipulation itself.

3.3 The KiloEx Exploit (April 2025)

In April 2025, KiloEx lost $7.4 million in a single oracle manipulation attack. The hacker exploited a price oracle access control vulnerability, opening an ETH/USD position at $100 and immediately closing it at an inflated $10,000. KiloEx used a custom price feed that allowed certain parameters to be set in ways that detached internal pricing from real market conditions. This is the most dangerous class: not manipulation of the underlying data, but direct write-access to the oracle state.

3.4 Vulnerable Solidity Pattern

// VULNERABLE: No access control on price setter, no staleness check
contract VulnerableOracle {
    mapping(address => uint256) public prices;
    mapping(address => uint256) public lastUpdated;

    // Missing: onlyOwner / onlyKeeper modifier
    function setPrice(address asset, uint256 price) external {
        prices[asset] = price;
        lastUpdated[asset] = block.timestamp;
    }

    function getPrice(address asset) external view returns (uint256) {
        // No staleness check — stale price accepted silently
        return prices[asset];
    }
}

An attacker calls setPrice directly, sets an absurd value, then opens and closes a position in the same transaction for risk-free profit.

3.5 Hardened Oracle Pattern

// SAFER: Role-gated, staleness-checked, deviation-bounded oracle
contract HardenedOracle is AccessControl {
    bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
    uint256 public constant MAX_STALENESS = 60 seconds;
    uint256 public constant MAX_DEVIATION_BPS = 500; // 5%

    struct PriceFeed {
        uint256 price;
        uint256 timestamp;
    }

    mapping(address => PriceFeed) private feeds;

    function updatePrice(address asset, uint256 newPrice)
        external
        onlyRole(KEEPER_ROLE)
    {
        PriceFeed storage feed = feeds[asset];
        if (feed.price != 0) {
            uint256 deviation = newPrice > feed.price
                ? ((newPrice - feed.price) * 10000) / feed.price
                : ((feed.price - newPrice) * 10000) / feed.price;
            require(deviation <= MAX_DEVIATION_BPS, "Price deviation too large");
        }
        feed.price = newPrice;
        feed.timestamp = block.timestamp;
    }

    function getPrice(address asset) external view returns (uint256) {
        PriceFeed storage feed = feeds[asset];
        require(
            block.timestamp - feed.timestamp <= MAX_STALENESS,
            "Stale price"
        );
        return feed.price;
    }
}

Every oracle query should include a timestamp check, rejecting price data older than the protocol’s maximum acceptable freshness window, and protocols should implement circuit breakers that halt operations when reported prices deviate more than a defined percentage from historical averages.


4. Mark Price vs. Index Price: The Security Relationship

The mark price exists to preserve an important invariant: liquidation should reflect economically meaningful market movement, not a transient or manipulated print. Research identifies funding rates and mark price as the two central mechanisms in crypto perpetuals, with mark price used to determine margin requirements and reduce unnecessary liquidations.

The canonical mark price formula is:

markPrice = indexPrice + EMA(markPrice - indexPrice)

where the EMA smooths out transient spikes. The security consequence:

  • Too tight coupling (mark ≈ spot at all times): Mark price inherits all spot volatility and can be manipulated via spot exchanges.
  • Too loose coupling (mark diverges from spot): Funding rates may not converge the spread, and liquidations may fire on economically stale prices.

This is where manipulation risk becomes real. If the reference price is weak, or if the oracle/index construction is fragile, a perp can become vulnerable not just to bad pricing but to direct exploitation.

4.1 Solidity: Mark Price Calculation

contract MarkPriceModule {
    uint256 public constant EMA_ALPHA_BPS = 200; // 2% weight per update
    uint256 public markPrice;
    IOracle public indexOracle;

    function updateMarkPrice() external {
        uint256 index = indexOracle.getPrice(baseAsset);
        uint256 currentMark = markPrice == 0 ? index : markPrice;
        // EMA: mark = alpha * index + (1 - alpha) * mark
        markPrice = (EMA_ALPHA_BPS * index + (10000 - EMA_ALPHA_BPS) * currentMark) / 10000;
    }

    function isLiquidatable(address trader) external view returns (bool) {
        int256 unrealizedPnl = _computePnL(trader, markPrice); // use mark, not index
        int256 equity = int256(collateral[trader]) + unrealizedPnl;
        uint256 maintenanceMargin = _maintenanceMargin(trader);
        return equity < int256(maintenanceMargin);
    }
}

5. Position Size Limit Bypasses via Multiple Accounts

Open interest (OI) caps are the primary defense against any single actor dominating a market and manipulating prices or funding rates. But on permissionless blockchains, they are trivially circumvented.

5.1 The Multi-Account Pattern

An attacker creates N wallets, each holding OI slightly below the per-account cap. In aggregate they control dominant OI in a thin market, enough to:

  • Move the mark price to trigger liquidations of honest users.
  • Hold one-sided positions while manipulating the funding rate.
  • Create wash volume that inflates the protocol’s reported metrics.

Three Hyperliquid accounts attacked the perpetuals DEX using a “self-liquidation” method. The first trader took a $4.1M short position on JELLY while two other accounts took long positions of $2.15M and $1.9M respectively to counter the short. This coordinated “self-liquidation” strategy is a form of sophisticated market manipulation — by opening large opposing positions, the attackers effectively controlled both sides of the trade, setting the stage to exploit the platform’s liquidation engine rather than speculating on the asset’s price.

5.2 Mitigation Patterns

On-chain OI limits without identity-binding cannot prevent Sybil attacks. Effective mitigations include:

  • Per-address OI caps (necessary but insufficient).
  • Global OI caps that slow the rate of new position opening when OI exceeds a threshold.
  • Graduated initial margin — requiring higher margin for larger OI concentration, making attacks economically expensive.
  • Off-chain monitoring for on-chain Sybil clusters with coordinated on-chain enforcement via proof-of-identity or proof-of-stake stake requirements.
// Graduated margin model — higher OI concentration = higher required margin
function getInitialMarginRate(uint256 positionSize) public view returns (uint256) {
    uint256 totalOI = longOpenInterest + shortOpenInterest;
    uint256 concentrationBps = (positionSize * 10000) / totalOI;
    
    if (concentrationBps < 100)  return BASE_MARGIN_RATE;           // < 1%  OI
    if (concentrationBps < 300)  return BASE_MARGIN_RATE * 2;       // 1-3%  OI
    if (concentrationBps < 1000) return BASE_MARGIN_RATE * 5;       // 3-10% OI
    return BASE_MARGIN_RATE * 20;                                    // > 10% OI
}

6. The PnL Accounting Invariant

6.1 Defining the Invariant

The core accounting invariant of any solvent perpetuals protocol is:

sum(collateral_i) + insuranceFund >= sum(unrealizedPnL_winners) - sum(unrealizedPnL_losers)

Equivalently: total assets must always cover total liabilities. Because unrealized PnL is zero-sum across longs and shorts (one side’s gain is the other side’s loss), the invariant simplifies to:

totalCollateral + insuranceFund >= 0  (net of bad debt)

Any code path that allows unrealized PnL to be realized without corresponding collateral transfer breaks this invariant.

6.2 Common Invariant Violations

Rounding errors accumulating to protocol-level bad debt. When computing funding payments at scale, systematic floor-rounding in favor of payers and ceiling-rounding against recipients creates a slow drain.

Position closing at stale prices. If a position is closed using a cached price that differs from the execution price, the PnL delta leaks from or into the protocol’s reserve.

Liquidation shortfall not tracked atomically. When a liquidation produces a loss exceeding the liquidated user’s collateral (underwater position), that bad debt must immediately reduce the insurance fund or trigger socialized loss. If it is deferred, the invariant is transiently violated.

// CORRECT: Atomic bad-debt accounting on liquidation
function liquidate(address trader) external {
    require(isLiquidatable(trader), "Not liquidatable");

    int256 remainingCollateral = _closePosition(trader);
    
    if (remainingCollateral < 0) {
        // Position was underwater — bad debt realized
        uint256 badDebt = uint256(-remainingCollateral);
        if (insuranceFund >= badDebt) {
            insuranceFund -= badDebt;
        } else {
            // Insurance fund exhausted — trigger socialized loss
            uint256 covered = insuranceFund;
            insuranceFund = 0;
            _socializeLoss(badDebt - covered);
        }
    }
    
    emit Liquidation(trader, remainingCollateral);
}

7. Socialized Loss Mechanisms and Their Abuse

7.1 What Socialized Loss Is

Autodeleveraging (ADL) is a last-resort loss socialization mechanism used by perpetual futures when liquidation and insurance buffers are insufficient to restore solvency. When the insurance fund is depleted, any uncovered loss is socialized among the winning traders at the end of the trading session. Under the socialized loss mechanism, all winners share the loss on a pro-rata basis proportional to the size of their profit.

The fund accumulates capital through user staking and from a portion of trading, borrowing, and liquidation fees. If the external fund is insufficient, the vAMM’s lifetime profit for that market covers remaining losses. If both are exhausted, socialized loss applies pro rata across all open positions within that market.

7.2 The Griefing Attack

Socialized loss is abusable by an attacker who:

  1. Opens a large profitable position (long, in a rising market).
  2. Simultaneously opens a smaller highly-leveraged position on the opposing side via a second account.
  3. Allows the leveraged position to go deeply underwater — generating bad debt.
  4. The bad debt consumes the insurance fund and triggers socialized loss.
  5. The socialized loss haircuts the profits of honest winners, while the attacker’s profitable position partially offsets their own haircut.

The net effect: honest traders’ PnL is redistributed to cover the attacker’s manufactured bad debt.

This demonstrates a natural trade-off that perpetuals exchanges must make: they can either aggressively socialize losses to their winners and potentially lose future revenue of these users while preserving solvency, or hold the losses due to insolvency on their balance sheet.

ADL mechanisms face a fundamental trilemma: no policy can simultaneously satisfy exchange solvency, revenue, and fairness to traders. This impossibility theorem implies that as participation scales, a novel form of moral hazard grows asymptotically, rendering “zero-loss” socialization impossible.

7.3 Mitigation

  • Per-market ADL caps: socialized loss in any one market cannot exceed X% of global insurance fund.
  • Graduated leverage limits that scale down maximum leverage as OI approaches the market’s liquidity depth.
  • Timed ADL: force partial position closure on the most profitable traders proportional to their leverage ratio before hitting full socialization.
// ADL: Select profitable positions for forced partial closure
// Ordered by: highest unrealized PnL / collateral ratio first
function triggerADL(uint256 badDebt) internal {
    address[] memory ranked = _rankByPnLRatio(); // off-chain sorted, on-chain verified
    uint256 remaining = badDebt;

    for (uint256 i = 0; i < ranked.length && remaining > 0; i++) {
        address trader = ranked[i];
        uint256 pnl = uint256(_getUnrealizedPnL(trader));
        uint256 haircut = pnl < remaining ? pnl : remaining;
        
        _applyHaircut(trader, haircut);
        remaining -= haircut;
        
        emit ADLApplied(trader, haircut);
    }
    
    require(remaining == 0, "ADL insufficient to cover bad debt");
}

8. Insurance Fund Solvency

Insurance fund dependence is a key risk: the fund may be insufficient during extreme events. The insurance fund is the first backstop to maintaining the solvency of the exchange in the event of any bankruptcies.

8.1 Solvency Stress Scenarios

An insurance fund that is 2% of total OI sounds robust until you consider:

  • Cascade liquidations: Flash crashes can trigger chain liquidations across connected positions. When liquidations exceeded $5 billion and overwhelmed insurance funds, exchanges forcibly closed the most profitable positions to maintain solvency.
  • Thin market exploitation: An attacker deliberately creates OI in a market with insufficient depth, knowing that any significant price move will generate bad debt in excess of the insurance fund.
  • Flash-loan-amplified liquidation cascade: A large flash loan can cause a brief price spike sufficient to liquidate many positions simultaneously, overwhelming the fund in a single block.

8.2 Insurance Fund Accounting in Solidity

contract InsuranceFund {
    uint256 public balance;
    uint256 public constant MAX_COVERAGE_PER_LIQUIDATION_BPS = 1000; // 10% of fund per event
    address public immutable clearingHouse;

    modifier onlyClearingHouse() {
        require(msg.sender == clearingHouse, "Only clearing house");
        _;
    }

    // Clearing house draws on fund to cover bad debt
    function coverBadDebt(uint256 amount) external onlyClearingHouse returns (uint256 covered) {
        // Cap per-event draw to prevent single-event fund drain
        uint256 maxCoverage = (balance * MAX_COVERAGE_PER_LIQUIDATION_BPS) / 10000;
        covered = amount > maxCoverage ? maxCoverage : amount;
        balance -= covered;
        emit BadDebtCovered(amount, covered, balance);
    }

    // Accumulate fees from trading activity
    function deposit(uint256 amount) external onlyClearingHouse {
        balance += amount;
    }

    // View: solvency ratio vs current OI
    function solvencyRatioBps(uint256 totalOI) external view returns (uint256) {
        if (totalOI == 0) return type(uint256).max;
        return (balance * 10000) / totalOI;
    }
}

The MAX_COVERAGE_PER_LIQUIDATION_BPS cap is a critical defense: without it, a single coordinated attack can drain the entire fund in one block, immediately triggering socialized loss.


9. Cross-Margin vs. Isolated Margin Security Implications

9.1 Structural Difference

In isolated margin, each position carries its own dedicated collateral. A position going to zero loses only its allocated margin — no contagion to other positions.

In cross-margin, all positions share a single collateral pool. A large loss in one market can cascade to force liquidation of positions in all other markets simultaneously.

9.2 Cross-Margin Attack Vectors

Contagion via correlated positions: An attacker who can force a loss in one cross-margined position that exceeds the shared collateral triggers liquidation of the entire portfolio — potentially including positions in other markets that honest traders intended as hedges. This exposed a critical risk of delta-neutral strategies that most traders had not considered: their short positions could be forcibly closed while their unprofitable longs remained open.

Cross-margin dust accumulation: Rounding errors in PnL settlement accumulate across all positions in a cross-margin account. Because all positions share collateral, the effective margin ratio calculation is more complex and easier to miscompute.

Leverage amplification: In cross-margin, the effective leverage of the entire account is the sum of notional across all positions divided by shared collateral. A protocol that enforces per-position leverage limits but not portfolio-level leverage limits is vulnerable to an attacker opening many small positions to achieve extremely high aggregate leverage.

9.3 Solidity: Margin Mode Isolation

enum MarginMode { ISOLATED, CROSS }

struct Position {
    int256 size;
    uint256 entryPrice;
    uint256 isolatedMargin;  // only used in ISOLATED mode
    MarginMode marginMode;
}

// Cross-margin equity calculation — must aggregate all positions
function getCrossMarginEquity(address trader) public view returns (int256) {
    int256 equity = int256(crossMarginCollateral[trader]);
    address[] memory markets = _getOpenMarkets(trader);
    for (uint256 i = 0; i < markets.length; i++) {
        equity += _getUnrealizedPnL(trader, markets[i]);
        equity -= int256(_getAccruedFunding(trader, markets[i]));
    }
    return equity;
}

// Critical: cross-margin liquidation must check PORTFOLIO equity, not per-position
function isCrossMarginLiquidatable(address trader) external view returns (bool) {
    int256 equity = getCrossMarginEquity(trader);
    uint256 totalMaintenanceMargin = _getTotalMaintenanceMargin(trader);
    return equity < int256(totalMaintenanceMargin);
}

Security note: Any protocol that calls _getUnrealizedPnL or _getAccruedFunding using a price oracle must ensure the oracle cannot be manipulated within the same transaction as a cross-margin liquidation call. Reentrancy between oracle updates and margin checks is a particularly dangerous surface in cross-margin systems.

9.4 Isolated Margin Security

Isolated margin is not unconditionally safer from a protocol perspective. If the liquidation engine cannot fully liquidate an isolated position at the mark price (illiquid market, price gap), the protocol must absorb the shortfall from the insurance fund. The sum of isolated position shortfalls can still overwhelm the fund.


10. Flash Loan Interactions with Perpetuals

Flash loans are a core vector for any attack that requires large but temporary capital. In perpetuals:

Flash loans allow users to borrow large amounts of capital without collateral, provided the loan is repaid within the same transaction. Attackers exploit this feature by executing large trades to temporarily inflate or deflate the price of assets in on-chain liquidity pools. This manipulated price can then be leveraged in other DeFi protocols that depend on the oracle, leading to cascading financial consequences.

Specific to perpetuals, flash loans enable:

  • Temporary spot price manipulation to trigger liquidations in the same transaction.
  • Deposit a flash-loaned amount as collateral, open a position, realize manipulated PnL, withdraw collateral, repay loan — all atomically.

The primary defense: mark price oracle must be update-resistant within a single transaction. If getMarkPrice() can return a value that reflects a state change made earlier in the same transaction, the protocol is vulnerable.

// Defense: Track last oracle update block; reject same-block usage in sensitive ops
mapping(address => uint256) public lastOracleUpdateBlock;

function updateIndexPrice(address asset, uint256 price) external onlyKeeper {
    indexPrices[asset] = price;
    lastOracleUpdateBlock[asset] = block.number;
}

function openPosition(address asset, ...) external {
    // Reject if oracle was updated in the same block (potential flash loan attack)
    require(
        block.number > lastOracleUpdateBlock[asset],
        "Oracle updated this block"
    );
    // ... rest of position opening logic
}

11. MEV and Front-Running in Perpetuals

For perpetual futures, sub-second data delivery is necessary to prevent “frontrunning” and ensure accurate execution during periods of high volatility. MEV in perpetuals has two primary expressions:

Liquidation front-running: Multiple bots race to submit the liquidation transaction first. This is largely benign (competition improves liquidation speed) but can create gas wars that fill blocks and delay honest transactions.

Oracle front-running: A keeper or validator who observes a pending oracle update transaction can insert trades that benefit from the price change before it is reflected on-chain. This is particularly dangerous when oracle updates are predictable (e.g., every N blocks).

Mitigation: Use pull-based oracle designs where price data is submitted atomically with the trade (Pyth / Stork model), preventing separation of oracle observation and trade execution.


12. Perpetuals Audit Checklist

The following checklist is organized by attack surface. Every item should be verified before deployment.

Oracle Security

  • Oracle has strict access control on setPrice / updatePrice — no public or unguarded write paths
  • Staleness check: every getPrice call validates block.timestamp - lastUpdated <= MAX_STALENESS
  • Deviation check: new price cannot deviate from last accepted price by more than MAX_DEVIATION_BPS in a single update
  • Oracle uses aggregated, multi-source feeds (e.g., Chainlink Data Streams, Pyth, Stork) rather than a single DEX pool
  • Protocol does not use spot AMM reserves as the index oracle (susceptible to flash loan manipulation)
  • Circuit breaker pauses trading when oracle price deviates >X% from TWAP within a configurable window
  • Oracle fallback behavior is defined — what happens if the primary feed is stale or reverts?

Funding Rate

  • Funding rate computed on TWAP of premium, not instantaneous mark/index spread
  • Funding rate is clamped to [MIN_FUNDING_RATE, MAX_FUNDING_RATE] to prevent economic griefing
  • Funding settlement is atomic with oracle update — no stale-rate settlement paths
  • Cumulative funding per position is tracked with sufficient precision to prevent rounding-based drain
  • Flash-loan manipulation of the mark price cannot affect a single funding epoch (same-block update guard)

Mark Price & Liquidations

  • Mark price uses EMA/TWAP smoothing rather than raw spot price
  • Liquidation threshold uses mark price, not index or last-trade price
  • Liquidation is not triggerable in the same block as an oracle update (flash loan defense)
  • Liquidation bonus is bounded — excessive bonus creates incentive to manufacture liquidatable positions
  • Partial liquidation path is tested: can a position be brought back to health without full closure?
  • Underwater liquidation (bad debt) is handled atomically — shortfall hits insurance fund in the same call

PnL Accounting Invariant

  • Prove the invariant: sum(collateral) + insuranceFund >= sum(unrealizedPnL_positive) at all code paths
  • No code path closes a position using a price that was not atomically validated in the same call
  • Rounding is always in protocol-favor (floor for payouts, ceiling for charges) to prevent slow drain