darkbookDocs

06DarkBook documentation

Settlement

Settlement is where the sealed number becomes a position. It is also the last point at which the program can refuse, so most of the safety in this design is concentrated in one instruction.

Read fromix/settlement.rsix/positions.rsix/funding.rsconstants.rs

A fill sitting in the committed order book is not a trade. It records that two traders crossed at a price, with each side bounded by its own declared ceiling. Turning that into open interest requires the plaintext for both legs, and the instruction that accepts it, claim_fill, is permissionless in the sense that anyone holding both plaintexts can submit it, and constrained in the sense that it verifies everything it is given.

claim_fill

Nine arguments, two of them the commitments the program is about to reproduce. The settler pays rent for both position accounts.

rust
// programs/darkbook/src/ix/settlement.rs
pub fn claim_fill(
    ctx: Context<ClaimFill>,
    fill_id: u64,
    taker_salt: [u8; 32],
    taker_size: u64,
    taker_leverage: u16,
    taker_commitment: [u8; 32],
    maker_salt: [u8; 32],
    maker_size: u64,
    maker_leverage: u16,
    maker_commitment: [u8; 32],
) -> Result<()>

The checks run in this order, and any failure reverts the whole instruction, so a half-settled fill is not a state the program can reach.

#CheckError on failure
1Market is not pausedMarketPaused
2Fill exists in the committed bookFillNotFound
3Fill has not already been claimedFillNotFound
4Taker and maker are different keys, since identical keys collide the two position PDAsUnauthorized
5Supplied user accounts belong to the fill's own taker and makerUnauthorized
6Each revealed size is at or below that leg's own band ceilingSizeExceedsBand
7SHA-256 of each revealed plaintext equals that leg's commitmentCommitmentMismatch
8The two revealed sizes are equal, since one fill is one matched quantityCommitmentMismatch
9Each side holds enough unlocked collateral for its positionInsufficientCollateral

The per-leg band bound

Each side is bounded by the ceiling it published, never by the counterparty's. Bounding the maker by the taker's band would leave the maker's own published ceiling non-binding, and that ceiling is the entire worst case a maker prices before quoting.

rust
// programs/darkbook/src/ix/settlement.rs:66-71
let taker_band_max = SizeBand::max_lots_for_code(fill.taker_size_band)
    .ok_or(DarkbookError::InvalidSizeBand)?;
let maker_band_max = SizeBand::max_lots_for_code(fill.maker_size_band)
    .ok_or(DarkbookError::InvalidSizeBand)?;
require!(taker_size <= taker_band_max, DarkbookError::SizeExceedsBand);
require!(maker_size <= maker_band_max, DarkbookError::SizeExceedsBand);

What opens

Two positions, one per leg. The taker took the long side because the crossing bid is recorded as the aggressor, and the maker holds the short. This is also the first point at which collateral is actually locked. Placement only required the trader to hold enough unlocked balance to cover the band ceiling; the lock here is taken against the revealed size, which is smaller for anyone who declared a band above their true quantity.

rust
// programs/darkbook/src/ix/settlement.rs, compute_collateral
let notional = (size_lots as u128) * (price_ticks as u128);
let collateral = notional * 100 / (leverage_bps as u128);
Position fieldSet to
sideLong for the taker, Short for the maker
size_lotsthe revealed, verified quantity
entry_price_ticksthe fill price, which is the resting order's price
collateral_lockednotional x 100 / leverage_bps
leverage_bpsthe revealed leverage, which the commitment also bound
last_funding_indexthe market's cumulative funding for that side
opened_tsthe settlement block timestamp
statusOpen

Market open interest is incremented on both sides at the same time, because the funding rate is computed from total_long_size against total_short_size.

Marks and the oracle

mark_position is read-only. It reads a Pyth price update, computes unrealized profit and loss against the entry price, and logs it. Nothing on the account changes, which is why it is safe to crank from anywhere.

Every instruction that reads a price, including liquidation and the funding update, goes through the same oracle path and rejects a price update older than 60 seconds. A stale oracle fails the transaction rather than returning a default, because a silent zero would propagate into every downstream profit calculation.

Funding

Funding is an open-interest imbalance transfer. The rate is the signed imbalance scaled by the per-interval cap, then clamped to it.

rust
// programs/darkbook/src/ix/funding.rs, update_funding

// MAX_FUNDING_BPS_PER_INTERVAL = 2_000, a 20% per-interval cap
let funding_rate_bps = (long_size - short_size)
    * MAX_FUNDING_BPS_PER_INTERVAL
    / total_size;
let funding_rate_bps =
    funding_rate_bps.clamp(-MAX_FUNDING_BPS_PER_INTERVAL, MAX_FUNDING_BPS_PER_INTERVAL);

// longs pay shorts when the rate is positive
market.cum_funding_long  += funding_rate_bps * intervals;
market.cum_funding_short -= funding_rate_bps * intervals;
  • The interval is a market parameter, funding_interval_secs, not a program constant. Market setup in scripts/setup-market.ts sets it to 8 hours.
  • update_funding refuses to run before a full interval has elapsed, and when several intervals have passed it scales the rate by the number of them rather than skipping the backlog.
  • accrue_funding applies the accumulated index to a single position and is permissionless, so a position cannot dodge funding by nobody cranking it.
  • With balanced open interest the rate is zero. With one-sided open interest it saturates at the 2,000 bps cap.

Liquidation

Permissionless, and gated on one ratio. After funding is accrued and the position is marked against the oracle, the remaining collateral is compared to what was originally locked.

rust
// programs/darkbook/src/ix/positions.rs, liquidate_position
let ratio_bps = remaining_collateral * 10_000 / position.collateral_locked;

// LIQUIDATION_THRESHOLD_BPS = 8_000
require!(
    ratio_bps < LIQUIDATION_THRESHOLD_BPS as u64,
    DarkbookError::NotLiquidatable
);

// LIQUIDATION_BOUNTY_BPS = 500, 5% of what is left
let bounty = remaining_collateral * LIQUIDATION_BOUNTY_BPS as u64 / 10_000;
ConstantValueMeaning
LIQUIDATION_THRESHOLD_BPS8,000a position is liquidatable once losses have eaten 20% of the collateral locked at entry
LIQUIDATION_BOUNTY_BPS500the liquidator keeps 5% of the remaining collateral
MAX_ORACLE_AGE_SECS60a price update older than this fails the instruction
MAX_FUNDING_BPS_PER_INTERVAL2,00020% per interval, the clamp on the funding rate
MAX_LEVERAGE_BPS20,000200x, the program ceiling; a market can set a lower one

Read from programs/darkbook/src/constants.rs. The threshold has to be at most 10,000, otherwise a flat position at a ratio of exactly 10,000 would be liquidatable.

  • A trader cannot liquidate their own position, which would otherwise let them sweep their own bounty.
  • A position with zero locked collateral has no exposure and is not liquidatable.
  • Funding is accrued first, so a position that only becomes undercollateralised after funding is handled correctly rather than on the next crank.
  • close_position is the trader's own exit, which realises profit and loss and unlocks the remaining collateral.