PDA — Accounts Owned by Programs

Every corporation in America has a corporate seal. It is not a person. It has no fingerprints, no driver's license, no ability to physically walk into a bank and sign a check. But it owns property. It holds bank accounts. It signs contracts. The corporate seal gives a legal entity — something that exists only on paper — the ability to control assets and authorize actions. No human needs to hand over their personal signature. The corporation acts on its own behalf, through its seal, under rules defined in its charter.

On Solana, a Program Derived Address is the corporate seal for an on-chain program.

A PDA is an account address that belongs to a program. No private key exists for it. No wallet holds its seed phrase. No human can sign a transaction as the PDA. Only the program that derived the address can authorize actions on its behalf. The program is the corporation. The PDA is the seal. And just like a corporate seal, the PDA's authority comes not from a person's identity but from a deterministic set of rules embedded in the system itself.

The Problem PDAs Solve

Regular Solana accounts are controlled by private keys. Someone generates a keypair — a private key and a corresponding public key — and the public key becomes the account address. Whoever holds the private key can sign transactions that modify the account. This is the fundamental security model: possession of the private key equals control of the account.

But programs need to own things too.

A DEX pool needs to hold tokens. Not a person's tokens — the pool's tokens. When a user swaps Token A for Token B, the pool needs to receive Token A into its own reserves and send Token B from its own reserves. If a human's private key controlled the pool's token accounts, that human could drain the pool at any time. The entire point of a decentralized exchange is that no single person controls the funds. The program controls them. The smart contract's logic — immutable, transparent, verifiable — is the only authority.

So the program needs an address. An address it controls. An address no one else can sign for. An address that is deterministic — derivable from known inputs, reproducible by anyone, yet controllable only by the program.

That address is a PDA.

Think of it like a safety deposit box at a bank. A regular bank account has an account holder with a signature card. The holder walks in, shows ID, signs the slip, and accesses the account. That is a regular Solana account — the private key is the signature card, and the holder is the signer.

A PDA is a safety deposit box that belongs to the bank itself. No individual customer's signature opens it. The bank's internal procedures — its programmed rules — determine when the box opens and what goes in or out. A customer can deposit into it (following the program's rules). The program can release funds from it (following its own logic). But no single person has the key. The rules are the key.

Seeds: The Address Formula

A PDA is derived from seeds. Seeds are a sequence of byte arrays that, combined with a program's address, produce a unique account address through a deterministic hash function.

Consider the U.S. Postal Service's P.O. Box system. Every P.O. Box has an address determined by its location. P.O. Box 1742 at the downtown branch of the Denver post office is a unique address. It is not random. It is not assigned by lottery. It is derived from the combination of city, branch, and box number. Anyone who knows those three pieces of information can address mail to that box. But only the box holder — the entity that rented it — can open it and retrieve the contents.

PDA seeds work the same way. A program defines a set of seeds — structured, meaningful byte arrays — and those seeds, combined with the program's own address, deterministically produce an account address. The same seeds always produce the same address. Different seeds produce different addresses.

Here is what a typical PDA derivation looks like for a DEX pool:

seeds = ["pool", mint_a_pubkey, mint_b_pubkey]
program_id = DEX_PROGRAM_ADDRESS

PDA = find_program_address(seeds, program_id)

The string "pool" is a namespace tag — it distinguishes pool addresses from other types of PDAs the same program might derive. mint_a_pubkey and mint_b_pubkey are the 32-byte addresses of the two token mints in the pool. Together, these three seeds plus the program's own address produce a single, unique, deterministic 32-byte address.

This is crucial. Given the same program and the same seeds, anyone can compute the same PDA. A client application can derive the address locally, without any network call, without any on-chain lookup. The address is a mathematical fact — a function of its inputs. If you know what pool you are looking for (which tokens it holds, which program created it), you can compute its address directly. No directory lookup. No registry query. No scanning the entire chain.

The seed structure varies by program and by account type. A Whirlpool pool might use seeds like ["whirlpool", config_pubkey, mint_a, mint_b, tick_spacing]. A Meteora DLMM pool might use ["lb_pair", mint_x, mint_y, bin_step]. An associated token account uses [wallet_pubkey, token_program_id, mint]. Each program defines its own seed conventions, and those conventions are part of the program's interface.

The seeds are not arbitrary. They encode the account's identity. A pool PDA's seeds contain the token mints because the pool is defined by which tokens it pairs. A tick array PDA's seeds contain the pool address and a tick index because the tick array is defined by which pool it belongs to and which price range it covers. The seeds are the account's DNA — they describe what the account is, and from that description, the address follows.

The Bump Seed: Staying Off the Curve

There is a mathematical subtlety in PDA derivation that matters more than it first appears.

Solana uses Ed25519 elliptic curve cryptography for its regular keypairs. Every regular public key is a point on the Ed25519 curve. By definition, every point on the curve has a corresponding private key. If a PDA happened to land on the curve, someone could theoretically compute its private key and sign transactions for it — completely defeating the purpose of a program-owned account.

PDAs must not be on the Ed25519 curve.

The find_program_address function handles this. It takes the seeds and program ID, hashes them together using SHA-256, and checks whether the resulting address falls on the Ed25519 curve. If it does — if the hash happens to produce a valid curve point — the function adds an extra byte called the bump seed and tries again. It starts with bump value 255 and decrements: 255, 254, 253, and so on, until it finds a combination that produces an address off the curve.

Think of it like the Social Security Number system. SSNs follow a specific structure — area number, group number, serial number — and certain combinations are reserved or invalid. The number 000-00-0000 is never issued. Numbers starting with 9 were historically reserved. The system guarantees that every issued SSN falls within the valid space, avoiding collisions with reserved ranges. The bump seed serves an analogous purpose: it is an extra digit appended to the input to guarantee the result falls in the valid PDA space — off the Ed25519 curve.

The bump is typically a single byte, so there are 256 possible values (0 through 255). In practice, the first bump value (255) works the vast majority of the time. The probability that a random hash lands on the Ed25519 curve is approximately 1 in 2, so statistically, you almost never need to try more than one or two values. But the mechanism exists because "almost never" is not "never," and cryptographic systems cannot tolerate "almost."

The bump is part of the PDA's identity. The canonical bump — the first value that produces a valid off-curve address — is the one programs use. When you derive a PDA, you get back both the address and the bump. The bump is often stored in the account's data or passed as an argument to instructions, because verifying a PDA (checking that a given address matches the expected seeds and program) requires the bump to recompute the derivation.

// Deriving a PDA in Rust
let (pda, bump) = Pubkey::find_program_address(
    &[b"pool", mint_a.as_ref(), mint_b.as_ref()],
    &program_id,
);

The function returns both the address and the bump. The pda is the account address — the corporate seal's identity. The bump is the extra byte that guaranteed the address is off the curve — the small adjustment that makes the whole system work.

PDAs in DEX Architecture

Every DEX on Solana relies heavily on PDAs. They are not an optional feature or an advanced technique. They are the foundation of how pools hold tokens, how authority is managed, and how state is organized.

Pool Authority. When a DEX pool holds Token A and Token B, those tokens sit in token accounts. Those token accounts need an owner — an address authorized to transfer tokens out of them. That owner is a PDA. It is the pool's authority, derived from seeds that identify the specific pool. When a user swaps, the program uses the pool authority PDA to sign a token transfer from the pool's reserves to the user's account. No human authorizes this. The program executes the transfer through the PDA's authority, and the Solana runtime verifies that the program ID matches the PDA's derivation.

This is the corporate seal in action. The corporation (program) authorizes a transaction (token transfer) using its seal (PDA). The counterparty (Solana runtime) verifies that the seal is authentic — that the PDA was genuinely derived from the claimed seeds and program. If everything checks out, the transaction executes. If not, it fails.

Tick Arrays. In concentrated liquidity DEXes like Orca Whirlpool, the price range is divided into ticks, and liquidity information is stored in tick array accounts. Each tick array covers a specific range of ticks for a specific pool. The tick array's address is a PDA derived from seeds like ["tick_array", pool_address, start_tick_index]. Given a pool and a price range, you can compute exactly which tick array account holds the relevant liquidity data. No scanning. No guessing. Direct derivation.

Bin Arrays. Meteora DLMM uses a similar concept with bin arrays. Each bin array covers a range of price bins for a specific pair. The address is a PDA derived from the pair address and a bin array index. Knowing the current price and the bin width, you compute which bin array you need, derive its PDA, and fetch the data directly.

Associated Token Accounts. The Associated Token Account (ATA) program uses PDAs to create a deterministic token account address for any wallet-mint combination. Given a wallet address and a token mint, there is exactly one ATA address. It is a PDA derived from [wallet, token_program, mint]. This convention means that anyone can compute where a specific wallet's USDC account lives, without querying the chain. It is the P.O. Box system applied to token accounts — the address is a function of identity (wallet) and purpose (which token).

Config and State Accounts. Many programs store global configuration in PDA accounts derived from simple seeds like ["config"] or ["global_state"]. These are singleton accounts — only one exists per program — and their PDA derivation guarantees uniqueness. There is exactly one address that results from hashing ["config"] with the program ID. If you try to create a second one, the derivation produces the same address, and the account already exists.

The Seed Mistake: Right Formula, Wrong Inputs

Here is where PDAs become dangerous in practice.

The deterministic nature of PDAs is a strength — same seeds, same address, every time. But it is also unforgiving. Wrong seeds produce a wrong address with the same confidence and the same determinism. The system does not tell you "close, but not quite." It gives you a completely different address that looks just as valid as the right one.

Consider the pool PDA derivation: seeds = ["pool", mint_a, mint_b]. What happens if you swap the order of the mints? ["pool", mint_b, mint_a] produces a different PDA. Not a similar one. Not a nearby one. A completely, cryptographically different address. The SHA-256 hash is entirely different. The resulting PDA has no relationship to the correct one.

You construct a swap transaction. You pass the wrong pool PDA — the one with swapped mint order. The transaction arrives at the program. The program checks: does this account exist? Does it contain valid pool data? It does not. The account at the wrong PDA is either empty or belongs to something else entirely. The transaction fails. The error message says something like "Account not initialized" or "Invalid account data" — which tells you nothing about the actual problem, which is that you computed the wrong address.

This is like mailing a package to P.O. Box 1742 at the downtown branch when it should go to P.O. Box 1724. You transposed two digits. The post office does not know you made a mistake. It delivers to Box 1742, which either belongs to someone else or is vacant. Your package either goes to the wrong recipient or bounces back as undeliverable. The system worked perfectly. Your input was wrong.

Seed mistakes are among the most frustrating bugs in Solana development because the error messages rarely point to the root cause. The transaction fails because an account does not contain the expected data, or because an account is not owned by the expected program, or because an account does not exist at all. None of these errors say "you computed the PDA with the wrong seeds." You diagnose seed mistakes by going back to the derivation and checking every input: the seed strings (byte-for-byte — "pool" and "Pool" produce different addresses), the seed order, the program ID, and the bump.

Common seed mistakes include:

Wrong order. Many programs sort token mints before using them as seeds — the lexicographically smaller mint always comes first. If the program sorts but the client does not (or vice versa), the derived addresses differ.

Wrong prefix. The namespace string matters. "pool" and "pool_state" and "lp_pool" are different seeds. Each DEX uses its own conventions. Raydium's seed prefix is not Orca's seed prefix.

Wrong program ID. The PDA derivation includes the program ID. If you derive against the wrong program — say, an old version of the program that was since upgraded to a new address — the PDA is different. The account exists under the correct program's derivation, not the one you used.

Missing or extra seeds. Some programs include additional seeds like a tick spacing value, a fee tier, or a version number. Omitting one or adding an extra one changes the result entirely.

Encoding mismatch. A seed might be a u16 encoded as two little-endian bytes. If you encode it as a big-endian u16, or as a u32, or as a decimal string, the seed bytes differ, and the PDA differs.

Every one of these mistakes produces a valid-looking PDA. The output is a 32-byte address that passes all format checks. It just does not match the account you actually need.

Compute Cost: find_program_address Is Not Free

The find_program_address function is computationally expensive by blockchain standards. It performs a SHA-256 hash, checks whether the result is on the Ed25519 curve, and potentially repeats with different bump values. On Solana, where every instruction has a compute unit budget, this cost matters.

A single find_program_address call costs roughly 1,500 compute units. That sounds small against the default 200,000 CU budget for a transaction. But DEX swap instructions are not simple. A typical swap might need to derive multiple PDAs: the pool authority, the tick array or bin array for the current price, the protocol fee account, possibly an oracle account. Four PDA derivations cost 6,000 CU. Eight cost 12,000 CU. In a complex multi-hop swap or a cross-program invocation chain, compute adds up fast.

The solution is to derive PDAs off-chain and pass them as accounts. Instead of having the program compute find_program_address during execution, the client pre-computes the PDA and includes it in the transaction's accounts list. The program then verifies the PDA using create_program_address — which takes the seeds, bump, and program ID, hashes them once, and checks that the result matches the passed address. This verification costs roughly 1,500 CU as well, but it is a single hash with a known bump, not a loop through multiple bump values.

Even better, many Anchor programs store the bump in the account data itself. When the account is created, the canonical bump is recorded as a field. Subsequent instructions read the bump from the account, use it with create_program_address for a single-hash verification, and avoid the loop entirely.

For an arbitrage bot, this distinction matters. Every compute unit spent on PDA derivation is a compute unit not available for swap logic, profit checking, or error handling. When you are building transactions that chain multiple swaps across multiple DEXes — each with its own PDA conventions — the cumulative compute cost of on-chain PDA derivation can push you over the budget. Pre-computing PDAs off-chain and caching them is not an optimization. It is a necessity.

The pattern looks like this:

Off-chain (client / bot):
  pool_pda, bump = find_program_address(["pool", mint_a, mint_b], program_id)
  tick_array_pda, _ = find_program_address(["tick_array", pool_pda, start_tick], program_id)
  authority_pda, _ = find_program_address(["authority", pool_pda], program_id)

On-chain (program):
  verify pool_pda matches create_program_address(["pool", mint_a, mint_b, bump], program_id)
  // proceed with swap logic

The heavy lifting — the loop through bump values — happens off-chain, where compute is free. The on-chain program only verifies, which is a single hash operation.

PDA Signing: Invoke Signed

When a program needs to act on behalf of a PDA — transferring tokens from a PDA-owned token account, for example — it uses a mechanism called invoke_signed. This is the moment where the corporate seal actually stamps the document.

Regular cross-program invocations use invoke. The calling program passes accounts and instruction data to a target program, and the target program executes. But if any of the accounts need to be signers, a regular invoke cannot produce a signature for a PDA — there is no private key to sign with.

invoke_signed solves this. The calling program passes the PDA's seeds and bump along with the invocation. The Solana runtime takes those seeds and bump, hashes them with the calling program's ID, and checks whether the result matches one of the accounts marked as a signer. If it does, the runtime treats the PDA as having signed the transaction. The program proved it knows the seeds that derive the PDA, and since only the owning program can use those seeds (the program ID is part of the derivation), this proof is equivalent to a signature.

invoke_signed(
    &transfer_instruction,
    &[pool_token_account, user_token_account, authority_pda],
    &[&[b"authority", pool_key.as_ref(), &[bump]]],
)?;

The &[&[b"authority", pool_key.as_ref(), &[bump]]] parameter is the signer seeds. It tells the runtime: "The PDA derived from these seeds and this program's ID should be treated as a signer for this invocation." The runtime verifies the derivation, confirms the match, and allows the transfer.

This is how DEX pools move tokens. The pool program invokes the Token Program's transfer instruction, passing the pool authority PDA as the signer. The Token Program checks that the signer matches the token account's owner. The runtime checks that the PDA derivation is correct. Everything verifies, and the tokens move. No private key. No human. Just deterministic math and programmatic authority.

Why This Matters

PDAs are not a Solana curiosity or an academic concept in cryptography. They are the mechanism that makes trustless, programmatic asset management possible.

Every token swap on a DEX goes through PDA-controlled accounts. Every liquidity provision deposits into PDA-owned reserves. Every yield farm distributes rewards from PDA-managed vaults. The entire DeFi stack on Solana — hundreds of billions in cumulative transaction volume — runs on the simple idea that a program can own an address, and that address can be computed from known inputs.

For building a swap transaction, the PDA is operational plumbing. You need to derive the right pool authority to include in the accounts list. You need to derive the right tick array or bin array for the current price range. You need the right ATA addresses for the source and destination tokens. Every one of these is a PDA, and every one must be derived correctly — right seeds, right order, right encoding, right program ID.

Get the seeds right, and the PDA is right. Get the PDA right, and the transaction has the correct accounts. Get the accounts right, and the swap executes.

Get any seed wrong — one byte, one ordering, one encoding — and the PDA is wrong. And the transaction fails with an error that does not tell you why.

The corporate seal stamps only for the corporation. The P.O. Box delivers only to the correct address. The safety deposit box opens only with the bank's procedures. The PDA operates only through its program, only at its deterministic address, and only when the seeds are exactly right. That precision is the price of trustlessness. And it is a price worth paying.

Disclaimer

This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.