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.
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:
| Function | Description |
|---|---|
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_sub | Checked 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 };
}
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:
| Field | Type | Purpose |
|---|---|---|
clearing_price | u128 (Q96) | The market-clearing price at this block |
cumulative_mps | u32 | Total MPS of supply released through this block |
cumulative_mps_per_price | U256 | The key settlement accumulator (see below) |
currency_raised_at_clearing_price_q96_x7 | U256 | Pro-rata allocation tracker for marginal bids |
status | enum | Uninitialized, 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:
- First call: creates the checkpoint, walks up to 10 ticks, transitions to
Resolvingif more remain - Subsequent calls: resume from the saved cursor, walk 10 more ticks
- 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 ticksnext_tick_price == 0-- end of tick list reachedticks_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_pricestart_accumulator=start_checkpoint.cumulative_mps_per_pricemps_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.
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.