PDA Derivation
Program Derived Addresses (PDAs) for all Seesaw accounts.
Overview
PDAs provide deterministic, program-owned addresses without private keys.
Config PDA
Protocol-wide configuration singleton.
Seeds
["seesaw", "config"]
Derivation
let (config_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"config"],
&program_id,
);
TypeScript
function findConfigPda(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 findMarketPda(
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 coexist 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 findOrderbookPda(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.
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 findVaultPda(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 (USDC)
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 findPositionPda(
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.
Bump Seeds
Storage
All PDAs store their bump seed for efficient re-derivation:
pub struct MarketAccount {
pub bump: u8,
pub orderbook_bump: u8,
pub vault_bump: u8,
// ...
}
Verification
On each instruction, verify PDA derivation:
fn verify_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(())
}
Canonical Bumps
Always use find_program_address during creation, which returns the canonical (highest valid) bump.
Derivation Hierarchy
Program ID
├── Config PDA
│ └── seeds: ["seesaw", "config"]
│
└── Market PDA (per epoch)
├── seeds: ["seesaw", "market", market_id]
│
├── Orderbook PDA
│ └── seeds: ["seesaw", "orderbook", market_pda]
│
├── Vault PDA
│ └── seeds: ["seesaw", "vault", market_pda]
│
└── Position PDA (per user)
└── seeds: ["seesaw", "position", market_pda, user_pda]
Helper Functions
Derive All Market PDAs
function deriveMarketPdas(
marketId: bigint,
programId: PublicKey
): {
market: [PublicKey, number];
orderbook: [PublicKey, number];
vault: [PublicKey, number];
} {
const [marketPda, marketBump] = findMarketPda(marketId, programId);
const [orderbookPda, orderbookBump] = findOrderbookPda(marketPda, programId);
const [vaultPda, vaultBump] = findVaultPda(marketPda, programId);
return {
market: [marketPda, marketBump],
orderbook: [orderbookPda, orderbookBump],
vault: [vaultPda, vaultBump],
};
}
Derive All User PDAs
function deriveUserPdas(
marketId: bigint,
userPubkey: PublicKey,
programId: PublicKey
): {
market: PublicKey;
position: [PublicKey, number];
} {
const [marketPda] = findMarketPda(marketId, programId);
const [positionPda, positionBump] = findPositionPda(marketPda, userPubkey, programId);
return {
market: marketPda,
position: [positionPda, positionBump],
};
}
Collision Prevention
PDAs are guaranteed unique due to:
- Prefix uniqueness:
"seesaw"namespace - Type uniqueness: Different prefixes per account type
- Key uniqueness: Market ID, user pubkey
- Bump search:
find_program_addressfinds valid bump
["seesaw", "config"] - unique (singleton)
["seesaw", "market", ID] - unique per epoch
["seesaw", "orderbook", MKT] - unique per market
["seesaw", "vault", MKT] - unique per market
["seesaw", "position", MKT, U] - unique per user/market
Security Considerations
Always Verify Derivation
// WRONG: Trust caller-provided address
let market = &ctx.accounts.market;
// RIGHT: Verify derivation
let expected = Pubkey::create_program_address(
&[
b"seesaw",
b"market",
&market_id.to_le_bytes(),
&[market.bump],
],
&ctx.program_id,
)?;
require!(market.key() == expected, Error::InvalidPDA);
Store Bumps
Store bumps to avoid recomputing:
// During creation
market.bump = bump;
// During use
let seeds = &[
b"seesaw",
b"market",
&market_id.to_le_bytes(),
&[market.bump],
];