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:

rust
#[program]
pub mod insecure_withdraw{
    use super::*;
    //..
 
    pub fn withdraw(ctx: Context<WithdrawTokens>) -> Result<()> {
 
        //..
        // other conditions/actions...
        //..
 
        let amount = ctx.accounts.vault.amount;
        
        let seeds = &[
            ctx.accounts.pool.mint.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(
        seeds = [b"pool", pool.mint.as_ref()],
        bump = pool.bump,                                        
    )]
    pool: Account<'info, TokenPool>,
    vault: Account<'info, TokenAccount>,
    withdraw_destination: Account<'info, TokenAccount>,
    //..
    // other accounts..
    //..
    token_program: Program<'info, Token>,
}
 
#[account]
#[derive(InitSpace)]
pub struct TokenPool {
    pub mint: Pubkey,
    pub bump: u8,
}

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

An attacker can exploit this by:

  • Creating their own vault for the same mint
  • Calling the instruction with 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 that should have access to those funds.

A possible solution is making PDAs specific to individual users and destinations and use Anchor's seeds and bump constraints to validate PDA derivation:

rust
#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
    #[account(
        has_one = vault,
        has_one = withdraw_destination,
        seeds = [b"pool", vault.key().as_ref(), withdraw_destination.key().as_ref()],
        bump = pool.bump,                                        
    )]
    pool: Account<'info, TokenPool>, // Authority for the vault
    #[account(mut)]
    vault: Account<'info, TokenAccount>,
    #[account(mut)]
    withdraw_destination: Account<'info, TokenAccount>,
    //..
    // other accounts..
    //..
    token_program: Program<'info, Token>,
}
 
#[account]
#[derive(InitSpace)]
pub struct TokenPool{
    pub vault:Pubkey,
    pub withdraw_destination:Pubkey,
    pub bump:u8
}

The same change is made for the instruction handler, one possible situation where this can hold is a leverage trading program that allows a users's trade to be liquidated when they have lost a certain amount e.g they set a stop loss themselves, the code would then check the condition of reaching that amount and then allow anyone to stop the trade and withdraw the remaining funds to the destination, the withdrawal account.

One single PDA controlling all the funds for a particular mint would create a situation where if conditions were met for users simultaneously e.g many user's being close to stop loss/liquidation, then any single user could withdraw those funds for all those users, possibly in a one or more transactions containing multiple instructions doing this for different users.

Contents
View Source
Blueshift © 2025Commit: bc7f093
Blueshift | Program Security | PDA Sharing