Skip to main content

Security Model

Runner Protocol is designed with security as a core principle. All auction logic, token custody, and settlement execution happens entirely on-chain, with no off-chain dependencies or privileged admin operations that could compromise fairness.

Fully On-Chain and Trustless

Every aspect of the protocol executes through Solana programs:

  • Price discovery is computed on-chain via the checkpoint tick-traversal mechanism
  • Token custody is held in Program Derived Accounts (PDAs), not in wallets controlled by any individual
  • Settlement is permissionless -- anyone can trigger each step of the pipeline
  • No off-chain oracles or external data feeds are involved in any auction operation

There is no multisig, admin key, or governance mechanism that can override the auction mechanics, drain escrows, or alter clearing prices.

PDA-Controlled Custody

All tokens and currency are held in Program Derived Accounts (PDAs) -- accounts whose signing authority is derived deterministically from the program and specific seeds. This means:

  • No individual holds the private key to any escrow or vault
  • Token transfers can only happen through the program's validated instruction handlers
  • The program enforces all invariants before any transfer occurs

CCA V2 Vaults

AccountPDA SeedsHolds
Base vault["cca_base_vault", auction_config]Auction tokens (base mint)
Quote vault["cca_quote_vault", auction_config]Bidder deposits (quote mint)
Vault authority["cca_vault_auth", auction_config]Signing authority for both vaults

Launchpad Escrows

AccountPDA SeedsHolds
Auction escrow["escrow", launch_config, "auction"]Auction allocation (Token-2022)
LP escrow["escrow", launch_config, "lp"]LP allocation (Token-2022)
Team escrow["escrow", launch_config, "team"]Team allocation (Token-2022)
Proceeds escrow["escrow", launch_config, "proceeds"]Swept quote tokens (SPL Token)
Fee escrow["escrow", launch_config, "fee"]Raydium pool creation fee (lamports)
Escrow authority["cca_launch_auth", launch_config]Signing authority for all escrows

Claims Gated Behind Pool Creation

Token claims are disabled until the Raydium liquidity pool has been successfully created (launch state = PoolCreated). The claim_slot on the CCA V2 program is set to far-future at auction creation, and only updated to the current slot during the enable_claims step -- which can only execute after create_raydium_pool.

This prevents the critical attack where a bidder could claim tokens before the official pool exists and create a competing pool at a manipulated price.

Mandatory Mint Authority Renunciation

On every terminal settlement path, the Token-2022 mint authority is renounced via set_authority(None):

Terminal StateMint Authority Renounced?
SettledYes (in enable_claims)
EmergencyWithdrawnYes (in emergency_withdraw)
FailedRecoveryYes (in recover_failed_auction)
CancelledMint authority was never transferred from the Launchpad PDA

After renunciation:

  • No one can mint additional tokens -- ever
  • The total supply is permanently fixed at the amount minted during launch creation
  • This eliminates inflation attacks and rug-pull scenarios where a creator mints and dumps additional supply

This is enforced programmatically. There is no code path that reaches a terminal state without renouncing mint authority.

State-Before-Transfer Pattern

Token transfers follow the state-before-transfer pattern to prevent reentrancy and double-spend:

// claim_tokens: zero balance BEFORE CPI transfer
let claim_amount = bid.tokens_filled;
bid.tokens_filled = 0; // State update first
// Then CPI transfer
transfer(base_vault, bidder_account, claim_amount)?;

Similarly, sweep operations set currency_swept = true and tokens_swept = true before executing transfers, preventing replay.

Emergency Withdrawal with Timeout

If settlement stalls at any intermediate step for longer than a configurable timeout, an emergency withdrawal can be triggered by anyone:

  • The timeout duration is snapshotted into the LaunchConfig at creation time -- the admin cannot change it retroactively
  • Emergency withdrawal is permissionless -- anyone can trigger it after the timeout, not just the creator or admin
  • The creator account in the emergency withdrawal instruction is not a signer -- this allows keeper bots to rescue funds for offline creators
  • Mint authority is still renounced in the emergency path

Snapshotted Protocol Configuration

Critical protocol parameters are copied into each LaunchConfig at creation time, not read from the global ProtocolConfig during settlement:

Snapshotted ParameterPurpose
protocol_fee_bpsFee calculation at pool creation
raydium_cpmm_programRaydium program identity validation
raydium_amm_configRaydium config validation
cca_programCCA V2 program identity validation
emergency_timeout_slotsTimeout for emergency escape

This prevents a retroactive attack where changing the ProtocolConfig could affect in-flight settlements. Even if the admin updates the global config, existing launches use their snapshotted values.

Token-2022 Extension Whitelist

Launched tokens use Solana's Token-2022 standard with only the MetadataPointer and TokenMetadata extensions enabled. The validate_only_metadata_extensions function explicitly blocks all other extensions:

ExtensionStatusReason
MetadataPointerAllowedOn-chain token name, symbol, URI
TokenMetadataAllowedMetadata storage on the mint
Transfer HookBlockedCould add hidden fees or restrictions
Transfer FeeBlockedHidden taxation on transfers
Permanent DelegateBlockedThird party could seize tokens
Close AuthorityBlockedCould destroy token accounts
Interest-BearingBlockedCould inflate supply indirectly

This ensures that launched tokens behave as standard fungible tokens with no hidden functionality.

Non-Withdrawable In-Range Bids

Bids with a max_price at or above the current clearing price cannot be withdrawn. This prevents:

  • Strategic bid manipulation: placing large fake bids to influence the clearing price, then withdrawing
  • Last-minute withdrawal attacks: destabilizing the clearing price by removing demand

Only out-of-range bids (max_price < clearing_price) can be withdrawn by the bidder.

Clearing Price Monotonicity

The clearing price is enforced to be monotonically non-decreasing across checkpoints:

let final_clearing_price = new_clearing_price.max(state.clearing_price);

If this invariant would be violated, the instruction returns MonotonicityViolated (error 6098). This prevents an attacker from manipulating the clearing price downward through strategic demand withdrawal.

Testing and Verification

The protocol has undergone extensive testing:

CategoryCountDescription
Rust unit tests211CCA V2 core logic, including 48 property-based (proptest)
CCA V2 integration29Full auction lifecycle on local validator
Launchpad integration29Settlement pipeline, cancellation, emergency paths
Indexer integration36API endpoints, data integrity, event processing
E2E lifecycle26Complete launch-to-settlement on local validator
Fuzz testing6 targets7.2M+ iterations across U256, Q96, checkpoint, bid, and tick targets
Total338+

The protocol underwent multiple rounds of adversarial security review examining the code from attacker, UX, equivalence, and correctness perspectives. These reviews identified and fixed:

  • CCA V2: 2 HIGH and 4 MEDIUM severity findings
  • Launchpad: 16 security fixes and 3 design gap fixes across 3 review rounds
Audit Status

A professional third-party security audit has not yet been completed. One is planned before any mainnet deployment. The testing and review described above represent internal verification only, not a substitute for a professional audit.

Protocol Invariants

The protocol enforces the following invariants at all times:

InvariantDescription
clearing_price >= floor_pricePrice never drops below the configured minimum
clearing_price is monotonically non-decreasingPrice can only increase or stay the same across checkpoints
tokens_cleared <= total_supplyMore tokens than exist can never be allocated
sum(bid_fills) == tokens_clearedEvery allocated token is accounted for in a specific bid
total_quote_collected == sum(fill * price)Currency accounting is consistent
Out-of-range bids have zero fillBids below clearing price receive nothing
cumulative_mps monotonically increasesSupply release never reverses
currency_swept and tokens_swept prevent replaySweep operations can only execute once
auction_bps + lp_bps + team_bps == 10000Token allocation is exactly 100%