Skip to content

Commit 1df481f

Browse files
authored
solana: refactor token accounts validation with Anchor context (#756)
* chore: refactor token account validation * chore: clean up errors * refactor: unpack after ctx validation * refactor: default pubkey for custom acc validation
1 parent ebaca9c commit 1df481f

File tree

18 files changed

+365
-332
lines changed

18 files changed

+365
-332
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::associated_token::get_associated_token_address_with_program_id;
3+
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
4+
use solana_program::address_lookup_table;
5+
6+
use crate::seed;
7+
use crate::CommonCcipError;
8+
9+
#[derive(Accounts)]
10+
#[instruction(token_receiver: Pubkey, chain_selector: u64, router: Pubkey, fee_quoter: Pubkey)]
11+
pub struct TokenAccountsValidationContext<'info> {
12+
#[account(
13+
constraint = user_token_account.key() == get_associated_token_address_with_program_id(
14+
&token_receiver.key(),
15+
&mint.key(),
16+
&token_program.key()
17+
) @ CommonCcipError::InvalidInputsTokenAccounts,
18+
)]
19+
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
20+
21+
/// CHECK: Per chain token billing config PDA
22+
// billing: configured via CCIP fee quoter
23+
// chain config: configured via pool
24+
#[account(
25+
seeds = [
26+
seed::PER_CHAIN_PER_TOKEN_CONFIG,
27+
chain_selector.to_le_bytes().as_ref(),
28+
mint.key().as_ref(),
29+
],
30+
seeds::program = fee_quoter.key(),
31+
bump
32+
)]
33+
pub token_billing_config: UncheckedAccount<'info>,
34+
35+
/// CHECK: Pool chain config PDA
36+
#[account(
37+
seeds = [
38+
seed::TOKEN_POOL_CONFIG,
39+
chain_selector.to_le_bytes().as_ref(),
40+
mint.key().as_ref(),
41+
],
42+
seeds::program = pool_program.key(),
43+
bump
44+
)]
45+
pub pool_chain_config: UncheckedAccount<'info>,
46+
47+
/// CHECK: Lookup table
48+
#[account(owner = address_lookup_table::program::id() @ CommonCcipError::InvalidInputsLookupTableAccounts)]
49+
pub lookup_table: UncheckedAccount<'info>,
50+
51+
/// CHECK: Token admin registry PDA
52+
#[account(
53+
seeds = [seed::TOKEN_ADMIN_REGISTRY, mint.key().as_ref()],
54+
seeds::program = router.key(),
55+
bump,
56+
owner = router.key() @ CommonCcipError::InvalidInputsTokenAdminRegistryAccounts,
57+
)]
58+
pub token_admin_registry: UncheckedAccount<'info>,
59+
60+
/// CHECK: Pool program
61+
#[account(executable)]
62+
pub pool_program: UncheckedAccount<'info>,
63+
64+
/// CHECK: Pool config PDA
65+
#[account(
66+
seeds = [seed::CCIP_TOKENPOOL_CONFIG, mint.key().as_ref()],
67+
seeds::program = pool_program.key(),
68+
bump,
69+
owner = pool_program.key() @ CommonCcipError::InvalidInputsPoolAccounts
70+
)]
71+
pub pool_config: UncheckedAccount<'info>,
72+
73+
#[account(
74+
address = get_associated_token_address_with_program_id(
75+
&pool_signer.key(),
76+
&mint.key(),
77+
&token_program.key()
78+
) @ CommonCcipError::InvalidInputsTokenAccounts
79+
)]
80+
pub pool_token_account: InterfaceAccount<'info, TokenAccount>,
81+
82+
/// CHECK: Pool signer PDA
83+
#[account(
84+
seeds = [seed::CCIP_TOKENPOOL_SIGNER, mint.key().as_ref()],
85+
seeds::program = pool_program.key(),
86+
bump
87+
)]
88+
pub pool_signer: UncheckedAccount<'info>,
89+
90+
pub token_program: Interface<'info, TokenInterface>,
91+
92+
#[account(owner = token_program.key() @ CommonCcipError::InvalidInputsTokenAccounts)]
93+
pub mint: InterfaceAccount<'info, Mint>,
94+
95+
/// CHECK: Fee token config PDA
96+
#[account(
97+
seeds = [
98+
seed::FEE_BILLING_TOKEN_CONFIG,
99+
mint.key().as_ref()
100+
],
101+
seeds::program = fee_quoter.key(),
102+
bump
103+
)]
104+
pub fee_token_config: UncheckedAccount<'info>,
105+
}

chains/solana/contracts/programs/ccip-common/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! v1 of the common logic may not necessarily be used by v1 of each particular program.
33
use anchor_lang::prelude::*;
44

5+
pub mod context;
56
pub mod seed;
67
pub mod v1;
78

@@ -15,8 +16,6 @@ pub enum CommonCcipError {
1516
InvalidInputsPoolAccounts,
1617
#[msg("Invalid token accounts")]
1718
InvalidInputsTokenAccounts,
18-
#[msg("Invalid config account")]
19-
InvalidInputsConfigAccounts,
2019
#[msg("Invalid Token Admin Registry account")]
2120
InvalidInputsTokenAdminRegistryAccounts,
2221
#[msg("Invalid LookupTable account")]

chains/solana/contracts/programs/ccip-common/src/v1.rs

Lines changed: 51 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anchor_lang::prelude::*;
2-
use anchor_spl::associated_token::get_associated_token_address_with_program_id;
32
use solana_program::address_lookup_table::state::AddressLookupTable;
43

5-
use crate::{router_accounts::TokenAdminRegistry, seed, CommonCcipError};
4+
use crate::{
5+
context::TokenAccountsValidationContext, router_accounts::TokenAdminRegistry, CommonCcipError,
6+
};
67

78
pub struct TokenAccounts<'a> {
89
pub user_token_account: &'a AccountInfo<'a>,
@@ -25,125 +26,58 @@ pub fn validate_and_parse_token_accounts<'info>(
2526
fee_quoter: Pubkey,
2627
accounts: &'info [AccountInfo<'info>],
2728
) -> Result<TokenAccounts> {
29+
// The program_id here is provided solely to satisfy the interface of try_accounts.
30+
// Note: All program IDs for PDA derivation are explicitly defined in the account context
31+
// (TokenAccountsValidationContext) via seeds and program attributes.
32+
// Therefore, the value of program_id (set here to Pubkey::default()) is effectively unused.
33+
// Changes in environment-specific program addresses will not affect the PDA derivation.
34+
let program_id = Pubkey::default();
35+
36+
let mut input_accounts = accounts;
37+
let mut bumps = <TokenAccountsValidationContext as anchor_lang::Bumps>::Bumps::default();
38+
let mut reallocs = std::collections::BTreeSet::new();
39+
40+
// leveraging Anchor's account context validation
41+
// Instead of manually checking each account (ownership, PDA derivation, constraints),
42+
// we're using Anchor's `try_accounts` to perform these validations based on the
43+
// constraints defined in the `TokenAccountsValidationContext` account context struct
44+
TokenAccountsValidationContext::try_accounts(
45+
&program_id,
46+
&mut input_accounts,
47+
&[
48+
token_receiver.as_ref(),
49+
&chain_selector.to_le_bytes(),
50+
router.as_ref(),
51+
fee_quoter.as_ref(),
52+
]
53+
.concat(),
54+
&mut bumps,
55+
&mut reallocs,
56+
)?;
57+
58+
let mut accounts_iter = accounts.iter();
59+
2860
// accounts based on user or chain
29-
let (user_token_account, remaining_accounts) = accounts.split_first().unwrap();
30-
let (token_billing_config, remaining_accounts) = remaining_accounts.split_first().unwrap();
31-
let (pool_chain_config, remaining_accounts) = remaining_accounts.split_first().unwrap();
61+
let user_token_account = next_account_info(&mut accounts_iter)?;
62+
let token_billing_config = next_account_info(&mut accounts_iter)?;
63+
let pool_chain_config = next_account_info(&mut accounts_iter)?;
3264

3365
// constant accounts for any pool interaction
34-
let (lookup_table, remaining_accounts) = remaining_accounts.split_first().unwrap();
35-
let (token_admin_registry, remaining_accounts) = remaining_accounts.split_first().unwrap();
36-
let (pool_program, remaining_accounts) = remaining_accounts.split_first().unwrap();
37-
let (pool_config, remaining_accounts) = remaining_accounts.split_first().unwrap();
38-
let (pool_token_account, remaining_accounts) = remaining_accounts.split_first().unwrap();
39-
let (pool_signer, remaining_accounts) = remaining_accounts.split_first().unwrap();
40-
let (token_program, remaining_accounts) = remaining_accounts.split_first().unwrap();
41-
let (mint, remaining_accounts) = remaining_accounts.split_first().unwrap();
42-
let (fee_token_config, remaining_accounts) = remaining_accounts.split_first().unwrap();
43-
44-
// Account validations (using remaining_accounts does not facilitate built-in anchor checks)
66+
let lookup_table = next_account_info(&mut accounts_iter)?;
67+
let token_admin_registry = next_account_info(&mut accounts_iter)?;
68+
let pool_program = next_account_info(&mut accounts_iter)?;
69+
let pool_config = next_account_info(&mut accounts_iter)?;
70+
let pool_token_account = next_account_info(&mut accounts_iter)?;
71+
let pool_signer = next_account_info(&mut accounts_iter)?;
72+
let token_program = next_account_info(&mut accounts_iter)?;
73+
let mint = next_account_info(&mut accounts_iter)?;
74+
let fee_token_config = next_account_info(&mut accounts_iter)?;
75+
76+
// collect remaining accounts
77+
let remaining_accounts = accounts_iter.as_slice();
78+
79+
// Additional validations that can't be expressed in the account context
4580
{
46-
// Check Token Admin Registry
47-
let (expected_token_admin_registry, _) = Pubkey::find_program_address(
48-
&[seed::TOKEN_ADMIN_REGISTRY, mint.key().as_ref()],
49-
&router,
50-
);
51-
require_eq!(
52-
token_admin_registry.key(),
53-
expected_token_admin_registry,
54-
CommonCcipError::InvalidInputsTokenAdminRegistryAccounts
55-
);
56-
57-
// check pool program + pool config + pool signer
58-
let (expected_pool_config, _) = Pubkey::find_program_address(
59-
&[seed::CCIP_TOKENPOOL_CONFIG, mint.key().as_ref()],
60-
&pool_program.key(),
61-
);
62-
let (expected_pool_signer, _) = Pubkey::find_program_address(
63-
&[seed::CCIP_TOKENPOOL_SIGNER, mint.key().as_ref()],
64-
&pool_program.key(),
65-
);
66-
require_eq!(
67-
*pool_config.owner,
68-
pool_program.key(),
69-
CommonCcipError::InvalidInputsPoolAccounts
70-
);
71-
require_eq!(
72-
pool_config.key(),
73-
expected_pool_config,
74-
CommonCcipError::InvalidInputsPoolAccounts
75-
);
76-
require_eq!(
77-
pool_signer.key(),
78-
expected_pool_signer,
79-
CommonCcipError::InvalidInputsPoolAccounts
80-
);
81-
82-
let (expected_fee_token_config, _) = Pubkey::find_program_address(
83-
&[seed::FEE_BILLING_TOKEN_CONFIG, mint.key.as_ref()],
84-
&fee_quoter,
85-
);
86-
require_eq!(
87-
fee_token_config.key(),
88-
expected_fee_token_config,
89-
CommonCcipError::InvalidInputsConfigAccounts
90-
);
91-
92-
// check token accounts
93-
require_eq!(
94-
*mint.owner,
95-
token_program.key(),
96-
CommonCcipError::InvalidInputsTokenAccounts
97-
);
98-
require_eq!(
99-
user_token_account.key(),
100-
get_associated_token_address_with_program_id(
101-
&token_receiver,
102-
&mint.key(),
103-
&token_program.key()
104-
),
105-
CommonCcipError::InvalidInputsTokenAccounts
106-
);
107-
require_eq!(
108-
pool_token_account.key(),
109-
get_associated_token_address_with_program_id(
110-
&pool_signer.key(),
111-
&mint.key(),
112-
&token_program.key()
113-
),
114-
CommonCcipError::InvalidInputsTokenAccounts
115-
);
116-
117-
// check per token per chain configs
118-
// billing: configured via CCIP fee quoter
119-
// chain config: configured via pool
120-
let (expected_billing_config, _) = Pubkey::find_program_address(
121-
&[
122-
seed::PER_CHAIN_PER_TOKEN_CONFIG,
123-
chain_selector.to_le_bytes().as_ref(),
124-
mint.key().as_ref(),
125-
],
126-
&fee_quoter,
127-
);
128-
let (expected_pool_chain_config, _) = Pubkey::find_program_address(
129-
&[
130-
seed::TOKEN_POOL_CONFIG,
131-
chain_selector.to_le_bytes().as_ref(),
132-
mint.key().as_ref(),
133-
],
134-
&pool_program.key(),
135-
);
136-
require_eq!(
137-
token_billing_config.key(),
138-
expected_billing_config, // TODO: determine if this can be zero key for optional billing config?
139-
CommonCcipError::InvalidInputsConfigAccounts
140-
);
141-
require_eq!(
142-
pool_chain_config.key(),
143-
expected_pool_chain_config,
144-
CommonCcipError::InvalidInputsConfigAccounts
145-
);
146-
14781
// Check Lookup Table Address configured in TokenAdminRegistry
14882
let token_admin_registry_account: Account<TokenAdminRegistry> =
14983
Account::try_from(token_admin_registry)?;

chains/solana/contracts/programs/ccip-offramp/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,6 @@ pub enum CcipOfframpError {
585585
InvalidInputsFeeQuoterAccount,
586586
#[msg("Invalid offramp authorization account")]
587587
InvalidInputsAllowedOfframpAccount,
588-
#[msg("Invalid config account")]
589-
InvalidInputsConfigAccounts,
590588
#[msg("Invalid Token Admin Registry account")]
591589
InvalidInputsTokenAdminRegistryAccounts,
592590
#[msg("Invalid LookupTable account")]

chains/solana/contracts/programs/ccip-router/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,6 @@ pub enum CcipRouterError {
510510
InvalidInputsPoolAccounts,
511511
#[msg("Invalid token accounts")]
512512
InvalidInputsTokenAccounts,
513-
#[msg("Invalid config account")]
514-
InvalidInputsConfigAccounts,
515513
#[msg("Invalid Token Admin Registry account")]
516514
InvalidInputsTokenAdminRegistryAccounts,
517515
#[msg("Invalid LookupTable account")]

chains/solana/contracts/target/idl/ccip_common.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,16 @@
5959
},
6060
{
6161
"code": 10003,
62-
"name": "InvalidInputsConfigAccounts",
63-
"msg": "Invalid config account"
64-
},
65-
{
66-
"code": 10004,
6762
"name": "InvalidInputsTokenAdminRegistryAccounts",
6863
"msg": "Invalid Token Admin Registry account"
6964
},
7065
{
71-
"code": 10005,
66+
"code": 10004,
7267
"name": "InvalidInputsLookupTableAccounts",
7368
"msg": "Invalid LookupTable account"
7469
},
7570
{
76-
"code": 10006,
71+
"code": 10005,
7772
"name": "InvalidInputsLookupTableAccountWritable",
7873
"msg": "Invalid LookupTable account writable access"
7974
}

0 commit comments

Comments
 (0)