diff --git a/chains/solana/contracts/programs/ccip-router/src/context.rs b/chains/solana/contracts/programs/ccip-router/src/context.rs index a26b548d..d8f5224b 100644 --- a/chains/solana/contracts/programs/ccip-router/src/context.rs +++ b/chains/solana/contracts/programs/ccip-router/src/context.rs @@ -124,6 +124,17 @@ pub struct GetFee<'info> { constraint = valid_version(dest_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] pub dest_chain_state: Account<'info, DestChain>, + #[account( + seeds = [FEE_BILLING_TOKEN_CONFIG, + if message.fee_token == Pubkey::default() { + native_mint::ID.as_ref() // pre-2022 WSOL + } else { + message.fee_token.as_ref() + } + ], + bump, + )] + pub billing_token_config: Account<'info, BillingTokenConfigWrapper>, } #[derive(Accounts)] diff --git a/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs b/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs index 7fc80323..0caf1443 100644 --- a/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs +++ b/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs @@ -5,9 +5,9 @@ use solana_program::{program::invoke_signed, system_instruction}; use crate::{ utils::{Exponential, Usd18Decimals}, - valid_version, BillingTokenConfig, BillingTokenConfigWrapper, CcipRouterError, DestChain, - PerChainPerTokenConfig, Solana2AnyMessage, SolanaTokenAmount, UnpackedDoubleU224, - CCIP_LOCK_OR_BURN_V1_RET_BYTES, FEE_BILLING_SIGNER_SEEDS, MAX_TOKEN_AND_CHAIN_CONFIG_V, + BillingTokenConfig, CcipRouterError, DestChain, PerChainPerTokenConfig, Solana2AnyMessage, + SolanaTokenAmount, UnpackedDoubleU224, CCIP_LOCK_OR_BURN_V1_RET_BYTES, + FEE_BILLING_SIGNER_SEEDS, }; // TODO change args and implement @@ -15,18 +15,15 @@ pub fn fee_for_msg( _dest_chain_selector: u64, message: &Solana2AnyMessage, dest_chain: &DestChain, - token_configs: &[BillingTokenConfig], - token_configs_for_dest_chain: &[PerChainPerTokenConfig], + fee_token_config: &BillingTokenConfig, + additional_token_configs: &[BillingTokenConfig], + additional_token_configs_for_dest_chain: &[PerChainPerTokenConfig], ) -> Result { let token = if message.fee_token == Pubkey::default() { native_mint::ID // Wrapped SOL } else { message.fee_token }; - let fee_token_config = token_configs - .iter() - .find(|c| c.mint == token) - .ok_or(CcipRouterError::UnsupportedToken)?; message.validate(dest_chain, fee_token_config)?; let fee_token_price = get_validated_token_price(&fee_token_config)?; @@ -35,8 +32,8 @@ pub fn fee_for_msg( let network_fee = network_fee( message, dest_chain, - token_configs, - token_configs_for_dest_chain, + additional_token_configs, + additional_token_configs_for_dest_chain, )?; // TODO un-hardcode @@ -54,66 +51,6 @@ pub fn fee_for_msg( Ok(SolanaTokenAmount { amount, token }) } -/// Parses and validates the account slice in the order expected by `get_fees`: -/// -/// * First, the billing token config accounts for each token involved, including the -/// fee token, sequentially. -/// * Then, the per chain / per token config of those tokens, sequentially in the same -/// order, for the destination chain. -pub fn get_accounts_for_fee_retrieval<'info>( - remaining_accounts: &'info [AccountInfo<'info>], - message: &Solana2AnyMessage, -) -> Result<(Vec, Vec)> { - let fee_token = if message.fee_token == Pubkey::default() { - native_mint::ID // Wrapped SOL - } else { - message.fee_token - }; - - let number_of_tokens_involved = if message.token_amounts.iter().any(|t| t.token == fee_token) { - message.token_amounts.len() - } else { - message.token_amounts.len() + 1 - }; - - require_eq!( - remaining_accounts.len(), - 2 * number_of_tokens_involved, - CcipRouterError::InvalidInputsTokenAccounts - ); - - let (token_billing_config_accounts, per_chain_per_token_config_accounts) = - remaining_accounts.split_at(number_of_tokens_involved); - - let token_billing_config_accounts = token_billing_config_accounts - .iter() - .map(|a| { - let account = Account::::try_from(a)?; - require!( - valid_version(account.version, 1), - CcipRouterError::InvalidInputs - ); - Ok(account.into_inner().config) - }) - .collect::>>()?; - let per_chain_per_token_config_accounts = per_chain_per_token_config_accounts - .iter() - .map(|a| { - let account = Account::::try_from(a)?; - require!( - valid_version(account.version, MAX_TOKEN_AND_CHAIN_CONFIG_V), - CcipRouterError::InvalidInputs - ); - Ok(account.into_inner()) - }) - .collect::>>()?; - - Ok(( - token_billing_config_accounts, - per_chain_per_token_config_accounts, - )) -} - struct NetworkFee { premium: Usd18Decimals, _token_transfer_gas: U256, @@ -324,7 +261,8 @@ mod tests { 0, &sample_message(), &sample_dest_chain(), - &[sample_billing_config()], + &sample_billing_config(), + &[], &[] ) .unwrap(), @@ -345,7 +283,8 @@ mod tests { 0, &sample_message(), &chain, - &[sample_billing_config()], + &sample_billing_config(), + &[], &[] ) .unwrap(), @@ -370,7 +309,8 @@ mod tests { 0, &message, &sample_dest_chain(), - &[sample_billing_config()], + &sample_billing_config(), + &[], &[] ) .unwrap_err(), @@ -385,7 +325,7 @@ mod tests { // Will have no effect because we're not using the network fee chain.config.network_fee_usdcents *= 0; - let (_, mut per_chain_per_token) = sample_additional_token(); + let (token_config, mut per_chain_per_token) = sample_additional_token(); // Not enabled == no overrides per_chain_per_token.billing.is_enabled = false; @@ -405,7 +345,8 @@ mod tests { 0, &message, &chain, - &[sample_billing_config()], + &sample_billing_config(), + &[token_config], &[per_chain_per_token] ) .unwrap(), @@ -439,7 +380,8 @@ mod tests { 0, &message, &chain, - &[sample_billing_config(), another_token_config], + &sample_billing_config(), + &[another_token_config], &[another_per_chain_per_token_config] ) .unwrap(), @@ -473,7 +415,8 @@ mod tests { 0, &message, &chain, - &[sample_billing_config(), another_token_config], + &sample_billing_config(), + &[another_token_config], &[another_per_chain_per_token_config] ) .unwrap(), @@ -504,7 +447,15 @@ mod tests { let per_chains: Vec<_> = per_chains.into_iter().collect(); set_syscall_stubs(Box::new(TestStubs)); assert_eq!( - fee_for_msg(0, &message, &sample_dest_chain(), &tokens, &per_chains).unwrap(), + fee_for_msg( + 0, + &message, + &sample_dest_chain(), + &sample_billing_config(), + &tokens, + &per_chains + ) + .unwrap(), SolanaTokenAmount { token: native_mint::ID, // Increases proportionally to the number of tokens @@ -552,7 +503,8 @@ mod tests { 0, &sample_message(), &sample_dest_chain(), - &[billing_config], + &billing_config, + &[], &[] ) .unwrap_err(), @@ -569,7 +521,8 @@ mod tests { 0, &sample_message(), &sample_dest_chain(), - &[billing_config], + &billing_config, + &[], &[] ) .unwrap_err(), @@ -590,7 +543,8 @@ mod tests { 0, &sample_message(), &chain, - &[sample_billing_config()], + &sample_billing_config(), + &[], &[] ) .unwrap_err(), diff --git a/chains/solana/contracts/programs/ccip-router/src/lib.rs b/chains/solana/contracts/programs/ccip-router/src/lib.rs index ce4074fb..5af97329 100644 --- a/chains/solana/contracts/programs/ccip-router/src/lib.rs +++ b/chains/solana/contracts/programs/ccip-router/src/lib.rs @@ -662,10 +662,10 @@ pub mod ccip_router { /// In addition to the fixed amount of accounts defined in the `GetFee` context, /// the following accounts must be provided: /// - /// * First, the billing token config accounts for each token involved, including the + /// * First, the billing token config accounts for each token sent, starting with the /// fee token, sequentially. - /// * Then, the per chain / per token config of those tokens, sequentially in the same - /// order, for the destination chain. + /// * Then, the per chain / per token config of every token sent with the message, sequentially + /// in the same order, for the destination chain. /// /// # Returns /// @@ -675,12 +675,45 @@ pub mod ccip_router { dest_chain_selector: u64, message: Solana2AnyMessage, ) -> Result { + let remaining_accounts = &ctx.remaining_accounts; + let message = &message; + require_eq!( + remaining_accounts.len(), + 2 * message.token_amounts.len(), + CcipRouterError::InvalidInputsTokenAccounts + ); + let (token_billing_config_accounts, per_chain_per_token_config_accounts) = - get_accounts_for_fee_retrieval(&ctx.remaining_accounts, &message)?; + remaining_accounts.split_at(message.token_amounts.len()); + + let token_billing_config_accounts = token_billing_config_accounts + .iter() + .map(|a| { + let account = Account::::try_from(a)?; + require!( + valid_version(account.version, 1), + CcipRouterError::InvalidInputs + ); + Ok(account.into_inner().config) + }) + .collect::>>()?; + let per_chain_per_token_config_accounts = per_chain_per_token_config_accounts + .iter() + .map(|a| { + let account = Account::::try_from(a)?; + require!( + valid_version(account.version, MAX_TOKEN_AND_CHAIN_CONFIG_V), + CcipRouterError::InvalidInputs + ); + Ok(account.into_inner()) + }) + .collect::>>()?; + Ok(fee_for_msg( dest_chain_selector, &message, &ctx.accounts.dest_chain_state, + &ctx.accounts.billing_token_config.config, &token_billing_config_accounts, &per_chain_per_token_config_accounts, )? @@ -711,14 +744,60 @@ pub mod ccip_router { let dest_chain = &mut ctx.accounts.dest_chain_state; - // TODO this is incorrect and breaks when transfering a token. Derive these accounts - // from the lookup table instead. - let (token_billing_config_accounts, per_chain_per_token_config_accounts) = - get_accounts_for_fee_retrieval(&ctx.remaining_accounts, &message)?; + let mut accounts_per_sent_token: Vec = vec![]; + + for (i, token_amount) in message.token_amounts.iter().enumerate() { + require!( + token_amount.amount != 0, + CcipRouterError::InvalidInputsTokenAmount + ); + + // Calculate the indexes for the additional accounts of the current token index `i` + let (start, end) = calculate_token_pool_account_indices( + i, + &message.token_indexes, + ctx.remaining_accounts.len(), + )?; + + let current_token_accounts = validate_and_parse_token_accounts( + ctx.accounts.authority.key(), + dest_chain_selector, + ctx.program_id.key(), + &ctx.remaining_accounts[start..end], + )?; + + accounts_per_sent_token.push(current_token_accounts); + } + + let token_billing_config_accounts = accounts_per_sent_token + .iter() + .map(|a| { + let account = Account::::try_from(a.fee_token_config)?; + require!( + valid_version(account.version, 1), + CcipRouterError::InvalidInputs + ); + Ok(account.into_inner().config) + }) + .collect::>>()?; + + let per_chain_per_token_config_accounts = accounts_per_sent_token + .iter() + .map(|a| { + let account = Account::::try_from(a.token_billing_config)?; + require!( + valid_version(account.version, MAX_TOKEN_AND_CHAIN_CONFIG_V), + CcipRouterError::InvalidInputs + ); + Ok(account.into_inner()) + }) + .collect::>>()?; + let fee = fee_for_msg( dest_chain_selector, &message, dest_chain, + &ctx.accounts.fee_token_config.config, &token_billing_config_accounts, &per_chain_per_token_config_accounts, )?; @@ -790,31 +869,13 @@ pub mod ccip_router { }; let seeds = &[EXTERNAL_TOKEN_POOL_SEED, &[ctx.bumps.token_pools_signer]]; - for (i, token_amount) in message.token_amounts.iter().enumerate() { - require!( - token_amount.amount != 0, - CcipRouterError::InvalidInputsTokenAmount - ); - - // Calculate the indexes for the additional accounts of the current token index `i` - let (start, end) = calculate_token_pool_account_indices( - i, - &message.token_indexes, - ctx.remaining_accounts.len(), - )?; - - let current_token_accounts = validate_and_parse_token_accounts( - ctx.accounts.authority.key(), - dest_chain_selector, - ctx.program_id.key(), - &ctx.remaining_accounts[start..end], - )?; - + for (i, (current_token_accounts, token_amount)) in accounts_per_sent_token + .iter() + .zip(message.token_amounts.iter()) + .enumerate() + { let router_token_pool_signer = &ctx.accounts.token_pools_signer; - let _token_billing_config = ¤t_token_accounts._token_billing_config; - // TODO: Implement charging depending on the token transfer - // CPI: transfer token amount from user to token pool transfer_token( token_amount.amount, diff --git a/chains/solana/contracts/programs/ccip-router/src/pools.rs b/chains/solana/contracts/programs/ccip-router/src/pools.rs index d41ae376..fcc36d00 100644 --- a/chains/solana/contracts/programs/ccip-router/src/pools.rs +++ b/chains/solana/contracts/programs/ccip-router/src/pools.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::{ associated_token::get_associated_token_address_with_program_id, + token::spl_token::native_mint, token_2022::spl_token_2022::{self, instruction::transfer_checked, state::Mint}, token_interface::TokenAccount, }; @@ -12,13 +13,13 @@ use solana_program::{program::get_return_data, program_pack::Pack}; use crate::{ CcipRouterError, ExternalExecutionConfig, TokenAdminRegistry, CCIP_TOKENPOOL_CONFIG, - CCIP_TOKENPOOL_SIGNER, TOKEN_ADMIN_REGISTRY_SEED, TOKEN_POOL_BILLING_SEED, - TOKEN_POOL_CONFIG_SEED, + CCIP_TOKENPOOL_SIGNER, FEE_BILLING_TOKEN_CONFIG, TOKEN_ADMIN_REGISTRY_SEED, + TOKEN_POOL_BILLING_SEED, TOKEN_POOL_CONFIG_SEED, }; pub const CCIP_POOL_V1_RET_BYTES: usize = 8; pub const CCIP_LOCK_OR_BURN_V1_RET_BYTES: u32 = 32; -const MIN_TOKEN_POOL_ACCOUNTS: usize = 11; // see TokenAccounts struct for all required accounts +const MIN_TOKEN_POOL_ACCOUNTS: usize = 12; // see TokenAccounts struct for all required accounts pub fn calculate_token_pool_account_indices( i: usize, @@ -47,7 +48,7 @@ pub fn calculate_token_pool_account_indices( pub struct TokenAccounts<'a> { pub user_token_account: &'a AccountInfo<'a>, - pub _token_billing_config: &'a AccountInfo<'a>, + pub token_billing_config: &'a AccountInfo<'a>, pub pool_chain_config: &'a AccountInfo<'a>, pub pool_program: &'a AccountInfo<'a>, pub pool_config: &'a AccountInfo<'a>, @@ -55,6 +56,7 @@ pub struct TokenAccounts<'a> { pub pool_signer: &'a AccountInfo<'a>, pub token_program: &'a AccountInfo<'a>, pub mint: &'a AccountInfo<'a>, + pub fee_token_config: &'a AccountInfo<'a>, pub remaining_accounts: &'a [AccountInfo<'a>], } @@ -78,6 +80,7 @@ pub fn validate_and_parse_token_accounts<'info>( let (pool_signer, remaining_accounts) = remaining_accounts.split_first().unwrap(); let (token_program, remaining_accounts) = remaining_accounts.split_first().unwrap(); let (mint, remaining_accounts) = remaining_accounts.split_first().unwrap(); + let (fee_token_config, remaining_accounts) = remaining_accounts.split_first().unwrap(); // Account validations (using remaining_accounts does not facilitate built-in anchor checks) { @@ -107,6 +110,22 @@ pub fn validate_and_parse_token_accounts<'info>( CcipRouterError::InvalidInputsPoolAccounts ); + let (expected_fee_token_config, _) = Pubkey::find_program_address( + &[ + FEE_BILLING_TOKEN_CONFIG, + if mint.key() == Pubkey::default() { + native_mint::ID.as_ref() // pre-2022 WSOL + } else { + mint.key.as_ref() + }, + ], + &router, + ); + require!( + fee_token_config.key() == expected_fee_token_config, + CcipRouterError::InvalidInputsConfigAccounts + ); + // check token accounts require!( *mint.owner == token_program.key(), @@ -182,6 +201,7 @@ pub fn validate_and_parse_token_accounts<'info>( pool_signer.key(), token_program.key(), mint.key(), + fee_token_config.key(), ]; let mut remaining_keys: Vec = remaining_accounts.iter().map(|x| x.key()).collect(); expected_entries.append(&mut remaining_keys); @@ -197,7 +217,7 @@ pub fn validate_and_parse_token_accounts<'info>( Ok(TokenAccounts { user_token_account, - _token_billing_config: token_billing_config, + token_billing_config, pool_chain_config, pool_program, pool_config, @@ -205,6 +225,7 @@ pub fn validate_and_parse_token_accounts<'info>( pool_signer, token_program, mint, + fee_token_config, remaining_accounts, }) } diff --git a/chains/solana/contracts/target/idl/ccip_router.json b/chains/solana/contracts/target/idl/ccip_router.json index 3271c23e..93cf82ef 100644 --- a/chains/solana/contracts/target/idl/ccip_router.json +++ b/chains/solana/contracts/target/idl/ccip_router.json @@ -994,10 +994,10 @@ "In addition to the fixed amount of accounts defined in the `GetFee` context,", "the following accounts must be provided:", "", - "* First, the billing token config accounts for each token involved, including the", + "* First, the billing token config accounts for each token sent, starting with the", "fee token, sequentially.", - "* Then, the per chain / per token config of those tokens, sequentially in the same", - "order, for the destination chain.", + "* Then, the per chain / per token config of every token sent with the message, sequentially", + "in the same order, for the destination chain.", "", "# Returns", "", @@ -1008,6 +1008,11 @@ "name": "destChainState", "isMut": false, "isSigner": false + }, + { + "name": "billingTokenConfig", + "isMut": false, + "isSigner": false } ], "args": [ diff --git a/chains/solana/contracts/tests/ccip/ccip_router_test.go b/chains/solana/contracts/tests/ccip/ccip_router_test.go index 044c948f..25b6e029 100644 --- a/chains/solana/contracts/tests/ccip/ccip_router_test.go +++ b/chains/solana/contracts/tests/ccip/ccip_router_test.go @@ -1834,15 +1834,12 @@ func TestCCIPRouter(t *testing.T) { FeeToken: wsol.mint, } - raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) - raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) - raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) feeResult := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, feeResult) - t.Log(feeResult.Meta.LogMessages) fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Meta.LogMessages, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) require.Equal(t, uint64(1), fee) }) @@ -1869,18 +1866,14 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, anotherAdmin, config.DefaultCommitment) - raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) - raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA) raw.AccountMetaSlice.Append(solana.Meta(token0BillingConfigPda)) - - raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) raw.AccountMetaSlice.Append(solana.Meta(token0PerChainPerConfigPda)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) feeResult := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, feeResult) - t.Log(feeResult.Meta.LogMessages) fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Meta.LogMessages, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) require.Equal(t, uint64(3), fee) }) @@ -1897,14 +1890,10 @@ func TestCCIPRouter(t *testing.T) { for _, address := range [][32]byte{tooBigAddress, tooSmallAddress} { message := ccip_router.Solana2AnyMessage{ Receiver: address[:], - FeeToken: token0.Mint.PublicKey(), + FeeToken: wsol.mint, } - tokenConfigPda := getTokenConfigPDA(token0.Mint.PublicKey()) - tokenBillingPda := getPerChainPerTokenConfigBillingPDA(token0.Mint.PublicKey()) - raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) - raw.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) - raw.AccountMetaSlice.Append(solana.Meta(tokenBillingPda)) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -1945,8 +1934,6 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() - raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) - raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -1980,8 +1967,6 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() - raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) - raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2665,6 +2650,11 @@ func TestCCIPRouter(t *testing.T) { replaceWith: token1.PoolLookupTable, errorStr: ccip_router.InvalidInputsLookupTableAccounts_CcipRouterError, }, + { + name: "invalid fee token config", + index: 11, + errorStr: ccip_router.InvalidInputsConfigAccounts_CcipRouterError, + }, { name: "extra accounts not in lookup table", index: 1_000, // large number to indicate append @@ -2672,7 +2662,7 @@ func TestCCIPRouter(t *testing.T) { }, { name: "remaining accounts mismatch", - index: 11, // only works with token0 + index: 12, // only works with token0 errorStr: ccip_router.InvalidInputsLookupTableAccounts_CcipRouterError, }, } @@ -2727,12 +2717,7 @@ func TestCCIPRouter(t *testing.T) { Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } - // getFee - tokenConfigPda := getTokenConfigPDA(token.mint) - perChainPerTokenConfigBillingPda := getPerChainPerTokenConfigBillingPDA(token.mint) - rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) - rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) - rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(perChainPerTokenConfigBillingPda)) + rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, token.billingConfigPDA) ix, err := rawGetFeeIx.ValidateAndBuild() require.NoError(t, err) @@ -2823,11 +2808,7 @@ func TestCCIPRouter(t *testing.T) { } // getFee - tokenConfigPda := getTokenConfigPDA(zeroPubkey) - perChainPerTokenConfigBillingPda := getPerChainPerTokenConfigBillingPDA(zeroPubkey) - rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) - rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) - rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(perChainPerTokenConfigBillingPda)) + rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA) ix, err := rawGetFeeIx.ValidateAndBuild() require.NoError(t, err) diff --git a/chains/solana/gobindings/ccip_router/GetFee.go b/chains/solana/gobindings/ccip_router/GetFee.go index db641574..558fe6bd 100644 --- a/chains/solana/gobindings/ccip_router/GetFee.go +++ b/chains/solana/gobindings/ccip_router/GetFee.go @@ -23,10 +23,10 @@ import ( // In addition to the fixed amount of accounts defined in the `GetFee` context, // the following accounts must be provided: // -// * First, the billing token config accounts for each token involved, including the +// * First, the billing token config accounts for each token sent, starting with the // fee token, sequentially. -// * Then, the per chain / per token config of those tokens, sequentially in the same -// order, for the destination chain. +// * Then, the per chain / per token config of every token sent with the message, sequentially +// in the same order, for the destination chain. // // # Returns // @@ -36,13 +36,15 @@ type GetFee struct { Message *Solana2AnyMessage // [0] = [] destChainState + // + // [1] = [] billingTokenConfig ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewGetFeeInstructionBuilder creates a new `GetFee` instruction builder. func NewGetFeeInstructionBuilder() *GetFee { nd := &GetFee{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), } return nd } @@ -70,6 +72,17 @@ func (inst *GetFee) GetDestChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetBillingTokenConfigAccount sets the "billingTokenConfig" account. +func (inst *GetFee) SetBillingTokenConfigAccount(billingTokenConfig ag_solanago.PublicKey) *GetFee { + inst.AccountMetaSlice[1] = ag_solanago.Meta(billingTokenConfig) + return inst +} + +// GetBillingTokenConfigAccount gets the "billingTokenConfig" account. +func (inst *GetFee) GetBillingTokenConfigAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + func (inst GetFee) Build() *Instruction { return &Instruction{BaseVariant: ag_binary.BaseVariant{ Impl: inst, @@ -103,6 +116,9 @@ func (inst *GetFee) Validate() error { if inst.AccountMetaSlice[0] == nil { return errors.New("accounts.DestChainState is not set") } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.BillingTokenConfig is not set") + } } return nil } @@ -122,8 +138,9 @@ func (inst *GetFee) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=1]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta("destChainState", inst.AccountMetaSlice[0])) + instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" destChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta("billingTokenConfig", inst.AccountMetaSlice[1])) }) }) }) @@ -162,9 +179,11 @@ func NewGetFeeInstruction( destChainSelector uint64, message Solana2AnyMessage, // Accounts: - destChainState ag_solanago.PublicKey) *GetFee { + destChainState ag_solanago.PublicKey, + billingTokenConfig ag_solanago.PublicKey) *GetFee { return NewGetFeeInstructionBuilder(). SetDestChainSelector(destChainSelector). SetMessage(message). - SetDestChainStateAccount(destChainState) + SetDestChainStateAccount(destChainState). + SetBillingTokenConfigAccount(billingTokenConfig) } diff --git a/chains/solana/gobindings/ccip_router/instructions.go b/chains/solana/gobindings/ccip_router/instructions.go index 36f2e303..fde1c073 100644 --- a/chains/solana/gobindings/ccip_router/instructions.go +++ b/chains/solana/gobindings/ccip_router/instructions.go @@ -276,10 +276,10 @@ var ( // In addition to the fixed amount of accounts defined in the `GetFee` context, // the following accounts must be provided: // - // * First, the billing token config accounts for each token involved, including the + // * First, the billing token config accounts for each token sent, starting with the // fee token, sequentially. - // * Then, the per chain / per token config of those tokens, sequentially in the same - // order, for the destination chain. + // * Then, the per chain / per token config of every token sent with the message, sequentially + // in the same order, for the destination chain. // // # Returns // diff --git a/chains/solana/utils/tokens/tokenpool.go b/chains/solana/utils/tokens/tokenpool.go index 3930ffe6..f0ee0e8f 100644 --- a/chains/solana/utils/tokens/tokenpool.go +++ b/chains/solana/utils/tokens/tokenpool.go @@ -15,8 +15,9 @@ import ( type TokenPool struct { // token details - Program solana.PublicKey - Mint solana.PrivateKey + Program solana.PublicKey + Mint solana.PrivateKey + FeeTokenConfig solana.PublicKey // admin registry PDA AdminRegistry solana.PublicKey @@ -47,6 +48,7 @@ func (tp TokenPool) ToTokenPoolEntries() []solana.PublicKey { tp.PoolSigner, tp.Program, tp.Mint.PublicKey(), + tp.FeeTokenConfig, } return append(list, tp.AdditionalAccounts...) } @@ -71,10 +73,15 @@ func NewTokenPool(program solana.PublicKey) (TokenPool, error) { if err != nil { return TokenPool{}, err } + tokenConfigPda, _, err := solana.FindProgramAddress([][]byte{[]byte("fee_billing_token_config"), mint.PublicKey().Bytes()}, config.CcipRouterProgram) + if err != nil { + return TokenPool{}, err + } p := TokenPool{ Program: program, Mint: mint, + FeeTokenConfig: tokenConfigPda, AdminRegistry: tokenAdminRegistryPDA, PoolLookupTable: solana.PublicKey{}, User: map[solana.PublicKey]solana.PublicKey{}, @@ -179,6 +186,7 @@ func ParseTokenLookupTable(ctx context.Context, client *rpc.Client, token TokenP solana.Meta(lookupTableEntries[5]), // PoolSigner solana.Meta(lookupTableEntries[6]), // TokenProgram solana.Meta(lookupTableEntries[7]).WRITE(), // Mint + solana.Meta(lookupTableEntries[8]), // FeeTokenConfig } for _, v := range token.AdditionalAccounts {