Skip to main content

Security Model

Security architecture and protections in the Seesaw protocol.

Threat Model

Adversary Capabilities

CapabilityDescription
Arbitrary TransactionsCan submit any valid Solana transaction
Multiple AccountsCan create unlimited accounts (Sybil)
Flash LoansCan access large capital temporarily
MEV ExtractionCan frontrun/sandwich transactions
Market ManipulationCan trade to move prices

Adversary Limitations

LimitationAssumption
Forge SignaturesEd25519 is secure
Break PythOracle data is authentic
Control SolanaNetwork operates honestly
Infinite CapitalCapital is finite per tx
Time TravelClock moves forward only

Security Layers

Account Security

Discriminator Validation

Every account type has a unique discriminator:

fn validate_account_type<T: Account>(account: &AccountInfo) -> Result<()> {
    let data = account.try_borrow_data()?;

    require!(
        data[..8] == T::DISCRIMINATOR,
        Error::InvalidAccountType
    );

    Ok(())
}

Owner Validation

Accounts must be owned by the program:

fn validate_owner(account: &AccountInfo, program_id: &Pubkey) -> Result<()> {
    require!(
        account.owner == program_id,
        Error::InvalidAccountOwner
    );
    Ok(())
}

PDA Validation

PDAs must derive correctly:

fn validate_pda(
    account: &AccountInfo,
    seeds: &[&[u8]],
    bump: u8,
    program_id: &Pubkey,
) -> Result<()> {
    let expected = Pubkey::create_program_address(
        &[seeds, &[&[bump]]].concat(),
        program_id,
    )?;

    require!(account.key == &expected, Error::InvalidPDA);
    Ok(())
}

Arithmetic Security

Checked Operations

All arithmetic uses checked operations:

// WRONG
let result = a + b; // Can overflow

// RIGHT
let result = a.checked_add(b).ok_or(Error::MathOverflow)?;

Protocol-Favorable Rounding

// Fees round UP (protocol receives more)
fn calculate_taker_fee(amount: u64, fee_bps: u16) -> u64 {
    let fee = amount * (fee_bps as u64);
    (fee + 9_999) / 10_000  // Ceiling division
}

// Payouts round DOWN (protocol pays less)
fn calculate_payout(shares: u64, price_bps: u16) -> u64 {
    shares * (price_bps as u64) / 10_000  // Floor division
}

Tick Rounding

// Bids round DOWN (buyer pays less)
fn round_bid(price: u16, tick: u16) -> u16 {
    (price / tick) * tick
}

// Asks round UP (seller receives more)
fn round_ask(price: u16, tick: u16) -> u16 {
    let rem = price % tick;
    if rem == 0 { price } else { price + tick - rem }
}

State Machine Security

State Transition Enforcement

fn validate_state_transition(
    current: MarketState,
    instruction: Instruction,
) -> Result<()> {
    let valid = match (current, instruction) {
        (Pending, CreateMarket) => true,
        (Created, SnapshotStart) => true,
        (Trading, PlaceOrder) => true,
        (Trading, CancelOrder) => true,
        (Trading, SnapshotEnd) => true,
        (Settling, ResolveMarket) => true,
        (Resolved, SettlePosition) => true,
        (Resolved, CloseMarket) => true,
        _ => false,
    };

    require!(valid, Error::InvalidStateTransition);
    Ok(())
}

Timing Enforcement

fn validate_timing(
    market: &MarketAccount,
    instruction: Instruction,
    clock: &Clock,
) -> Result<()> {
    match instruction {
        SnapshotStart => {
            require!(clock.unix_timestamp >= market.t_start, Error::TooEarly);
        }
        SnapshotEnd => {
            require!(clock.unix_timestamp >= market.t_end, Error::TooEarly);
        }
        PlaceOrder => {
            require!(clock.unix_timestamp < market.t_end, Error::TradingEnded);
        }
        _ => {}
    }
    Ok(())
}

Oracle Security

Feed Validation

fn validate_oracle_feed(
    pyth_feed: &AccountInfo,
    expected_feed: &Pubkey,
) -> Result<()> {
    // Must match configured feed
    require!(pyth_feed.key == expected_feed, Error::OracleMismatch);

    // Must be owned by Pyth program
    require!(pyth_feed.owner == &PYTH_PROGRAM_ID, Error::InvalidOracleOwner);

    Ok(())
}

Price Validation

fn validate_oracle_price(
    price: &PythPrice,
    time_boundary: i64,
    clock: &Clock,
) -> Result<()> {
    // Price must be positive
    require!(price.price > 0, Error::InvalidPrice);

    // Publish time must be at or after boundary
    require!(price.publish_time >= time_boundary, Error::StaleOracle);

    // Not too far in future (clock skew)
    require!(
        price.publish_time <= clock.unix_timestamp + 60,
        Error::FutureOracle
    );

    Ok(())
}

Immutability

Once captured, snapshots cannot change:

fn capture_snapshot(market: &mut MarketAccount, price: i64) -> Result<()> {
    // Idempotency check
    if market.start_price != 0 {
        return Ok(()); // Already captured, no-op
    }

    market.start_price = price; // Immutable after this
    Ok(())
}

Solvency Security

No Naked Shorts

fn validate_sell_order(
    position: &UserPositionAccount,
    side: OrderSide,
    quantity: u64,
) -> Result<()> {
    let available = match side {
        SellYes => position.yes_shares - position.locked_yes_shares,
        SellNo => position.no_shares - position.locked_no_shares,
        _ => return Ok(()), // Not a sell
    };

    require!(quantity <= available, Error::InsufficientShares);
    Ok(())
}

Collateral Coverage

fn validate_buy_order(
    user_balance: u64,
    locked_collateral: u64,
    price_bps: u16,
    quantity: u64,
) -> Result<()> {
    let required = (price_bps as u64)
        .checked_mul(quantity)
        .ok_or(Error::Overflow)?
        .checked_div(10_000)
        .ok_or(Error::Overflow)?;

    let available = user_balance - locked_collateral;

    require!(required <= available, Error::InsufficientCollateral);
    Ok(())
}

Vault Solvency Invariant

vault.balance >= max(total_yes_shares, total_no_shares)

Reentrancy Protection

Solana Runtime Protection

  1. Account Locking: Accounts locked for transaction duration
  2. No Recursive CPI: Cannot re-borrow mutably
  3. No Token Callbacks: SPL Token has no hooks

Settlement Idempotency

fn settle_position(position: &mut UserPositionAccount) -> Result<()> {
    // Check BEFORE any state change
    if position.settled {
        return Ok(()); // Already settled, no-op
    }

    // Calculate and transfer payout...

    position.settled = true; // Atomic with transfer
    Ok(())
}

Protocol Invariants

IDInvariantDescription
INV-1SolvencyVault >= max(yes, no) shares
INV-2ConservationTrading doesn't create value
INV-3Non-negativeNo negative share balances
INV-4Immutable SnapshotsPrices never change
INV-5DeterministicSame inputs = same outputs
INV-6No Crossed Bookbest_bid < best_ask
INV-7Rent RecoveryAll accounts closeable

Security Checklist

Pre-Deployment

  • All invariants have runtime checks
  • All arithmetic uses checked operations
  • All accounts validate ownership and type
  • All PDAs verify derivation
  • All state transitions validate preconditions
  • All timing constraints enforced
  • Oracle validation complete
  • Solvency constraints prevent exploitation
  • Idempotency prevents double-processing

Audit Focus Areas

AreaPriorityComplexity
Oracle integrationCriticalHigh
Order matchingCriticalHigh
Collateral accountingCriticalMedium
State machineHighMedium
PDA derivationHighLow

Incident Response

Severity Levels

LevelDescriptionResponse
P0Funds at riskImmediate
P1Potential fund loss< 4 hours
P2Non-critical issue< 24 hours
P3Minor issue< 1 week

Response Procedures

  1. Assess scope of compromise
  2. Pause protocol if possible
  3. Implement emergency fix
  4. Coordinate disclosure

Next Steps