Crypto OS
Technical Crypto OSStage 4 · DeFi Engineering

T23 · Perp DEX

How do margin, funding, and a risk engine form one system?

Capabilities practiced
BuilderFinancial Literacy
Hands on
Implement position and margin calculations, simulate cascading liquidations, and record insurance-fund usage.
AI Lab
Have AI design a funding-rate formula. Backtest whether it pulls the price toward spot using historical data.

A Real-World Problem

Your perpetual trading venue has been live for three weeks. Most of your work went into matching: the order book, price-time priority, partial fills, cancellations, and event feeds. It runs cleanly. Latency is steady under load, and every fill is recorded.

Then one night the underlying asset drops 18% in twelve minutes.

The next morning, every subsystem log looks normal:

  • Matching did not stall; every fill reached the database.
  • The liquidation engine triggered 214 times, all successfully.
  • The price source kept updating, and none of T21's four checks raised an alarm.

Yet the books do not balance.

A perpetual market is a zero-sum ledger: every dollar a long earns must be a dollar a short loses. The day's aggregate position P&L should be zero. Instead it is negative: the system owes users money.

You trace the difference. Thirty-one long positions were closed after their losses exceeded the margin they posted. The counterparties' gains are real, but the losing side has no funds left to pay. Not one line of your three-week matching implementation is wrong, and it cannot help.

Who owns the hole, and who fills it?

Thought Experiment

Strip away the exchange. Keep one sheet of paper and one person. The sheet says A is long ten units, B is short ten units, and the underlying price is 100. For each one-unit price move, one pays the other ten.

The person's job is not matching; A and B already matched. Their only job is to ensure every balance on the paper can always be paid. Now assume A and B posted only 50 margin each, rather than 1,000.

As price moves, four questions arise. They are the whole chapter.

First, how much does each person have right now? At 103, A has an unrealized gain of 30 and B an unrealized loss of 30. B has only 20 equity left. This must be computable at any moment, not only when the position closes.

Second, when must someone be removed? If B is removed only when equity reaches zero, price is at 105. Can the position close exactly at 105? No: execution takes time and price does not wait. The system must act earlier, while B still has money. The amount remaining at the trigger is a parameter you must write down.

Third, who covers a late close? Price jumps from 104 to 108, leaving B with negative 30 equity. A should receive 80, but B contributed only 50. Someone must provide that missing 30 for the ledger to balance. Leaving it pending makes A's balance fictional.

Fourth, what if everyone wants to go long? The sheet requires one long for each short. If a hundred want to go long and only three want to short, who takes the other ninety-seven?

The answer to the fourth question is the funding rate from Chapter 18: put a price on occupying the crowded side so payments encourage movement toward the scarce side. That chapter explained why funding holds the price near its anchor. This one asks how to implement it.

Together, the four questions define a machine that continuously reads prices, recalculates equity, closes positions before equity runs out, and finds someone to cover shortfalls when it acts too late.

Matching decides who trades with whom. This machine decides whether the venue can pay its debts.

Your Decision

Return to the thirty-one bankrupt positions. The shortfall is a definite number; someone must bear it today.

Who do you choose?

What Happens

The options all answer one question: who bears the zero-sum ledger's missing amount today?

BearerPredictable to users?When usedMain cost
Unsettled bad debtNoNever resolvedFirst-come, first-served run
Dedicated fundYesFirst fallbackFinite size; denomination may fall too
Proportional haircutNoAfter fund exhaustionUnpriceable loss source
Forced close of winnersPartlyLast defenseCorrect traders lose part of a profitable position

The first row is the only truly wrong choice. The other three can be layers of one defense. In sequence they form the chapter's skeleton:

  1. Liquidate at the maintenance margin line
  2. Use the position’s remaining margin
  3. Use the dedicated fund
  4. Force-close profitable counterparts
Activate four defenses in order. Farther to the right, losses fall on people less connected to the original risk, making public rules more essential.

State the condition for bankruptcy as an inequality. It has the same shape as T22's:

A position goes bankrupt when:

    Mark-price movement between liquidation trigger and completed close
                >
    Maintenance margin rate + portion reserved for liquidation

Where:
  Left side = market speed × liquidation delay  ← not under your control
  Right side = your parameters                  ← design choices

Two differences from T22 are why this chapter exists: the right-hand buffer is an order of magnitude smaller (lending may have twenty-plus percentage points, while high-leverage perpetual maintenance margins may be a few percent or less), and the gap cannot be carried (a lender can account for bad debt over time; a perp venue must settle with a user today).

Therefore, a perpetual venue is fundamentally a continuously running risk engine; matching is its shell. Matching decides fill quality. The risk engine decides whether one night leaves it owing users money.

Build a Model

Consider five components in the order they respond to a price change.

1. Mark price: the foundation for every decision

A perpetual market has three prices. Confusing them is one of its costliest errors:

NameWhat it isUse
Index priceWeighted prices from external spot venuesFunding baseline and mark-price input
Mark priceIndex price with a bounded deviationP&L and liquidation
Last tradeMost recent fill on this venueDisplay and matching

Liquidation must use the mark, not the last trade. The last trade is set by this venue's own order book, which one trade can move. Using it lets anyone willing to hit a thin book decide which positions liquidate.

The simplest mark uses the external index as an anchor and allows trades to deviate only within a band:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

uint256 constant BPS = 10_000;

/// Anchor the mark to the external index and allow only a small deviation
function markPrice() public view returns (uint256) {
    uint256 index = priceGuard.readPrice();   // Includes T21's four checks
    uint256 last = lastTradePrice;

    uint256 upper = index * (BPS + markBandBps) / BPS;   // Configurable; often tens to hundreds of bps
    uint256 lower = index * (BPS - markBandBps) / BPS;

    if (last > upper) return upper;
    if (last < lower) return lower;
    return last;
}

The snippet's value is raising attack cost from moving your own book to moving the external spot market. External market depth is the numerator of T21's safety margin. All of T21 lives inside readPrice(). If it returns a manipulated value, the next four components fail while executing normally and raising no error.

2. Positions and margin: equity available at any instant

int256 constant WAD = 1e18;

struct Position {
    int256 size;          // Positive for long, negative for short; underlying units
    uint256 margin;       // Margin posted for this position
    uint256 entryPrice;   // Mark price at opening
    int256 entryFunding;  // Cumulative funding index at opening; see next section
}

/// Unrealized P&L: signed size handles longs and shorts in one expression
function unrealizedPnl(Position memory p, uint256 mark) pure returns (int256) {
    return p.size * (int256(mark) - int256(p.entryPrice)) / WAD;
}

/// Notional = absolute position size × mark price
function notional(Position memory p, uint256 mark) pure returns (uint256) {
    int256 s = p.size >= 0 ? p.size : -p.size;
    return uint256(s) * mark / uint256(WAD);
}

A signed size is the important design choice: long and short P&L, funding, and liquidation share formulas instead of branching in every function. Branches almost inevitably create asymmetric boundary errors.

3. Funding: you cannot iterate over positions

Chapter 18 explained why funding anchors prices. Engineering imposes a hard constraint: you cannot iterate through every position every eight hours to collect payments. There may be tens of thousands; one onchain call has no such budget.

Use the same pattern as T22 interest: maintain a global cumulative index, and store its opening value in each position. Funding owed at any moment is a subtraction:

int256 public cumulativeFunding;  // Cumulative amount owed per unit of long size, signed
uint256 public lastFundingAt;

/// Accrue funding since the last update into the index. O(1) regardless of position count
function accrueFunding(uint256 mark, uint256 index) internal {
    uint256 elapsed = block.timestamp - lastFundingAt;
    if (elapsed == 0) return;

    // Premium: deviation of mark from index, scaled by WAD
    int256 premium = (int256(mark) - int256(index)) * WAD / int256(index);

    // Rate: premium spread per second plus interest term, then clamp it
    int256 rate = premium / int256(fundingPeriod) + interestPerSecond;
    if (rate > fundingCapPerSecond) rate = fundingCapPerSecond;
    if (rate < -fundingCapPerSecond) rate = -fundingCapPerSecond;

    cumulativeFunding += rate * int256(elapsed) * int256(index) / WAD;
    lastFundingAt = block.timestamp;
}

/// Positive means the position pays; negative means it receives
function fundingOwed(Position memory p, int256 cumulative) pure returns (int256) {
    return p.size * (cumulative - p.entryFunding) / WAD;
}

/// Equity = margin + unrealized P&L - funding owed
function equity(Position memory p, uint256 mark, int256 cumulative) pure returns (int256) {
    return int256(p.margin) + unrealizedPnl(p, mark) - fundingOwed(p, cumulative);
}

Three parameters need explicit decisions. All are configurable; none has a universal value:

ParameterDeterminesIf wrong
fundingPeriodHow long the premium is spread overToo long weakens the anchor; too short charges repeatedly even in calm markets
fundingCapPerSecondMaximum force of the anchorToo narrow and it saturates during extremes, adding no more pressure
Charge basisNotional or marginThese differ by leverage; using the wrong one creates a scale error

The second row is often skipped. The cap is not just a safety setting; it limits the anchor's pull. Beyond it, a larger divergence no longer produces a larger payment. Arbitrageurs alone must restore price, precisely when they are most likely to retreat.

Another code requirement: accrue funding before checking liquidation. An idle position can cross maintenance purely through accumulated funding. Omit that update and apparently healthy positions may have needed liquidation long ago.

4. Liquidation: settle the ledger here

error NotLiquidatable();

/// Liquidatable when equity falls below the maintenance requirement
function isLiquidatable(
    Position memory p,
    uint256 mark,
    int256 cumulative,
    uint256 maintenanceBps    // Configurable maintenance rate, often tens to hundreds of bps, tiered by size
) pure returns (bool) {
    int256 e = equity(p, mark, cumulative);
    if (e <= 0) return true;
    return uint256(e) <= notional(p, mark) * maintenanceBps / BPS;
}

function liquidate(address trader) external {
    Position storage p = positions[trader];
    uint256 mark = markPrice();
    accrueFunding(mark, priceGuard.readPrice());          // Settle funding before the decision

    if (!isLiquidatable(p, mark, cumulativeFunding, maintenanceBps)) revert NotLiquidatable();

    int256 e = equity(p, mark, cumulativeFunding);
    uint256 fee = notional(p, mark) * liquidationFeeBps / BPS;   // The liquidator's entire incentive

    if (e > int256(fee)) {
        _payout(msg.sender, fee);                  // Fee to liquidator
        _payout(trader, uint256(e) - fee);         // Return remainder to trader
    } else if (e > 0) {
        _payout(msg.sender, uint256(e));           // Only part of fee can be paid
    } else {
        // Negative equity: bankruptcy. Find a payer for the gap in this transaction
        _coverShortfall(uint256(-e));
    }

    _closePosition(trader);
}

function _coverShortfall(uint256 gap) internal {
    emit ShortfallCovered(gap);
    if (insuranceFund >= gap) {
        insuranceFund -= gap;
        return;
    }
    uint256 remaining = gap - insuranceFund;
    insuranceFund = 0;
    _autoDeleverage(remaining);      // Rank winners by profit and leverage; force-close a portion
}

_coverShortfall is the one branch in the contract that must not revert. It may exhaust the fund and call the last defense, but it must return because the ledger must balance in this transaction.

Compare T22: lending's _recordBadDebt means “record now and resolve later.” Perpetuals' _coverShortfall means “find the funds now.” The same location has two different obligations.

5. Why liquidators arrive

As in T22, liquidators have no salary or duty. They appear only when notional times the liquidation fee exceeds gas plus slippage in closing the position. Thus the failure modes overlap: small positions are unprofitable, congestion destroys economics, and nine of ten competitors waste fees. The outcome differs: lending bad debt accumulates, while perpetuals can leave someone unable to withdraw today.

What It Is Called

PositionPosition

A user's open exposure in this market: direction, size, entry price, and posted margin.

Represent direction with a signed quantity rather than a boolean so P&L, funding, and liquidation share formulas. Splitting long and short logic tends to fail at asymmetric boundaries.

MarginMargin

Principal posted to support a leveraged position.

Distinguish initial margin, which limits position size, from maintenance margin, which triggers liquidation. Also distinguish margin modes: isolated margin gives a liquidation price calculable at opening; under cross margin that price moves continuously with P&L on other positions. Do not treat the opening estimate as a constant.

Mark PriceMark price

The price used for P&L and liquidation, anchored to an external index with only bounded deviation.

Its purpose is to move liquidation decisions from a thin market to a deep one. Replacing it with the last trade lets anyone trigger liquidation by moving a shallow book, a classic category of perpetual-market incident.

FundingFunding

Periodic payments between long and short sides, based on deviation of mark from index.

It is not a protocol fee: money moves among users. Onchain implementation cannot iterate through positions; it needs a cumulative index with each position's entry value. Check three parameters: period, cap, and charge basis. The cap sets the anchor's maximum force.

Insurance FundInsurance fund

Dedicated money to cover bankrupt positions, usually funded by liquidation surplus and retained fees.

Ask what asset it holds: the venue's own token may shrink during the same crisis. Also ask its size relative to open interest. Absolute fund size alone says little; open interest is the stock of leveraged exposure it must back.

Auto-deleveragingAuto-deleveraging

When the fund cannot cover a gap, forcibly close part of profitable opposing positions according to a ranking.

It is the final defense and a distinctive perpetual risk: you may trade in the right direction, have a profit, and still be closed by the rules. Publish the ranking so users know where they stand.

Hands-on

Hands onImplement position and margin arithmetic; simulate cascading liquidations and track the insurance fundSolidity and any contract test frameworkFree, using only a local network

Implement the central structure and four pure functions. Define Position and unrealizedPnl, notional, fundingOwed, and equity. Use a signed size.

Start with pure functions; verify every one by hand:

  • A long of ten units entered at 100 and marked at 103 has 30 unrealized profit. An equally sized short (negative size) loses 30.
  • If the cumulative funding index rises from 0 to 2, a ten-unit long owes 20 and the short receives 20.
  • Funding across both sides must sum to zero. This is the first zero-sum test and should remain true in later tests.

Implement the mark and funding accrual. Use an index plus band for the mark and T21's PriceGuard as the source, retaining all four checks. With a controllable stub feed, verify three things: a last trade inside the band is returned; one outside returns the clamped boundary, not the trade; and an expired source makes the liquidation path follow the fallback policy you explicitly chose in T21.

Implement liquidation and both fallbacks. Write every branch in liquidate, especially negative e. When the insurance fund is insufficient, _coverShortfall must continue and must not revert. Test it directly: set the fund to zero, create a bankrupt position, and call liquidation. It must return successfully and trigger auto-deleveraging.

A revert leaves a position that can never be liquidated. T22 warned about this; here it also traps the opposing exposure and prevents others' gains from becoming withdrawable.

Simulate a liquidation cascade and track fund depletion. This is the Lab's main exercise. Create three batches of long positions with increasing leverage and drive a falling-price sequence. Re-read the mark, accrue funding, and reevaluate after every step, rather than calculating the whole sequence once.

Initial mark 100; choose your own maintenance margin rate

Three long batches: A at 5× leverage with 100 million notional,
                    B at 10× with 300 million,
                    C at 20× with 200 million
Depth assumption: each 50 million notional closed lowers the mark another 1%
Initial shock: mark falls to 96

Record each round:

RoundMarkNotional liquidated this roundBankruptcy gapFund balanceAuto-deleveraging?
1
2
3

Find when the fund hits zero. That round is when your rules begin harming traders who were right about direction.

Now change only the depth assumption from “1% per 50 million” to “1% per 200 million,” and rerun. Side-by-side tables reveal the main lesson: the same leverage, parameters, and code can produce a routine pullback or an exhausted insurance fund solely because market depth changed. A perpetual on a poorly traded underlying can be an order of magnitude riskier without any contract difference.

Calculate attack economics. This is the chapter's quantitative exercise.

Earlier you asked whether the market might break the venue. Now ask: what would it cost someone to break it deliberately?

Scenario: your market has N notional at a leverage tier; liquidation prices
          are d percent from the current mark

Step 1: Cost to push the mark by d
        The mark is anchored to external spot markets, so those are what must move
        T21 formula: about spot depth × (sqrt(1+d) - 1)

Step 2: Scale of liquidations triggered, and how much forced closing moves price again
        Use the cascade and your depth assumption

Step 3: What the attacker receives
        a) Gain on a pre-positioned opposite exposure
        b) Liquidation fees when acting as liquidator
        c) Discounted assets bought during forced closes

Step 4: Profit = Step 3 - Step 1 - fees - gas

Try spot depths of five million, fifty million, and five hundred million, with d = 5%. The first case may be profitable. In a shallow underlying, liquidation can be a business someone creates, not just a consequence of falling prices.

Invert the exercise twice. First, how deep must external spot markets be for this attack to be unprofitable under your parameters? That is the minimum liquidity requirement for listing the asset, the same kind of threshold as T21 collateral admission. Second, if mark is replaced by last trade, what does Step 1 cost? Moving your own book may cost one or two orders of magnitude less than moving external spot. That multiple is the value of using a mark price.

Use only a local network, never real funds. Real funds enter only the capstone after Stage 5 security acceptance.

AI Lab

AI LabAsk AI to design funding; backtest whether it pulls perp prices toward spot using historical dataLevel 2 · AI Copilot

Ask in three steps; the third is the real output:

Step one:
Design a funding-rate formula for a perpetual market. Give the complete
expression, define each term, explain cap selection, and implement it onchain
in O(1) without iterating through positions. What do the period and caps protect?

Step two:
Given historical spot and simultaneous perpetual trade prices, calculate
funding period by period. For each period output deviation, rate, annualized
holding cost for longs, and annualized receipts for shorts. Mark periods that
hit either cap.

Step three:
During periods at the upper cap, did deviation keep growing or converge?
If it kept growing, what does that mean? How would you change parameters,
and what would the new costs be?

Most models can shape a plausible formula for step one, but implementation is often wrong: they loop over every position or move the work offchain. Ask how much gas an eight-hour sweep of tens of thousands of positions would require.

Use real historical data for step two. Fetch public spot and perpetual sequences, preferably during volatile periods, and feed them to the model. Independently recalculate three periods by hand.

Step three is the Lab's value. If divergence grows while funding sits at its cap, the mechanism has stopped strengthening even though every accounting entry looks normal. Ask what still pulls the price back: arbitrageurs, who may retreat in stressed markets. Finally plug the formula into your Lab contract and run the cascade: judge its behavior, not the formula's elegance.

Verify these points yourself after using AI

  • Does its formula include upper and lower caps? Without them extreme markets can produce absurd rates
  • Substitute the caps and calculate maximum anchor pressure at a 10% deviation; is it enough?
  • Does it charge on notional or on margin? These differ by leverage, so it must say which
  • Does its implementation iterate through all positions? Onchain it must use a cumulative index
  • When funding is negative, does it explain money flowing from shorts to longs and the accounting?
  • Does the backtest distinguish index and mark prices? Mixing them makes results unreliable
  • Treat every specific period, cap, or maintenance rate as a configurable starting point, not a fact

Real-World Case

When the last trade triggered liquidationsEarly perpetual markets

Some early venues liquidated using their own contract's last trade. The attack is calculable: estimate a group of positions' trigger prices, estimate the cost of hitting a thin local book until it reaches them, and compare with forced-close volume. If the latter is larger, it is profitable. The forced orders hit the same thin book, stretching the wick and triggering more positions. Price later returns, but closed positions do not.

The industry moved broadly toward indexes weighted from external spot venues and marks constrained around them. The change is exactly what markPrice() does: it moves liquidation judgment from a thin market to a deep one. It is T21's problem in another form.

Using one's own position to drain an insurance fund2022An onchain perpetual venue

An attacker chose an underlying with shallow spot liquidity that nevertheless had a perpetual market. They built a large long perp position, then bought the spot market higher. The index rose and their unrealized profit became enormous; under that venue's rules, those gains could be withdrawn immediately.

No code misbehaved. They were profitable, so no liquidation triggered; the oracle accurately reflected a real spot rise; and margin arithmetic was correct. The false assumption was that “unrealized profit is real.”

Two lessons: check whether spot depth supports a market's exposure limit before listing, and decide separately whether unrealized profit may be withdrawn at once. Many systems restrict it.

One price shock exhausts the insurance fundRepeated volatile marketsMultiple venues

A price gap leaves many positions below maintenance before closing begins. Losses exceed the remaining margin; the insurance fund drains and auto-deleveraging activates broadly. Many users who correctly chose the direction and had profitable positions are forcibly closed in part. They did nothing wrong and often did not expect it, though the rules disclosed it.

Common follow-up changes are to publish the fund-to-open-interest ratio, show users their place in the auto-deleveraging queue, and tier maintenance margin upward for larger positions, which need more buffer because closing them moves price further. The last is most effective and least popular among large traders.

Funding remains at its cap through a one-sided marketSustained trendsMultiple perpetual markets

Funding can sit at its upper or lower cap across many periods while price divergence keeps growing.

Do not mistake “the rate is at its maximum” for “pressure keeps strengthening.” Once capped, the rate no longer increases with divergence; only arbitrageurs pull the market back, and they face the greatest difficulty during stress.

The cap is the anchor's force limit. It belongs near the top of the parameter checklist for any perpetual market, though most users never inspect it.

Change One Variable

What ifUse last trade again instead of mark price

When the book is deep, code and tests look identical. The difference emerges as the book thins. It changes the nature of risk, not just its size: liquidation stops being a consequence of price moves and becomes something a trader can manufacture.

The multiple you calculated at the end of the Lab is the cost of this change. This is the last line to change in the chapter: it switches attack cost from moving external spot to moving your own book.

What ifRaise maximum leverage from 20× to 100×

The move needed to bankrupt an individual position shrinks from about 4.5% to 0.5%. More importantly, the same margin can now support five times the notional. Open interest grows with new positions whose buffers are thin, while the insurance fund does not grow automatically. A 0.5% disturbance can now start a cascade that previously needed 5%.

Rerun the Lab's table with the higher leverage; fund exhaustion arrives earlier. A higher leverage limit is not merely an extra user choice. It reduces the disturbance needed to move the market from calm to failure.

What ifAccrue funding every block instead of settling every eight hours

The anchor tightens: divergence begins incurring cost immediately, without waiting for a settlement time. With a cumulative index, this is nearly free in design terms: call accrueFunding before each state change.

Three changes follow. Opening or closing just around a settlement boundary no longer offers an easy timing trade, a benefit. Each transaction needs another price read and index update, raising gas. Precision becomes important: per-block rates are tiny, and repeated integer truncation can systematically over- or undercharge across hundreds of thousands of blocks. Round to protect the fund, the same principle as T19's rule that rounding must favor the pool.

What ifDenominate the insurance fund in a token issued by the venue

In ordinary times the fund looks large and inexpensive to maintain. During extreme markets, shortfalls grow while the token's price often falls, shrinking coverage exactly when needed. A second defense fails alongside the first.

Ask the deeper question from Chapter 21: is the insurance fund driven by the same risk it is supposed to insure? If yes, it acts more like leverage than insurance.

Takeaway Questions

5
Who is paying?

Keep three payment flows separate: funding transfers between longs and shorts without passing through the protocol; liquidation fees flow from liquidated traders to liquidators; and bankruptcy gaps flow from the fund or winners to the bankrupt trader's counterparties. Users pay and receive most of these flows.

The protocol is primarily the accountant and backstop, not the fee recipient. Its failure mode is inability to pay balances, rather than an ordinary operating loss.

8
Who supplies liquidity?

Perpetual liquidity has three layers: order-book depth governs opening and closing slippage; external spot depth governs whether the mark can be manipulated; and the insurance fund provides capital without finding a counterparty when a gap opens. Analyses often inspect only the first. A deep local book on a thin spot asset is the risk this chapter asks you to recognize.

9
Who bears the risk?

Answer in four layers, matching the defense chain: normally, leveraged traders bear their own risk; during liquidation, some risk moves to the liquidator through slippage and losing the race; after bankruptcy, the fund pays; after fund exhaustion, profitable opponents pay. The last is unique to perpetuals: the rules can close you even when your market call was right. Inspect fund size, denomination, and auto-deleveraging rank.

11
Can the product still run if its token price goes to zero?

Here is the concrete test: if the venue's own token fell to zero, how much insurance would remain? Extend it to all volatile dependencies: fund denomination, liquidators' hedging instruments, and market-maker incentives. Each can deteriorate together with the risk it is meant to address.

Chapter Check

Key idea

A perpetual contract is fundamentally a continuously running risk engine; order matching is only its outer shell.

Finished this chapter’s lab? Mark it completeView all progress

On this page