Skip to main content

Price Discovery

This page explains the mathematical building blocks of CCA V2's on-chain price discovery: Q96 fixed-point arithmetic, the clearing price algorithm, the cumulative accumulator system, and how settlement amounts are computed at bid exit time.

Q96 Fixed-Point Arithmetic

All prices in the protocol are represented using Q96 fixed-point arithmetic. Prices are stored as unsigned integers that have been multiplied by 2^96:

stored_value = actual_price * 2^96
actual_price = stored_value / 2^96

The constant:

// Source: packages/v2/programs/cca-v2/src/constants.rs
pub const Q96_SHIFT: u32 = 96;
pub const Q96: u128 = 1u128 << 96;
// Q96 = 79,228,162,514,264,337,593,543,950,336

This provides approximately 29 decimal digits of precision while keeping all arithmetic as deterministic integer operations. No floating point is ever used on-chain.

Why Q96?

Floating-point arithmetic is non-deterministic across different hardware, making it unsuitable for blockchain programs where every validator must produce identical results. Q96 fixed-point matches Uniswap's CCA specification and gives sufficient precision for price representation. It is also compatible with the U256 arithmetic used for intermediate products.

For example, a price of $0.15 per token is stored as:

0.15 * 2^96 = 11,884,224,377,139,650,639,031,592,555

Single Q96 values fit in Rust's u128. Intermediate products (such as amount * price) can exceed 128 bits and are computed using the U256 library.

U256 Representation

The protocol uses a custom U256 type stored as [u8; 32] in little-endian byte order. Internally, computations use four u64 limbs with u128 intermediates for carry propagation.

Key operations:

FunctionDescription
u256_mul_div(a, b, c)Computes (a * b) / c using a 512-bit intermediate to prevent overflow
u256_div_round_up(a, b)Ceiling division
u256_add / u256_subChecked addition/subtraction with overflow/underflow errors

The critical function for settlement math is u256_mul_div, which uses a 512-bit widening multiplication to avoid the intermediate overflow that would occur if computing a * b in 256 bits first.

Client-Side Q96

On the frontend and SDK, Q96 values are decoded using pure BigInt arithmetic:

const Q96 = BigInt("79228162514264337593543950336"); // 2^96

function decodeQ96(value: bigint): { whole: bigint; fractional: bigint } {
const whole = value / Q96;
const remainder = value % Q96;
return { whole, remainder };
}
danger

Never convert Q96 values through JavaScript Number() at any intermediate step. Values above 2^53 lose precision, producing incorrect prices. Always use BigInt end-to-end.

Checkpoint Architecture

A Checkpoint is a snapshot of the auction state at a specific auction block boundary. Checkpoints form a doubly-linked list (by prev_checkpoint_auction_block and next_checkpoint_auction_block) and contain the accumulators needed for lazy settlement.

Each Checkpoint PDA (["cca_checkpoint", auction_config, auction_block.to_le_bytes()]) stores:

FieldTypePurpose
clearing_priceu128 (Q96)The market-clearing price at this block
cumulative_mpsu32Total MPS of supply released through this block
cumulative_mps_per_priceU256The key settlement accumulator (see below)
currency_raised_at_clearing_price_q96_x7U256Pro-rata allocation tracker for marginal bids
statusenumUninitialized, Resolving, or Finalized

Checkpoint Contiguity

Checkpoints must be created for strictly consecutive auction blocks. The first checkpoint must be for block 0, and each subsequent checkpoint must be for latest_block + 1. This prevents gaps that could break settlement math.

Multi-Transaction Resolution

Because Solana transactions have compute budget limits, a single checkpoint may not be able to traverse all active ticks. The resolution can span multiple transactions:

  1. First call: creates the checkpoint, walks up to 10 ticks, transitions to Resolving if more remain
  2. Subsequent calls: resume from the saved cursor, walk 10 more ticks
  3. Final call: all ticks processed, transitions to Finalized

The Clearing Price Algorithm

The create_checkpoint instruction implements clearing price discovery by walking the tick linked list from the lowest active tick upward.

Step by Step

Step 1 -- Load initial state:

let sum_demand: U256 = state.sum_currency_demand_above_clearing;
let next_tick_price: u128 = state.next_active_tick_price;

Step 2 -- Compute initial candidate:

candidate_price = ceil(sum_demand / total_supply_unlocked)

Step 3 -- Tick traversal loop (up to 10 ticks per transaction):

while next_tick_price != 0
AND candidate_price >= next_tick_price
AND ticks_traversed < 10:

1. Load tick at next_tick_price
2. Subtract tick's demand: sum_demand -= tick.currency_demand_q96
3. Record last subtracted tick info
4. Advance: next_tick_price = tick.next_tick_price
5. Recompute: candidate_price = ceil(sum_demand / total_supply)
6. ticks_traversed++

The loop terminates when:

  • candidate_price < next_tick_price -- converged between two ticks
  • next_tick_price == 0 -- end of tick list reached
  • ticks_traversed == 10 -- save progress, resume in next transaction

Step 4 -- Apply constraints:

// Floor: clearing price never drops below pMin
let new_clearing_price = candidate_price.max(config.floor_price);
// Monotonicity: clearing price can only increase
let final_clearing_price = new_clearing_price.max(state.clearing_price);

Step 5 -- Update the settlement accumulator:

// This is the key accumulator for lazy settlement
cumulative_mps_per_price += delta_mps * Q96 / final_clearing_price

This accumulates delta_mps / clearing_price (scaled by Q96) at each checkpoint. The accumulator captures the integral of supply-over-price across the auction's lifetime.

The Settlement Accumulator

The cumulative_mps_per_price field is what makes lazy O(1) settlement possible. Instead of computing per-checkpoint allocations for each bid (which would be O(bids * checkpoints)), each bid's total allocation is computed from the difference between two accumulator values:

allocation = f(accumulator_at_exit - accumulator_at_entry)

At each checkpoint, the accumulator grows by:

delta = delta_mps * Q96 / clearing_price

This means higher clearing prices contribute less to the accumulator per unit of MPS, and lower clearing prices contribute more. Bids active during low-price periods receive proportionally more tokens -- exactly the correct economic behavior.

Settlement Math

Full Settlement (exit_bid)

For bids that were fully above the clearing price for their entire duration:

Tokens filled:

tokens_filled = amount_q96 * (end_accumulator - start_accumulator) / (Q96^2 * mps_remaining)

Where:

  • amount_q96 = bid's effective demand (scaled at placement time)
  • end_accumulator = end_checkpoint.cumulative_mps_per_price
  • start_accumulator = start_checkpoint.cumulative_mps_per_price
  • mps_remaining = MPS - bid.start_cumulative_mps

Currency spent:

currency_spent = min(
amount_q96 * (end_mps - start_mps) / mps_remaining,
raw_amount
)

Refund:

refund = raw_amount - currency_spent

Non-Graduated Auctions

If the auction did not graduate (total currency raised did not meet the threshold), all bids receive full refunds with zero tokens:

if !is_graduated {
bid.tokens_filled = 0;
bid.currency_spent = 0;
// Transfer full raw_amount back to bidder
}

Partial Settlement (exit_partially_filled_bid)

Bids that were at the clearing price for some portion of their duration use three-period settlement:

|<-- Period 1 -->|<-- Period 2 -->|<-- Period 3 -->|
Fully Filled At Clearing Outbid
(above clear) (marginal) (zero fill)

Period 1 (fully filled): Same formulas as exit_bid for the range where bid.max_price > clearing_price.

Period 2 (at clearing price): Pro-rata allocation among all bids at the clearing tick:

bid_share = amount_q96 / tick.currency_demand_q96
currency_spent_p2 = currency_at_clearing_delta * bid_share / mps_remaining
tokens_filled_p2 = currency_spent_p2 * Q96 / bid.max_price

Period 3 (outbid): Zero allocation. The clearing price rose above the bid's max_price.

The instruction requires "hint" checkpoints identifying the transition boundaries, which are validated against the doubly-linked checkpoint list to prevent manipulation.

Why Lazy Settlement?

The CCA's elegance lies in the cumulative accumulator. Instead of tracking per-checkpoint allocations for every bid (which would be O(bids * checkpoints) in storage and computation), it uses the difference A_exit - A_entry to compute each bid's total allocation in O(1) at exit time. This is what makes the protocol scalable to thousands of bids and hundreds of checkpoints.