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
| Account | PDA Seeds | Holds |
|---|---|---|
| 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
| Account | PDA Seeds | Holds |
|---|---|---|
| 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 State | Mint Authority Renounced? |
|---|---|
| Settled | Yes (in enable_claims) |
| EmergencyWithdrawn | Yes (in emergency_withdraw) |
| FailedRecovery | Yes (in recover_failed_auction) |
| Cancelled | Mint 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
creatoraccount 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 Parameter | Purpose |
|---|---|
protocol_fee_bps | Fee calculation at pool creation |
raydium_cpmm_program | Raydium program identity validation |
raydium_amm_config | Raydium config validation |
cca_program | CCA V2 program identity validation |
emergency_timeout_slots | Timeout 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:
| Extension | Status | Reason |
|---|---|---|
| MetadataPointer | Allowed | On-chain token name, symbol, URI |
| TokenMetadata | Allowed | Metadata storage on the mint |
| Transfer Hook | Blocked | Could add hidden fees or restrictions |
| Transfer Fee | Blocked | Hidden taxation on transfers |
| Permanent Delegate | Blocked | Third party could seize tokens |
| Close Authority | Blocked | Could destroy token accounts |
| Interest-Bearing | Blocked | Could 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:
| Category | Count | Description |
|---|---|---|
| Rust unit tests | 211 | CCA V2 core logic, including 48 property-based (proptest) |
| CCA V2 integration | 29 | Full auction lifecycle on local validator |
| Launchpad integration | 29 | Settlement pipeline, cancellation, emergency paths |
| Indexer integration | 36 | API endpoints, data integrity, event processing |
| E2E lifecycle | 26 | Complete launch-to-settlement on local validator |
| Fuzz testing | 6 targets | 7.2M+ iterations across U256, Q96, checkpoint, bid, and tick targets |
| Total | 338+ |
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
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:
| Invariant | Description |
|---|---|
clearing_price >= floor_price | Price never drops below the configured minimum |
clearing_price is monotonically non-decreasing | Price can only increase or stay the same across checkpoints |
tokens_cleared <= total_supply | More tokens than exist can never be allocated |
sum(bid_fills) == tokens_cleared | Every 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 fill | Bids below clearing price receive nothing |
cumulative_mps monotonically increases | Supply release never reverses |
currency_swept and tokens_swept prevent replay | Sweep operations can only execute once |
auction_bps + lp_bps + team_bps == 10000 | Token allocation is exactly 100% |