Skip to content

fix: enforce canonical bump in ata verification#2249

Merged
ananas-block merged 6 commits intomainfrom
fix/audit-issue-15-canonical-bump
Feb 11, 2026
Merged

fix: enforce canonical bump in ata verification#2249
ananas-block merged 6 commits intomainfrom
fix/audit-issue-15-canonical-bump

Conversation

@ananas-block
Copy link
Contributor

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

Summary

  • Replace derive_address with find_program_address in verify_pda to ensure only the canonical bump seed is accepted, preventing non-canonical bump attacks.

Summary by CodeRabbit

  • Refactor

    • Removed explicit ATA bump from public APIs and instruction payloads; ATA bump is now derived on-chain.
    • Consolidated and simplified associated-token creation paths and SDK helpers to use the derived ATA only.
  • Documentation

    • Updated examples and docs to remove bump parameters and reflect on-chain bump derivation.
  • Tests

    • Adjusted tests to expect single ATA result (no bump) and added validations rejecting explicit bump usage.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

The PR removes explicit ATA bump fields and parameters across programs, SDKs, macros, and tests, shifting bump derivation to on-chain canonical computation. Function signatures, instruction data, macro attributes, and CPI/SDK builders were updated to stop passing or returning bump values and to return ATA addresses only.

Changes

Cohort / File(s) Summary
Core instruction/data
program-libs/token-interface/src/instructions/create_associated_token_account.rs
Removed public bump field from CreateAssociatedTokenAccountInstructionData.
PDA verification utilities
programs/compressed-token/program/src/shared/create_pda_account.rs, programs/compressed-token/program/src/shared/validate_ata_derivation.rs
Changed verify_pda/validate_ata_derivation to derive canonical bump on-chain and return it (Result<u8, ...>) instead of accepting a bump parameter.
Compressed-token flows
programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs, programs/compressed-token/program/src/ctoken/create_ata.rs
Consume canonical bump returned from verification; stop using stored/passsed bump values for PDA seeds and ATA derivation.
SDK derive helpers
sdk-libs/account-pinocchio/src/lib.rs, sdk-libs/account/src/lib.rs, sdk-libs/token-pinocchio/src/instruction/create_ata.rs, sdk-libs/token-sdk/src/instruction/create_associated_token_account.rs, sdk-libs/token-sdk/src/instruction/create_ata.rs
Changed derive_associated_token_account to return only ATA address (removed bump from return type) and adjusted callers. Consolidated and simplified ATA creation paths in token-sdk (removed many bump-based variants).
CPI & instruction builders
sdk-libs/sdk-types/src/interface/cpi/create_token_accounts.rs, sdk-libs/sdk-types/src/interface/program/decompression/create_token_account.rs, sdk-libs/sdk-types/src/interface/program/decompression/token.rs
Removed bump fields from CPI structs and build functions; dropped bump parameter from instruction builders and instruction data construction.
Macro parsing & codegen
sdk-libs/macros/src/light_pdas/accounts/light_account.rs, sdk-libs/macros/src/light_pdas/accounts/token.rs, sdk-libs/macros/src/light_pdas/light_account_keywords.rs
Removed associated_token::bump support: AtaField struct no longer contains bump, parser rejects associated_token::bump, and associated_token keys list updated.
Macro docs & examples
sdk-libs/macros/docs/accounts/architecture.md, sdk-libs/macros/docs/accounts/associated_token.md, sdk-libs/account/README.md
Documentation and examples updated to remove bump param from light_account attributes and CreateAtaParams; state that bump is derived on-chain.
Client & test call sites
sdk-libs/client/src/interface/account_interface.rs, sdk-libs/client/src/interface/light_program_interface.rs, sdk-libs/client/src/interface/load_accounts.rs, sdk-libs/program-test/src/program_test/rpc.rs, sdk-libs/token-sdk/tests/*
Updated callers to handle new derive return type (single ATA address) and removed/destructured bump usage in tests and client code.
Decompression & token logic
sdk-libs/token-sdk/src/instruction/decompress.rs, sdk-libs/sdk-types/src/interface/program/decompression/create_token_account.rs
Adjusted decompression/ATA creation paths to stop passing bump; where needed, switched to functions that return ATA+bump pair from other helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

✨ Bumps once carried in tuples light and small,
Now quietly derived where on-chain proofs call.
Signatures slimmed, macros taught a new rule,
One address returns — the codebase feels cool.
A tidy drift from bump to canonical hall.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: enforcing canonical bump in ATA verification by refactoring PDA verification to derive and use on-chain canonical bumps instead of client-provided values.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/audit-issue-15-canonical-bump

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ananas-block
Copy link
Contributor Author

This PR absorbs #2250 (issue #3) and #2255 (issue #4) — all three modify the same block in create_ata.rs.

Combined changes:

  1. Remove bump from instruction data (issue Swen dev v4 cli pt 1/x #15): Refactor verify_pda to drop bump param, return canonical bump from find_program_address. Update validate_ata_derivation, CreateAssociatedTokenAccountInstructionData, decompress_mint.rs, and all SDK callers.
  2. Move validate_ata_derivation before IDEMPOTENT check (issue Zhao/feat merkle tree anchor #3): Both code paths validate derivation.
  3. Add mint/owner checks in idempotent early return (issue feat: impl invoke cpi #4): Use pubkey_eq with Token::from_account_info_checked before return Ok(()).
  4. Use returned canonical bump for ATA seeds instead of inputs.bump.
  5. Update test in program-tests/compressed-token-test/tests/light_token/create_ata.rs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
sdk-libs/token-sdk/src/instruction/decompress.rs (1)

126-135: 🧹 Nitpick | 🔵 Trivial

bump field still written to TLV but no longer consumed on-chain — consider follow-up cleanup.

The CompressedOnlyExtensionInstructionData.bump is populated here (line 133) but the on-chain decompression path in token.rs now discards it (_ata_bump). The on-chain verify_pda re-derives the canonical bump independently. This is safe, but the field is now vestigial in the verification flow. A follow-up PR could remove it from the struct to avoid future confusion — though that would require a migration for existing compressed accounts that already have this field serialized.

sdk-libs/macros/docs/accounts/associated_token.md (1)

251-254: ⚠️ Potential issue | 🟡 Minor

Documentation is inconsistent with the new API — derive_associated_token_account no longer returns a tuple.

The function now returns a single value (the ATA address), not (address, bump). This example will confuse developers who try to copy-paste it.

📝 Proposed fix
-let (expected_ata, _) = derive_associated_token_account(&owner, &mint);
-assert_eq!(ata_pubkey, expected_ata);
+let expected_ata = derive_associated_token_account(&owner, &mint);
+assert_eq!(ata_pubkey, expected_ata);

As per coding guidelines: "Verify that all function signatures, struct definitions, and behavior described in the documentation accurately match the actual implementation."

sdk-libs/macros/src/light_pdas/accounts/light_account.rs (1)

1003-1008: ⚠️ Potential issue | 🟡 Minor

Stale doc comment — associated_token::bump mapping no longer exists.

Line 1008 still documents associated_token::bump -> bump as a valid mapping, but the implementation now explicitly rejects it with an error. This will confuse anyone reading the function docs.

📝 Proposed fix
 /// Build an AtaField from parsed namespaced key-value pairs.
 ///
 /// Mapping from new syntax to internal fields:
 /// - `associated_token::authority` -> `owner` (renamed to match Anchor's ATA naming)
 /// - `associated_token::mint` -> `mint`
-/// - `associated_token::bump` -> `bump`
+///
+/// Note: `associated_token::bump` is rejected — the bump is derived on-chain.
sdk-libs/token-sdk/src/instruction/create_associated_token_account.rs (4)

53-70: 🧹 Nitpick | 🔵 Trivial

The 5-element tuple for compressible config is hard to read at a glance.

Line 106 defines compressible_config: Option<(u8, Option<u32>, Pubkey, Pubkey, TokenDataVersion)> — a 5-element tuple with positional semantics. Inside the function, elements are destructured with _ placeholders (lines 117, 155), which makes it easy to mix up positions.

Consider using a small struct (or reusing CreateCompressibleAssociatedTokenAccountInputs fields) instead of a bare tuple to improve clarity in this internal API.


134-163: ⚠️ Potential issue | 🔴 Critical

Add missing owner and mint accounts to the accounts list.

The accounts list (lines 147–163) must include owner and mint as the first two accounts to match the on-chain handler's expected account order:

0. owner
1. mint
2. fee_payer
3. associated_token_account
4. system_program
5. [optional] compressible_config
6. [optional] rent_sponsor

Currently, the list starts with payer, causing the on-chain AccountIterator to read the wrong account at position 0 when it expects owner. The on-chain handler needs both owner and mint to validate the ATA derivation via validate_ata_derivation(associated_token_account, owner_bytes, mint_bytes).


12-14: 🧹 Nitpick | 🔵 Trivial

Document why 1-byte discriminators (100, 102) are used instead of Anchor's standard trait.

This Anchor program intentionally uses 1-byte discriminators for SPL Token compatibility (as noted in programs/compressed-token/program/src/lib.rs). However, the SDK code here treats these hardcoded values as unexplained constants. Add a comment explaining that these match the program's InstructionType enum values for SPL compatibility:

/// Discriminators for create ATA instructions
/// Note: 1-byte discriminators used for SPL Token instruction compatibility (see InstructionType enum in programs/compressed-token)
const CREATE_ATA_DISCRIMINATOR: u8 = 100;
const CREATE_ATA_IDEMPOTENT_DISCRIMINATOR: u8 = 102;

186-260: ⚠️ Potential issue | 🔴 Critical

The instruction's accounts list is missing and misaligned with the on-chain handler's expected account order.

The on-chain handler (programs/compressed-token/program/src/ctoken/create_ata.rs, lines 37–47) explicitly expects:

  1. owner (non-mut, non-signer)
  2. mint (non-mut, non-signer)
  3. fee_payer (signer, mut)
  4. associated_token_account (mut)
  5. system_program
    6–7. Optional compressible accounts

However, create_ata_instruction_unified builds accounts as:

  1. payer (fee_payer) ← should be owner
  2. ata_pubkey ← should be mint
  3. system_program ← should be at index 4
    4–5. Optional compressible accounts (wrong position)

The function receives owner and mint as parameters but discards them instead of adding them to the accounts list. This mismatch explains the asymmetry in the CPI wrappers: the non-idempotent variant attempts to pass authority as an extra account hoping the handler reads from remaining_accounts, while the idempotent variant doesn't pass it at all. Neither approach works because the handler explicitly reads from fixed account indices, not remaining_accounts.

The owner and mint must be added to the accounts list in the correct positions before payer and ata_pubkey.

🤖 Fix all issues with AI agents
In `@programs/compressed-token/program/src/shared/create_pda_account.rs`:
- Around line 104-105: In create_pda_account.rs, update the PDA mismatch error
so that when the derived PDA doesn't equal the provided account key (the check
that currently returns ProgramError::InvalidAccountData), it returns
ProgramError::InvalidSeeds instead; locate the equality check around account_key
vs expected_pubkey in the function that derives/validates the PDA (e.g.,
create_pda_account or the PDA validation block) and replace the returned error
variant accordingly so callers receive InvalidSeeds for seed-derivation
failures.

In `@sdk-libs/macros/src/light_pdas/accounts/light_account.rs`:
- Around line 1624-1635: The test currently verifies that
parse_light_account_attr rejects the shorthand associated_token::bump but
doesn't assert on the error text; update the
test_parse_associated_token_bump_rejected case to capture the Err from
parse_light_account_attr and assert that the error message contains a clear
identifier like "associated_token::bump" (or the exact error string emitted by
parse_light_account_attr) so regressions in the error text will be caught;
reference parse_light_account_attr and the associated_token::bump attribute in
your assertion.

Comment on lines 104 to 105
if account_key != &expected_pubkey {
return Err(ProgramError::InvalidAccountData);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual code at the specified lines
cat -n programs/compressed-token/program/src/shared/create_pda_account.rs | sed -n '100,110p'

Repository: Lightprotocol/light-protocol

Length of output: 404


🏁 Script executed:

# Search for verify_pda function definition and usage
rg -n 'fn verify_pda' --type=rust -A 10 programs/compressed-token/

Repository: Lightprotocol/light-protocol

Length of output: 1207


🏁 Script executed:

# Check all callers of verify_pda to understand error handling
rg -n 'verify_pda' --type=rust -B 2 -A 5 programs/compressed-token/

Repository: Lightprotocol/light-protocol

Length of output: 5592


🏁 Script executed:

# Look at the error definitions to understand available error types
rg -n 'enum.*Error|InvalidSeeds|InvalidAccountData' --type=rust programs/compressed-token/program/src/error.rs

Repository: Lightprotocol/light-protocol

Length of output: 158


🏁 Script executed:

# Find error definitions in the compressed-token program
fd -t f 'error' programs/compressed-token/program/src/ | head -20

Repository: Lightprotocol/light-protocol

Length of output: 141


🏁 Script executed:

# Search for error enum definitions
rg -n 'enum.*Error|InvalidSeeds|InvalidAccountData' --type=rust programs/compressed-token/program/src/

Repository: Lightprotocol/light-protocol

Length of output: 3915


🏁 Script executed:

# Check the structure of the program directory
ls -la programs/compressed-token/program/src/

Repository: Lightprotocol/light-protocol

Length of output: 613


Use InvalidSeeds for PDA mismatch validation.

The function derives a PDA and validates it matches the provided account key. When they don't match, return ProgramError::InvalidSeeds instead of InvalidAccountData—it more precisely describes the failure (the seeds don't derive to the expected account). The codebase already uses InvalidSeeds in similar validation contexts (e.g., withdraw_funding_pool.rs), and neither caller pattern-matches on the error type, so this change is safe.

Suggested diff
     if account_key != &expected_pubkey {
-        return Err(ProgramError::InvalidAccountData);
+        return Err(ProgramError::InvalidSeeds);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if account_key != &expected_pubkey {
return Err(ProgramError::InvalidAccountData);
if account_key != &expected_pubkey {
return Err(ProgramError::InvalidSeeds);
}
🤖 Prompt for AI Agents
In `@programs/compressed-token/program/src/shared/create_pda_account.rs` around
lines 104 - 105, In create_pda_account.rs, update the PDA mismatch error so that
when the derived PDA doesn't equal the provided account key (the check that
currently returns ProgramError::InvalidAccountData), it returns
ProgramError::InvalidSeeds instead; locate the equality check around account_key
vs expected_pubkey in the function that derives/validates the PDA (e.g.,
create_pda_account or the PDA validation block) and replace the returned error
variant accordingly so callers receive InvalidSeeds for seed-derivation
failures.

@ananas-block ananas-block changed the title fix: enforce canonical bump in PDA verification fix: enforce canonical bump in ata verification Feb 10, 2026
Audit issue #15 (HIGH): verify_pda used derive_address which accepts
any bump seed, allowing non-canonical bumps for ATAs. Switch to
find_program_address to derive the canonical bump and reject any
non-canonical bump with InvalidSeeds error.
…on-chain

Remove client-provided bump from CreateAssociatedTokenAccountInstructionData
and all SDK/test callers. The on-chain program now derives the canonical bump
via find_program_address, preventing non-canonical bump attacks (audit #15).

- Remove bump field from instruction data structs
- Update verify_pda to derive canonical bump and return it
- Update validate_ata_derivation and decompress_mint callers
- Remove _with_bump SDK variants and ATA2 dead code
- Remove associated_token::bump from macro attribute support
- Update derive_associated_token_account to return Pubkey only
- Update all 100+ call sites across SDK, tests, and TypeScript
With canonical bumps, the program derives the bump internally so
providing a wrong bump is no longer possible. Replace with a test
that passes a wrong ATA address to verify PDA validation.
@ananas-block ananas-block force-pushed the fix/audit-issue-15-canonical-bump branch from 947d024 to 3c24b30 Compare February 11, 2026 19:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sdk-libs/macros/docs/accounts/associated_token.md (1)

251-254: ⚠️ Potential issue | 🟡 Minor

Stale derivation example — won't compile with the new API.

derive_associated_token_account now returns a Pubkey directly (not a tuple). This code example still destructures it as let (expected_ata, _) = ..., which would be a compile error.

📝 Proposed fix
-let (expected_ata, _) = derive_associated_token_account(&owner, &mint);
+let expected_ata = derive_associated_token_account(&owner, &mint);
 assert_eq!(ata_pubkey, expected_ata);
sdk-libs/macros/src/light_pdas/accounts/light_account.rs (1)

1003-1008: ⚠️ Potential issue | 🟡 Minor

Stale doc comment — associated_token::bump mapping still listed.

The function doc on Line 1008 still documents associated_token::bump -> bump as a valid mapping, but the body now explicitly rejects it. Keeping this line will confuse future readers into thinking bump is supported.

🧹 Proposed fix
 /// Build an AtaField from parsed namespaced key-value pairs.
 ///
 /// Mapping from new syntax to internal fields:
 /// - `associated_token::authority` -> `owner` (renamed to match Anchor's ATA naming)
 /// - `associated_token::mint` -> `mint`
-/// - `associated_token::bump` -> `bump`

@ananas-block ananas-block merged commit 291933b into main Feb 11, 2026
34 checks passed
@ananas-block ananas-block deleted the fix/audit-issue-15-canonical-bump branch February 11, 2026 21:03
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