Skip to content

Commit

Permalink
Solana: full getValidatedFee flow [NONEVM-676] (#405)
Browse files Browse the repository at this point in the history
* Implement FeeQuoter getValidatedFee flow in Solana

Implement network fee calculation

Refactor and cleanup

Update unit test values

Add unit tests

More unit tests

Use account array for GetFee

Fix all tests except token happy path

Cleanup

Use lookup table for fee calculation in ccip_send

Cleanup

Rebase fixes

Fix lints

Comment improvement

Implement all fee calculations

Fix unit tests

Validate accounts

Make fee billing config optional

Address review comments

Adjust test values for BPS

Regenerate bindings and update tests

Address review comments

Cleanup

* Address review comment
  • Loading branch information
PabloMansanet authored Jan 7, 2025
1 parent e3ada00 commit 76f93c4
Show file tree
Hide file tree
Showing 13 changed files with 1,035 additions and 214 deletions.
530 changes: 500 additions & 30 deletions chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs

Large diffs are not rendered by default.

144 changes: 112 additions & 32 deletions chains/solana/contracts/programs/ccip-router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ use crate::ocr3base::*;
mod fee_quoter;
use crate::fee_quoter::*;

mod utils;

// Anchor discriminators for CPI calls
const CCIP_RECEIVE_DISCRIMINATOR: [u8; 8] = [0x0b, 0xf4, 0x09, 0xf9, 0x2c, 0x53, 0x2f, 0xf5]; // ccip_receive
const TOKENPOOL_LOCK_OR_BURN_DISCRIMINATOR: [u8; 8] =
Expand Down Expand Up @@ -532,16 +534,19 @@ pub mod ccip_router {
/// # Arguments
///
/// * `ctx` - The context containing the accounts required for setting the token billing configuration.
/// * `_chain_selector` - The chain selector.
/// * `_mint` - The public key of the token mint.
/// * `chain_selector` - The chain selector.
/// * `mint` - The public key of the token mint.
/// * `cfg` - The token billing configuration.
pub fn set_token_billing(
ctx: Context<SetTokenBillingConfig>,
_chain_selector: u64,
_mint: Pubkey,
chain_selector: u64,
mint: Pubkey,
cfg: TokenBilling,
) -> Result<()> {
ctx.accounts.per_chain_per_token_config.version = 1; // update this if we change the account struct
ctx.accounts.per_chain_per_token_config.billing = cfg;
ctx.accounts.per_chain_per_token_config.chain_selector = chain_selector;
ctx.accounts.per_chain_per_token_config.mint = mint;
Ok(())
}

Expand Down Expand Up @@ -673,19 +678,58 @@ pub mod ccip_router {
/// * `dest_chain_selector` - The chain selector for the destination chain.
/// * `message` - The message to be sent.
///
/// # Additional accounts
///
/// 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 sent with the message, sequentially.
/// For each token with no billing config account (i.e. tokens that cannot be possibly used as fee
/// tokens, which also have no BPS fees enabled) the ZERO address must be provided instead.
/// * Then, the per chain / per token config of every token sent with the message, sequentially
/// in the same order.
///
/// # Returns
///
/// The fee amount in u64.
pub fn get_fee(
ctx: Context<GetFee>,
pub fn get_fee<'info>(
ctx: Context<'_, '_, 'info, 'info, GetFee>,
dest_chain_selector: u64,
message: Solana2AnyMessage,
) -> Result<u64> {
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) =
remaining_accounts.split_at(message.token_amounts.len());

let token_billing_config_accounts = token_billing_config_accounts
.iter()
.zip(message.token_amounts.iter())
.map(|(a, SolanaTokenAmount { token, .. })| {
BillingTokenConfig::validated_try_from(a, *token)
})
.collect::<Result<Vec<_>>>()?;
let per_chain_per_token_config_accounts = per_chain_per_token_config_accounts
.iter()
.zip(message.token_amounts.iter())
.map(|(a, SolanaTokenAmount { token, .. })| {
PerChainPerTokenConfig::validated_try_from(a, *token, dest_chain_selector)
})
.collect::<Result<Vec<_>>>()?;

Ok(fee_for_msg(
dest_chain_selector,
&message,
message,
&ctx.accounts.dest_chain_state,
&ctx.accounts.billing_token_config.config,
&token_billing_config_accounts,
&per_chain_per_token_config_accounts,
)?
.amount)
}
Expand Down Expand Up @@ -758,8 +802,58 @@ pub mod ccip_router {
let config = ctx.accounts.config.load()?;

let dest_chain = &mut ctx.accounts.dest_chain_state;
let fee_token_config = &ctx.accounts.fee_token_config.config;
let fee = fee_for_msg(dest_chain_selector, &message, dest_chain, fee_token_config)?;

let mut accounts_per_sent_token: Vec<TokenAccounts> = 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(|accs| {
BillingTokenConfig::validated_try_from(accs.fee_token_config, accs.mint.key())
})
.collect::<Result<Vec<_>>>()?;

let per_chain_per_token_config_accounts = accounts_per_sent_token
.iter()
.map(|accs| {
PerChainPerTokenConfig::validated_try_from(
accs.token_billing_config,
accs.mint.key(),
dest_chain_selector,
)
})
.collect::<Result<Vec<_>>>()?;

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,
)?;

let is_paying_with_native_sol = message.fee_token == Pubkey::zeroed();
if is_paying_with_native_sol {
Expand Down Expand Up @@ -828,31 +922,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 = &current_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,
Expand Down Expand Up @@ -1673,6 +1749,10 @@ pub enum CcipRouterError {
InsufficientLamports,
#[msg("Insufficient funds")]
InsufficientFunds,
#[msg("Unsupported token")]
UnsupportedToken,
#[msg("Inputs are missing token configuration")]
InvalidInputsMissingTokenConfig,
}

// TODO: Refactor this to use the same structure as messages: execution_report.validate(..)
Expand Down
30 changes: 19 additions & 11 deletions chains/solana/contracts/programs/ccip-router/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,11 @@ impl Solana2AnyMessage {

#[cfg(test)]
pub(crate) mod tests {
use crate::utils::Exponential;

use super::*;
use anchor_lang::solana_program::pubkey::Pubkey;
use anchor_spl::token::spl_token::native_mint;
use bytemuck::Zeroable;

/// Builds a message and hash it, it's compared with a known hash
Expand Down Expand Up @@ -563,44 +566,49 @@ pub(crate) mod tests {

pub fn sample_billing_config() -> BillingTokenConfig {
let mut value = [0; 28];
value[27] = 3;
value.clone_from_slice(&3u32.e(18).to_be_bytes()[4..]);
BillingTokenConfig {
enabled: true,
mint: Pubkey::new_unique(),
mint: native_mint::ID,
usd_per_token: crate::TimestampedPackedU224 {
value,
timestamp: 100,
},
premium_multiplier_wei_per_eth: 0,
premium_multiplier_wei_per_eth: 1,
}
}

pub fn sample_dest_chain() -> DestChain {
let mut value = [0; 28];
// L1 gas price
value[0..14].clone_from_slice(&1u32.e(18).to_be_bytes()[18..]);
// L2 gas price
value[14..].clone_from_slice(&U256::new(22u128).to_be_bytes()[18..]);
DestChain {
version: 1,
chain_selector: 1,
state: crate::DestChainState {
sequence_number: 0,
usd_per_unit_gas: crate::TimestampedPackedU224 {
value: [0; 28],
timestamp: 0,
value,
timestamp: 100,
},
},
config: crate::DestChainConfig {
is_enabled: true,
max_number_of_tokens_per_msg: 5,
max_data_bytes: 200,
max_per_msg_gas_limit: 0,
dest_gas_overhead: 0,
dest_gas_overhead: 1,
dest_gas_per_payload_byte: 0,
dest_data_availability_overhead_gas: 0,
dest_gas_per_data_availability_byte: 0,
dest_data_availability_multiplier_bps: 0,
default_token_fee_usdcents: 0,
dest_gas_per_data_availability_byte: 1,
dest_data_availability_multiplier_bps: 1,
default_token_fee_usdcents: 100,
default_token_dest_gas_overhead: 0,
default_tx_gas_limit: 0,
gas_multiplier_wei_per_eth: 0,
network_fee_usdcents: 0,
gas_multiplier_wei_per_eth: 1,
network_fee_usdcents: 100,
gas_price_staleness_threshold: 10,
enforce_out_of_order: false,
chain_family_selector: CHAIN_FAMILY_SELECTOR_EVM.to_be_bytes(),
Expand Down
32 changes: 27 additions & 5 deletions chains/solana/contracts/programs/ccip-router/src/pools.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -12,12 +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;
const MIN_TOKEN_POOL_ACCOUNTS: usize = 11; // see TokenAccounts struct for all required accounts
pub const CCIP_LOCK_OR_BURN_V1_RET_BYTES: u32 = 32;
const MIN_TOKEN_POOL_ACCOUNTS: usize = 12; // see TokenAccounts struct for all required accounts

pub fn calculate_token_pool_account_indices(
i: usize,
Expand Down Expand Up @@ -46,14 +48,15 @@ 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>,
pub pool_token_account: &'a AccountInfo<'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>],
}

Expand All @@ -77,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)
{
Expand Down Expand Up @@ -106,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(),
Expand Down Expand Up @@ -181,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<Pubkey> = remaining_accounts.iter().map(|x| x.key()).collect();
expected_entries.append(&mut remaining_keys);
Expand All @@ -196,14 +217,15 @@ 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,
pool_token_account,
pool_signer,
token_program,
mint,
fee_token_config,
remaining_accounts,
})
}
Expand Down
Loading

0 comments on commit 76f93c4

Please sign in to comment.