Skip to content

feat: pinocchio swap example#18

Merged
SwenSchaeferjohann merged 2 commits into
mainfrom
jorrit/feat-pinocchio-example
Feb 9, 2026
Merged

feat: pinocchio swap example#18
SwenSchaeferjohann merged 2 commits into
mainfrom
jorrit/feat-pinocchio-example

Conversation

@ananas-block
Copy link
Copy Markdown
Contributor

@ananas-block ananas-block commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

Comment on lines +55 to +104
pub fn parse(accounts: &'a [AccountInfo], params: &SwapParams) -> Result<Self, ProgramError> {
if accounts.len() < Self::FIXED_LEN {
return Err(ProgramError::NotEnoughAccountKeys);
}

let user = &accounts[0];
let pool = &accounts[1];
let pool_authority = &accounts[2];
let mint_a = &accounts[3];
let mint_b = &accounts[4];
let vault_a = &accounts[5];
let vault_b = &accounts[6];
let user_token_a = &accounts[7];
let user_token_b = &accounts[8];
let light_token_program = &accounts[9];
let light_token_cpi_authority = &accounts[10];
let system_program = &accounts[11];

// Validate user is signer
if !user.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}

// Validate global pool authority PDA
{
let seeds: &[&[u8]] = &[POOL_AUTHORITY_SEED];
let (expected_pda, expected_bump) =
pinocchio::pubkey::find_program_address(seeds, &crate::ID);
if pool_authority.key() != &expected_pda {
return Err(SwapError::InvalidAuthoritySeeds.into());
}
if expected_bump != params.authority_bump {
return Err(SwapError::InvalidAuthoritySeeds.into());
}
}

Ok(Self {
user,
pool,
pool_authority,
mint_a,
mint_b,
vault_a,
vault_b,
user_token_a,
user_token_b,
light_token_program,
light_token_cpi_authority,
system_program,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing pool account ownership and PDA validation in swap instruction enables fee manipulation

The swap instruction does not validate that the pool account is owned by the program or is a valid PDA derived from the mints.

Click to expand

Vulnerability Details

In swap/accounts.rs:55-104, the SwapAccounts::parse function validates the pool_authority PDA (lines 78-89) but does NOT validate the pool account:

  • No check that pool.owner() == &crate::ID
  • No check that pool is a valid PDA derived from [POOL_SEED, mint_a, mint_b]

In swap/processor.rs:62-72, the processor validates that the provided vaults and mints match what's stored in the pool state, but an attacker controls what's in the pool state if they provide a fake pool account.

Attack Scenario

  1. Attacker creates a fake pool account (not owned by the program) with:
    • token_a_vault and token_b_vault pointing to legitimate vaults from an existing pool
    • token_a_mint and token_b_mint matching the legitimate pool
    • fee_bps = 0 (instead of the intended 30 bps)
  2. Attacker calls swap with the fake pool account and legitimate vaults
  3. Swap proceeds with 0% fee instead of the intended 0.3% fee

Since pool_authority is a global PDA ([POOL_AUTHORITY_SEED]) that owns all vaults across all pools, the transfer from vault will succeed because pool_authority legitimately owns the vault.

Impact

Fee bypass allowing users to swap without paying the intended pool fees, causing loss of protocol revenue.

Recommendation: Add validation in SwapAccounts::parse to verify the pool account is owned by the program and is a valid PDA derived from the mints:

// Validate pool is owned by this program
if pool.owner() != &crate::ID {
    return Err(SwapError::InvalidPoolState.into());
}

// Validate pool PDA
let mint_a_key = mint_a.key();
let mint_b_key = mint_b.key();
let seeds: &[&[u8]] = &[POOL_SEED, mint_a_key.as_ref(), mint_b_key.as_ref()];
let (expected_pool_pda, _) = pinocchio::pubkey::find_program_address(seeds, &crate::ID);
if pool.key() != &expected_pool_pda {
    return Err(SwapError::InvalidPoolSeeds.into());
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread pinocchio/swap/Cargo.toml
Comment on lines +30 to +33
solana-pubkey = "2.2"
solana-instruction = "2.2"
solana-msg = "2.2"
solana-program-error = "2.2"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be necessary

@SwenSchaeferjohann SwenSchaeferjohann merged commit 7a5ba0e into main Feb 9, 2026
18 checks passed
@ananas-block ananas-block deleted the jorrit/feat-pinocchio-example branch February 10, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants