Skip to content

Settlement

Matching decides who trades with whom. Settlement moves the tokens. It happens on Solana, in the markets program, through a single instruction.

Settles one maker leg of a maker↔taker match on one outcome token, as a direct swap. The market vault is not involved.

pub fn execute_trade(
ctx: Ctx,
side: Side, // the MAKER's side: 0 = BUY, 1 = SELL
maker_amount: u64, // the maker order's total size
taker_amount: u64, // the taker order's total size
fee_rate_bps: u16,
making: u64, // this fill: 0 < making <= maker_amount
// the maker's off-chain order, for on-chain signature verification
salt: u64,
signer: Pubkey,
order_taker: Pubkey,
token_id: Vec<u8>,
expiration: u64,
nonce: u64,
signature_type: u8,
signature: [u8; 64],
) -> Result<()>

What it does, in order:

  1. Validate making > 0 && making <= maker_amount, EOA only, signer == maker.
  2. taking = calculate_taking_amount(making, maker_amount, taker_amount).
  3. fee = calculate_fee(…) on the taker asset.
  4. Require fee <= taking.
  5. Transfer: maker → taker (making, via the delegate PDA), taker → maker (taking − fee), taker → fee vault (fee).
  6. Advance the market’s price accumulator.
  7. Emit TradeSettled.
PartySigns?Why
TakeryesTheir own leg
OperatoryesOnly the config operator may settle trades
Programyes (PDA)Signs for the maker’s delegate
MakernoTokens move under a standing Approve allowance

The maker being absent is the whole point. Post an order, close your laptop; the delegate PDA carries your side.

Delegate PDA seeds = ["delegate", maker]

A standing allowance is powerful, so the program will not move a maker’s tokens on the operator’s word alone. The maker’s off-chain order travels as instruction data, and the handler:

  • Recomputes the domain-separated SHA-256 order hash.
  • Requires a top-level ed25519 verify instruction in the same transaction. The ed25519 program cannot be CPI’d, so the handler scans the Instructions sysvar to prove it’s there.
  • Requires signature_type == 0 and signer == maker.
  • Requires the order’s taker to be public or the settling taker.
  • Requires a non-zero expiration to be in the future.

So the allowance can only be spent against an order the maker actually signed, for the amount and price they signed.

Fifteen, in #[derive(Accounts)] order:

#Account
1Market PDA (must be Open or Closed)
2Outcome mint (YES or NO)
3Collateral mint
4Maker (not a signer)
5Taker (signer)
6Delegate PDA
7–8Maker’s collateral and outcome token accounts
9–10Taker’s collateral and outcome token accounts
11Fee vault token account
12Instructions sysvar
13SPL Token program
14Config PDA
15Config operator (signer)

The program keeps no per-order state. There is no nonce PDA and no fill-status PDA. It verifies a signature and moves tokens; it does not remember having done so.

Replay protection therefore lives entirely in the Executor:

  • De-dupes by full trade key — taker and maker order hashes, fill amounts, fee rate, bucket.
  • Persists that key set alongside the pending snapshot, so it survives a restart.
  • Bounded, with oldest keys evicted past MAX_DEDUPE_KEYS.

The executor is the trusted single settlement instance. A caller who bypasses it and sends execute_trade directly can settle the same signed order repeatedly — each time up to maker_amount, in total bounded only by the maker’s standing delegate allowance.

Two implications for anyone trading here:

  1. The operator signature is what keeps that path closed. It is required on every settlement.
  2. Size your delegate allowance deliberately. It is the real ceiling on your exposure, not your order size.

The executor:

  1. Re-runs order validation on every order in the request.
  2. Checks the de-dupe key.
  3. Assembles one execute_trade per maker leg, plus the residual mint_tokens / merge_tokens for same-side matches, the top-level ed25519 verify instruction, and ComputeBudget instructions.
  4. Sets the CU budget to simulateTransaction × 1.2, capped at MAX_CU.
  5. Signs with the operator keypair (payer) and the taker key(s).
  6. Compiles to v0 + address lookup table when ADDRESS_LOOKUP_TABLE is set — MINT/MERGE transactions exceed the 1232-byte legacy limit.

A worker batches one getSignatureStatuses per tick across all pending signatures. Solana has no reorgs, so finalized is the settlement point.

OutcomeAction
FinalizedLedger UpdateTrade(CONFIRMED)
Out of gas (CU ≥ 95% of limit)Retry with CU ×2, fee ×2, up to RETRIES_LIMIT
FailedUpdateTrade(FAILED) + OrderSizeUpdate to eject affected orders
Idle past STUCK_TX_MAX_IDLE_TIMERe-executed through the same path

The pending map and de-dupe set are snapshotted to local JSON on every change. On restart, recovered transactions are re-confirmed, never re-sent.

#[event]
pub struct TradeSettled {
pub market_id: u64,
pub side: Side, // the maker's side
pub making: u64,
pub taking: u64,
pub fee: u64,
pub maker: Pubkey,
pub taker: Pubkey,
pub signer: Pubkey, // verified on-chain
pub outcome_mint: Pubkey, // YES or NO — tells indexers which leg
pub order_hash: [u8; 32], // the off-chain de-duping key
}

order_hash lets an indexer join on-chain settlements to off-chain orders without guessing, and outcome_mint disambiguates which side of the market moved.

execute_trade requires the market to be Open or Closed and before end_time. The Closed allowance lets a match made just before close still land — it is not a window for new trading.