General
Program Security

Program Security

PDA Sharing

PDA sharing attacks exploit programs that use the same Program Derived Address (PDA) across multiple users or domains, allowing attackers to access funds, data, or permissions that don't belong to them. While using a global PDA might seem elegant for program-wide operations, it creates dangerous cross-contamination where one user's actions can affect another user's assets.

The vulnerability stems from insufficient seed specificity when deriving PDAs. When multiple accounts share the same PDA authority, the program loses the ability to distinguish between legitimate and illegitimate access attempts. An attacker can create their own accounts that reference the same shared PDA, then use that PDA's signing authority to manipulate assets belonging to other users.

This is particularly devastating in DeFi protocols where PDAs control token vaults, user balances, or withdrawal permissions. A shared PDA essentially creates a master key that unlocks multiple users' assets, turning individual user operations into potential attacks against the entire protocol.

Anchor

Consider this vulnerable withdrawal system that uses a mint-based PDA for signing:

#[program]
pub mod insecure_withdraw{
    use super::*;
    //..
 
    pub fn withdraw(ctx: Context<WithdrawTokens>) -> Result<()> {
        let amount = ctx.accounts.vault.amount;
        
        let seeds = &[
            ctx.accounts.pool.withdraw_destination.as_ref(),
            &[ctx.accounts.pool.bump],
        ];
 
        transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.withdraw_destination.to_account_info(),
                    authority: ctx.accounts.pool.to_account_info(),
                },
            ),
            &amount,
            seeds,
        )?;
 
        Ok(())
    }
        
    //..
}
 
#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
    #[account(has_one = vault, has_one = withdraw_destination)]
    pool: Account<'info, TokenPool>,
    vault: Account<'info, TokenAccount>,
    withdraw_destination: Account<'info, TokenAccount>,
    /// CHECK: This is the PDA that signs for the transfer
    authority: UncheckedAccount<'info>,
    token_program: Program<'info, Token>,
}
 
#[account]
#[derive(InitSpace)]
pub struct TokenPool {
    pub vault: Pubkey,
    pub mint: Pubkey,
    pub withdraw_destination: Pubkey,
    pub bump: u8,
}

This code has a critical flaw: the PDA is derived using only the mint address. This means that all pools for the same token type share the same signing authority, creating a dangerous attack vector.

An attacker can exploit this by:

  • Creating their own TokenPool for the same mint
  • Setting their own address as the withdraw_destination
  • Using the shared PDA authority to withdraw tokens from any vault that uses the same mint
  • Draining other users' funds to their own destination

The attack succeeds because the PDA authority doesn't distinguish between different pool instances, it only cares about the mint type, not the specific user or pool that should have access to those funds.

The first improvement is making PDAs specific to individual users or destinations and use Anchor's seeds and bump constraints to validate PDA derivation:

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
    #[account(
        seeds = [withdraw_destination.key().as_ref()],
        bump = pool.bump,                             
        has_one = vault,                              
        has_one = withdraw_destination,              
    )]
    pool: Account<'info, TokenPool>,
    #[account(mut)]
    vault: Account<'info, TokenAccount>,
    #[account(mut)]
    withdraw_destination: Account<'info, TokenAccount>,
    token_program: Program<'info, Token>,
}
Contents
View Source
Blueshift © 2025Commit: e508535