Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solana: full getValidatedFee flow [NONEVM-676] #405

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
PabloMansanet marked this conversation as resolved.
Show resolved Hide resolved
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>,
PabloMansanet marked this conversation as resolved.
Show resolved Hide resolved
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()
},
Comment on lines +115 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably not necessary for tokenpools. Although this is how we solve for fee_token_config accounts, AFAIK it should never be the case that someone is sending native SOL through CCIP (@agusaldasoro or @aalu1418 to confirm, IMHO they should send WSOL in that case instead).
If my above statement holds, you can just require a non-zero mint and remove this if.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will address this on a followup if the conclusion is to remove :)

],
&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
Loading