PDA Derivation#
Program Derived Addresses (PDAs) for all Seesaw accounts.
Overview#
PDAs are deterministic, program-owned addresses without private keys.
Config PDA#
Protocol-wide configuration singleton.
Seeds#
["seesaw", "config"]
Referrer Treasury PDA#
Shared escrow token accounts that accumulate the referral share of taker fees. Per-referrer balances are tracked in individual ReferrerEarningsAccount PDAs; claim_referrer_earnings transfers from the relevant shard.
Seeds#
["seesaw", "referrer_treasury", shard_index] // shard_index: single byte, 0..8
Derivation#
let (referrer_treasury_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"referrer_treasury", &[shard_index]],
&program_id,
);
TypeScript#
function deriveReferrerTreasuryPda(
shardIndex: number, // 0..8
programId: PublicKey
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('referrer_treasury'), Buffer.from([shardIndex])],
programId
);
}
Uniqueness#
Eight sharded escrow accounts per program deployment (shard_index ∈ [0, 8)). Sharding spreads write contention so concurrent fee-routing transactions don't all serialize through a single account. Created as SPL Token accounts holding the settlement mint.
Referral Account PDA#
Per-user referral attribution record. Written by set_referrer and locked for 365 days.
Seeds#
["seesaw", "referral", user_pubkey]
Derivation#
let (referral_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"referral", user_pubkey.as_ref()],
&program_id,
);
Uniqueness#
One per user. Re-writable only after the 365-day lifetime elapses.
Referrer Earnings Account PDA#
Per-referrer earnings ledger. Tracks accumulated (claimable) and total_claimed (lifetime). Credited on every attributed fill, drained by claim_referrer_earnings.
Seeds#
["seesaw", "referrer_earnings", referrer_pubkey]
Derivation#
let (referrer_earnings_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"referrer_earnings", referrer_pubkey.as_ref()],
&program_id,
);
Uniqueness#
One per referrer. Created by init_referrer_earnings_account.
Derivation#
let (config_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"config"],
&program_id,
);
TypeScript#
function deriveConfigPda(programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('config')],
programId
);
}
Uniqueness#
Exactly one per program deployment.
Market PDA#
Per-epoch market account.
Seeds#
["seesaw", "market", pyth_feed_id, duration_seconds.to_le_bytes(), market_id.to_le_bytes(), creator_pubkey]
Derivation#
let (market_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"market",
&pyth_feed_id,
&duration_seconds.to_le_bytes(),
&market_id.to_le_bytes(),
creator_pubkey.as_ref(),
],
&program_id,
);
TypeScript#
function deriveMarketPda(
pythFeedId: Uint8Array,
durationSeconds: bigint,
marketId: bigint,
creatorPubkey: PublicKey,
programId: PublicKey
): [PublicKey, number] {
const durationBuffer = Buffer.alloc(8);
durationBuffer.writeBigUInt64LE(durationSeconds);
const marketIdBuffer = Buffer.alloc(8);
marketIdBuffer.writeBigUInt64LE(marketId);
return PublicKey.findProgramAddressSync(
[
Buffer.from('seesaw'),
Buffer.from('market'),
pythFeedId,
durationBuffer,
marketIdBuffer,
creatorPubkey.toBuffer(),
],
programId
);
}
Market ID Calculation#
function getCurrentMarketId(durationSeconds: number = 900): bigint {
const now = Math.floor(Date.now() / 1000);
return BigInt(Math.floor(now / durationSeconds));
}
Uniqueness#
One market per creator per feed per duration per epoch. Multiple creators and durations can run concurrently for the same asset.
Orderbook PDA#
Order book for a specific market.
Seeds#
["seesaw", "orderbook", market_pubkey]
Derivation#
let (orderbook_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"orderbook",
market_pda.as_ref(),
],
&program_id,
);
TypeScript#
function deriveOrderbookPda(marketPda: PublicKey, programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('orderbook'), marketPda.toBuffer()],
programId
);
}
Uniqueness#
One orderbook per market.
Vault PDA#
Token account holding market collateral (USDT).
Seeds#
["seesaw", "vault", market_pubkey]
Derivation#
let (vault_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"vault",
market_pda.as_ref(),
],
&program_id,
);
TypeScript#
function deriveVaultPda(marketPda: PublicKey, programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('vault'), marketPda.toBuffer()],
programId
);
}
Notes#
- This is an SPL Token account
- Owned by the market PDA
- Holds settlement currency (USDT)
Position PDA#
User's position in a specific market.
Seeds#
["seesaw", "position", market_pubkey, user_pubkey]
Derivation#
let (position_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"position",
market_pda.as_ref(),
user_pubkey.as_ref(),
],
&program_id,
);
TypeScript#
function derivePositionPda(
marketPda: PublicKey,
userPubkey: PublicKey,
programId: PublicKey
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('position'), marketPda.toBuffer(), userPubkey.toBuffer()],
programId
);
}
Uniqueness#
One position per user per market.
Trader Ledger PDA#
Per-market internal balance ledger for free-funds instructions.
Seeds#
["seesaw", "trader_ledger", market_pubkey]
Derivation#
let (ledger_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"trader_ledger", market_pda.as_ref()],
&program_id,
);
TypeScript#
function deriveTraderLedgerPda(marketPda: PublicKey, programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('seesaw'), Buffer.from('trader_ledger'), marketPda.toBuffer()],
programId
);
}
Notes#
Must be pre-grown via EnsureTraderLedgerSpace before CreateMarket. See
Market Capacity for tier sizes.
YES Mint PDA#
SPL Token mint for a market's YES shares.
Seeds#
["seesaw", "yes_mint", market_pubkey]
Derivation#
let (yes_mint_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"yes_mint", market_pda.as_ref()],
&program_id,
);
NO Mint PDA#
SPL Token mint for a market's NO shares.
Seeds#
["seesaw", "no_mint", market_pubkey]
Derivation#
let (no_mint_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"no_mint", market_pda.as_ref()],
&program_id,
);
YES Escrow PDA#
SPL Token account that holds YES shares escrowed by open sell orders.
Seeds#
["seesaw", "escrow_yes", market_pubkey]
Derivation#
let (yes_escrow_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"escrow_yes", market_pda.as_ref()],
&program_id,
);
NO Escrow PDA#
SPL Token account that holds NO shares escrowed by open sell orders.
Seeds#
["seesaw", "escrow_no", market_pubkey]
Derivation#
let (no_escrow_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"escrow_no", market_pda.as_ref()],
&program_id,
);
Bump Seeds#
Storage#
All PDAs store their bump seed to avoid re-derivation on each instruction:
pub struct MarketAccount {
pub bump: u8,
pub orderbook_bump: u8,
pub vault_bump: u8,
// ...
}
Verification#
On each instruction, every PDA account passed in is re-derived from its seeds
and stored bump, then compared against the supplied account key. A mismatch
returns SeesawError::InvalidPDA (0x5003). Conceptually:
// Re-derive the address from seeds + stored bump and compare.
let expected = create_program_address(&[seeds, &[&[bump]]], program_id)?;
if account.key() != &expected {
return Err(SeesawError::InvalidPDA.into());
}
The program uses Pinocchio's account model (AccountView / raw key bytes), not
Anchor account contexts — there is no #[derive(Accounts)] validation layer, so
each PDA is verified explicitly inside the instruction processor.
Canonical Bumps#
Use find_program_address at creation time; it returns the canonical (highest valid) bump.
Derivation Hierarchy#
Program ID
├── Config PDA
│ └── seeds: ["seesaw", "config"]
│
├── Market PDA (per creator/feed/duration/epoch)
│ ├── seeds: ["seesaw", "market", pyth_feed_id, duration_seconds, market_id, creator]
│ │
│ ├── Orderbook PDA
│ │ └── seeds: ["seesaw", "orderbook", market_pda]
│ │
│ ├── Vault PDA
│ │ └── seeds: ["seesaw", "vault", market_pda]
│ │
│ ├── YES Mint PDA
│ │ └── seeds: ["seesaw", "yes_mint", market_pda]
│ │
│ ├── NO Mint PDA
│ │ └── seeds: ["seesaw", "no_mint", market_pda]
│ │
│ ├── YES Escrow PDA
│ │ └── seeds: ["seesaw", "escrow_yes", market_pda]
│ │
│ ├── NO Escrow PDA
│ │ └── seeds: ["seesaw", "escrow_no", market_pda]
│ │
│ ├── TraderLedger PDA
│ │ └── seeds: ["seesaw", "trader_ledger", market_pda]
│ │
│ └── Position PDA (per user)
│ └── seeds: ["seesaw", "position", market_pda, user_pda]
│
├── ReferralAccount PDA (per referee)
│ └── seeds: ["seesaw", "referral", user_pubkey]
│
├── ReferrerEarningsAccount PDA (per referrer)
│ └── seeds: ["seesaw", "referrer_earnings", referrer_pubkey]
│
└── ReferrerTreasury PDA (8 shards)
└── seeds: ["seesaw", "referrer_treasury", &[shard_index]] shard_index ∈ [0, 8)
Helper Functions#
Derive All Market PDAs#
function deriveMarketPdas(
pythFeedId: Uint8Array,
durationSeconds: bigint,
marketId: bigint,
creatorPubkey: PublicKey,
programId: PublicKey
): {
market: [PublicKey, number];
orderbook: [PublicKey, number];
vault: [PublicKey, number];
} {
const [marketPda, marketBump] = deriveMarketPda(
pythFeedId,
durationSeconds,
marketId,
creatorPubkey,
programId
);
const [orderbookPda, orderbookBump] = deriveOrderbookPda(marketPda, programId);
const [vaultPda, vaultBump] = deriveVaultPda(marketPda, programId);
return {
market: [marketPda, marketBump],
orderbook: [orderbookPda, orderbookBump],
vault: [vaultPda, vaultBump],
};
}
Derive All User PDAs#
function deriveUserPdas(
pythFeedId: Uint8Array,
durationSeconds: bigint,
marketId: bigint,
userPubkey: PublicKey,
creatorPubkey: PublicKey,
programId: PublicKey
): {
market: PublicKey;
position: [PublicKey, number];
} {
const [marketPda] = deriveMarketPda(
pythFeedId,
durationSeconds,
marketId,
creatorPubkey,
programId
);
const [positionPda, positionBump] = derivePositionPda(marketPda, userPubkey, programId);
return {
market: marketPda,
position: [positionPda, positionBump],
};
}
Collision Prevention#
PDAs are unique by construction:
- Namespace:
"seesaw"prefix prevents collisions with other programs - Type discriminant: Each account type has a distinct inner prefix
- Key components: Market ID, user pubkey, etc. scope each PDA to its owner
- Bump search:
find_program_addressreturns the canonical valid bump
["seesaw", "config"] - unique (singleton)
["seesaw", "market", FEED, DUR, ID, CREATOR] - unique per creator/feed/duration/epoch
["seesaw", "orderbook", MKT] - unique per market
["seesaw", "vault", MKT] - unique per market
["seesaw", "yes_mint", MKT] - unique per market
["seesaw", "no_mint", MKT] - unique per market
["seesaw", "escrow_yes", MKT] - unique per market
["seesaw", "escrow_no", MKT] - unique per market
["seesaw", "trader_ledger", MKT] - unique per market
["seesaw", "position", MKT, U] - unique per user/market
["seesaw", "referral", USER] - unique per user (referee)
["seesaw", "referrer_earnings", REFERRER] - unique per referrer
["seesaw", "referrer_treasury", SHARD] - 8 shards (SHARD ∈ [0, 8))
Security Considerations#
For guidance on how to verify PDA derivations and why bumps are stored at creation time, see Security Model → PDA validation.