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.
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.
// 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.
| # | Check | Error on failure |
|---|---|---|
| 1 | Market is not paused | MarketPaused |
| 2 | Fill exists in the committed book | FillNotFound |
| 3 | Fill has not already been claimed | FillNotFound |
| 4 | Taker and maker are different keys, since identical keys collide the two position PDAs | Unauthorized |
| 5 | Supplied user accounts belong to the fill's own taker and maker | Unauthorized |
| 6 | Each revealed size is at or below that leg's own band ceiling | SizeExceedsBand |
| 7 | SHA-256 of each revealed plaintext equals that leg's commitment | CommitmentMismatch |
| 8 | The two revealed sizes are equal, since one fill is one matched quantity | CommitmentMismatch |
| 9 | Each side holds enough unlocked collateral for its position | InsufficientCollateral |
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.
// 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.
// 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 field | Set to |
|---|---|
side | Long for the taker, Short for the maker |
size_lots | the revealed, verified quantity |
entry_price_ticks | the fill price, which is the resting order's price |
collateral_locked | notional x 100 / leverage_bps |
leverage_bps | the revealed leverage, which the commitment also bound |
last_funding_index | the market's cumulative funding for that side |
opened_ts | the settlement block timestamp |
status | Open |
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.
// 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 inscripts/setup-market.tssets it to 8 hours. update_fundingrefuses 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_fundingapplies 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.
// 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;
| Constant | Value | Meaning |
|---|---|---|
LIQUIDATION_THRESHOLD_BPS | 8,000 | a position is liquidatable once losses have eaten 20% of the collateral locked at entry |
LIQUIDATION_BOUNTY_BPS | 500 | the liquidator keeps 5% of the remaining collateral |
MAX_ORACLE_AGE_SECS | 60 | a price update older than this fails the instruction |
MAX_FUNDING_BPS_PER_INTERVAL | 2,000 | 20% per interval, the clamp on the funding rate |
MAX_LEVERAGE_BPS | 20,000 | 200x, 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_positionis the trader's own exit, which realises profit and loss and unlocks the remaining collateral.