Security Model
Security architecture and protections in the Seesaw protocol.
Threat Model
Adversary Capabilities
| Capability | Description |
|---|---|
| Arbitrary Transactions | Can submit any valid Solana transaction |
| Multiple Accounts | Can create unlimited accounts (Sybil) |
| Flash Loans | Can access large capital temporarily |
| MEV Extraction | Can frontrun/sandwich transactions |
| Market Manipulation | Can trade to move prices |
Adversary Limitations
| Limitation | Assumption |
|---|---|
| Forge Signatures | Ed25519 is secure |
| Break Pyth | Oracle data is authentic |
| Control Solana | Network operates honestly |
| Infinite Capital | Capital is finite per tx |
| Time Travel | Clock 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
- Account Locking: Accounts locked for transaction duration
- No Recursive CPI: Cannot re-borrow mutably
- 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
| ID | Invariant | Description |
|---|---|---|
| INV-1 | Solvency | Vault >= max(yes, no) shares |
| INV-2 | Conservation | Trading doesn't create value |
| INV-3 | Non-negative | No negative share balances |
| INV-4 | Immutable Snapshots | Prices never change |
| INV-5 | Deterministic | Same inputs = same outputs |
| INV-6 | No Crossed Book | best_bid < best_ask |
| INV-7 | Rent Recovery | All 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
| Area | Priority | Complexity |
|---|---|---|
| Oracle integration | Critical | High |
| Order matching | Critical | High |
| Collateral accounting | Critical | Medium |
| State machine | High | Medium |
| PDA derivation | High | Low |
Incident Response
Severity Levels
| Level | Description | Response |
|---|---|---|
| P0 | Funds at risk | Immediate |
| P1 | Potential fund loss | < 4 hours |
| P2 | Non-critical issue | < 24 hours |
| P3 | Minor issue | < 1 week |
Response Procedures
- Assess scope of compromise
- Pause protocol if possible
- Implement emergency fix
- Coordinate disclosure
Next Steps
- See Threat Model for detailed analysis
- Review Invariants for formal specifications