diff --git a/chains/solana/Makefile b/chains/solana/Makefile index b0acf8bdd..2da8701c1 100644 --- a/chains/solana/Makefile +++ b/chains/solana/Makefile @@ -69,6 +69,14 @@ docker-build-contracts: -e CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ ${ANCHOR_IMAGE} anchor build +.PHONY: docker-update-contracts +docker-update-contracts: + docker run --rm \ + -v $(shell pwd)/contracts:/workdir \ + -v ${TARGET_DIR}:/workdir/target \ + -e CARGO_TARGET_DIR=/workdir/target \ + -e CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ + ${ANCHOR_IMAGE} anchor keys sync .PHONY: solana-checks solana-checks: clippy anchor-go-gen format gomodtidy lint-go rust-tests go-tests build-contracts diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/interfaces.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/interfaces.rs new file mode 100644 index 000000000..059e7a262 --- /dev/null +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/interfaces.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; + +use crate::context::{ + AcceptOwnership, AddBillingTokenConfig, AddDestChain, AddPriceUpdater, GasPriceUpdate, GetFee, + RemovePriceUpdater, SetTokenTransferFeeConfig, TokenPriceUpdate, UpdateBillingTokenConfig, + UpdateConfig, UpdateDestChainConfig, UpdatePrices, +}; +use crate::messages::{GetFeeResult, SVM2AnyMessage}; +use crate::state::{BillingTokenConfig, CodeVersion, DestChainConfig, TokenTransferFeeConfig}; + +pub trait Public { + fn get_fee<'info>( + &self, + ctx: Context<'_, '_, 'info, 'info, GetFee>, + dest_chain_selector: u64, + message: SVM2AnyMessage, + ) -> Result; +} + +pub trait Prices { + fn update_prices<'info>( + &self, + ctx: Context<'_, '_, 'info, 'info, UpdatePrices<'info>>, + token_updates: Vec, + gas_updates: Vec, + ) -> Result<()>; +} + +pub trait Admin { + fn transfer_ownership(&self, ctx: Context, proposed_owner: Pubkey) -> Result<()>; + + fn accept_ownership(&self, ctx: Context) -> Result<()>; + + fn set_default_code_version( + &self, + ctx: Context, + code_version: CodeVersion, + ) -> Result<()>; + + fn add_billing_token_config( + &self, + ctx: Context, + config: BillingTokenConfig, + ) -> Result<()>; + + fn update_billing_token_config( + &self, + ctx: Context, + config: BillingTokenConfig, + ) -> Result<()>; + + fn add_dest_chain( + &self, + ctx: Context, + chain_selector: u64, + dest_chain_config: DestChainConfig, + ) -> Result<()>; + + fn disable_dest_chain( + &self, + ctx: Context, + chain_selector: u64, + ) -> Result<()>; + + fn update_dest_chain_config( + &self, + ctx: Context, + chain_selector: u64, + dest_chain_config: DestChainConfig, + ) -> Result<()>; + + fn add_price_updater( + &self, + _ctx: Context, + price_updater: Pubkey, + ) -> Result<()>; + + fn remove_price_updater( + &self, + _ctx: Context, + price_updater: Pubkey, + ) -> Result<()>; + + fn set_token_transfer_fee_config( + &self, + ctx: Context, + chain_selector: u64, + mint: Pubkey, + cfg: TokenTransferFeeConfig, + ) -> Result<()>; +} diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/mod.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/mod.rs index a3a6d96c3..e955012ae 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/mod.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/mod.rs @@ -1 +1,4 @@ -pub mod v1; +mod interfaces; +mod v1; + +pub mod router; diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/router.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/router.rs new file mode 100644 index 000000000..304ab5f70 --- /dev/null +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/router.rs @@ -0,0 +1,47 @@ +use crate::state::CodeVersion; + +use super::interfaces::*; +use super::v1; + +/** + * This file routes traffic between multiple versions of our business logic, which can be upgraded in a + * backwards-compatible way and so we can gradually shift traffic between versions (and rollback if there are issues). + * This also supports flexible criteria on how to shift traffic between versions (e.g. per-lane, all at once, etc) + * for each module (e.g. the criteria for admin actions may differ the criteria for public-facing interfaces). + * + * On any code changes to the business logic, a new version of the module should be created and leave the previous one + * untouched. The new version should be added to the match statement below so that traffic can be progressively shifted, + * and is possible to rollback easily via config changes without having to redeploy. + * + * As we currently have a single version, all branches lead to the same outcome, but the code is structured in a way + * that is easy to extend to multiple versions. + */ + +pub fn public( + lane_code_version: CodeVersion, + default_code_version: CodeVersion, +) -> &'static dyn Public { + // The lane-specific code version takes precedence over the default code version. + // If the lane just specifies using the default, then we use that one. + match lane_code_version { + CodeVersion::V1 => &v1::public::Impl, + CodeVersion::Default => match default_code_version { + CodeVersion::Default => &v1::public::Impl, + CodeVersion::V1 => &v1::public::Impl, + }, + } +} + +pub fn prices(code_version: CodeVersion) -> &'static dyn Prices { + match code_version { + CodeVersion::Default => &v1::prices::Impl, + CodeVersion::V1 => &v1::prices::Impl, + } +} + +pub fn admin(code_version: CodeVersion) -> &'static dyn Admin { + match code_version { + CodeVersion::Default => &v1::admin::Impl, + CodeVersion::V1 => &v1::admin::Impl, + } +} diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/admin.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/admin.rs index 4d44a6c2f..25ff85dc7 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/admin.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/admin.rs @@ -9,152 +9,179 @@ use crate::event::{ OwnershipTransferRequested, OwnershipTransferred, PriceUpdaterAdded, PriceUpdaterRemoved, TokenTransferFeeConfigUpdated, }; +use crate::instructions::interfaces::Admin; use crate::state::{ - BillingTokenConfig, DestChain, DestChainConfig, DestChainState, PerChainPerTokenConfig, - TimestampedPackedU224, TokenTransferFeeConfig, + BillingTokenConfig, CodeVersion, DestChain, DestChainConfig, DestChainState, + PerChainPerTokenConfig, TimestampedPackedU224, TokenTransferFeeConfig, }; use crate::FeeQuoterError; -pub fn transfer_ownership(ctx: Context, proposed_owner: Pubkey) -> Result<()> { - let config = &mut ctx.accounts.config; - require!( - proposed_owner != config.owner, - FeeQuoterError::RedundantOwnerProposal - ); - emit!(OwnershipTransferRequested { - from: config.owner, - to: proposed_owner, - }); - ctx.accounts.config.proposed_owner = proposed_owner; - Ok(()) -} +pub struct Impl; +impl Admin for Impl { + fn transfer_ownership(&self, ctx: Context, proposed_owner: Pubkey) -> Result<()> { + let config = &mut ctx.accounts.config; + require!( + proposed_owner != config.owner, + FeeQuoterError::RedundantOwnerProposal + ); + emit!(OwnershipTransferRequested { + from: config.owner, + to: proposed_owner, + }); + ctx.accounts.config.proposed_owner = proposed_owner; + Ok(()) + } -pub fn accept_ownership(ctx: Context) -> Result<()> { - let config = &mut ctx.accounts.config; - emit!(OwnershipTransferred { - from: config.owner, - to: config.proposed_owner, - }); - ctx.accounts.config.owner = ctx.accounts.config.proposed_owner; - ctx.accounts.config.proposed_owner = Pubkey::default(); - Ok(()) -} + fn accept_ownership(&self, ctx: Context) -> Result<()> { + let config = &mut ctx.accounts.config; + emit!(OwnershipTransferred { + from: config.owner, + to: config.proposed_owner, + }); + ctx.accounts.config.owner = ctx.accounts.config.proposed_owner; + ctx.accounts.config.proposed_owner = Pubkey::default(); + Ok(()) + } -pub fn add_billing_token_config( - ctx: Context, - config: BillingTokenConfig, -) -> Result<()> { - emit!(FeeTokenAdded { - fee_token: config.mint, - enabled: config.enabled - }); - ctx.accounts.billing_token_config.version = 1; // update this if we change the account struct - ctx.accounts.billing_token_config.config = config; - Ok(()) -} + fn set_default_code_version( + &self, + ctx: Context, + code_version: CodeVersion, + ) -> Result<()> { + ctx.accounts.config.default_code_version = code_version; + Ok(()) + } -pub fn update_billing_token_config( - ctx: Context, - config: BillingTokenConfig, -) -> Result<()> { - if config.enabled != ctx.accounts.billing_token_config.config.enabled { - // enabled/disabled status has changed - match config.enabled { - true => emit!(FeeTokenEnabled { - fee_token: config.mint - }), - false => emit!(FeeTokenDisabled { - fee_token: config.mint - }), - } + fn add_billing_token_config( + &self, + ctx: Context, + config: BillingTokenConfig, + ) -> Result<()> { + emit!(FeeTokenAdded { + fee_token: config.mint, + enabled: config.enabled + }); + ctx.accounts.billing_token_config.version = 1; // update this if we change the account struct + ctx.accounts.billing_token_config.config = config; + Ok(()) } - // TODO should we emit an event if the config has changed regardless of the enabled/disabled? - ctx.accounts.billing_token_config.version = 1; // update this if we change the account struct - ctx.accounts.billing_token_config.config = config; - Ok(()) -} + fn update_billing_token_config( + &self, + ctx: Context, + config: BillingTokenConfig, + ) -> Result<()> { + if config.enabled != ctx.accounts.billing_token_config.config.enabled { + // enabled/disabled status has changed + match config.enabled { + true => emit!(FeeTokenEnabled { + fee_token: config.mint + }), + false => emit!(FeeTokenDisabled { + fee_token: config.mint + }), + } + } + // TODO should we emit an event if the config has changed regardless of the enabled/disabled? + + ctx.accounts.billing_token_config.version = 1; // update this if we change the account struct + ctx.accounts.billing_token_config.config = config; + Ok(()) + } -pub fn add_dest_chain( - ctx: Context, - chain_selector: u64, - dest_chain_config: DestChainConfig, -) -> Result<()> { - validate_dest_chain_config(chain_selector, &dest_chain_config)?; - ctx.accounts.dest_chain.set_inner(DestChain { - version: 1, - chain_selector, - state: DestChainState { - usd_per_unit_gas: TimestampedPackedU224 { - timestamp: 0, - value: [0; 28], + fn add_dest_chain( + &self, + ctx: Context, + chain_selector: u64, + dest_chain_config: DestChainConfig, + ) -> Result<()> { + validate_dest_chain_config(chain_selector, &dest_chain_config)?; + ctx.accounts.dest_chain.set_inner(DestChain { + version: 1, + chain_selector, + state: DestChainState { + usd_per_unit_gas: TimestampedPackedU224 { + timestamp: 0, + value: [0; 28], + }, }, - }, - config: dest_chain_config.clone(), - }); - emit!(DestChainAdded { - dest_chain_selector: chain_selector, - dest_chain_config, - }); - Ok(()) -} + config: dest_chain_config.clone(), + }); + emit!(DestChainAdded { + dest_chain_selector: chain_selector, + dest_chain_config, + }); + Ok(()) + } -pub fn disable_dest_chain(ctx: Context, chain_selector: u64) -> Result<()> { - ctx.accounts.dest_chain.config.is_enabled = false; - emit!(DestChainConfigUpdated { - dest_chain_selector: chain_selector, - dest_chain_config: ctx.accounts.dest_chain.config.clone(), - }); - Ok(()) -} + fn disable_dest_chain( + &self, + ctx: Context, + chain_selector: u64, + ) -> Result<()> { + ctx.accounts.dest_chain.config.is_enabled = false; + emit!(DestChainConfigUpdated { + dest_chain_selector: chain_selector, + dest_chain_config: ctx.accounts.dest_chain.config.clone(), + }); + Ok(()) + } -pub fn update_dest_chain_config( - ctx: Context, - chain_selector: u64, - dest_chain_config: DestChainConfig, -) -> Result<()> { - validate_dest_chain_config(chain_selector, &dest_chain_config)?; - ctx.accounts.dest_chain.config = dest_chain_config.clone(); - emit!(DestChainConfigUpdated { - dest_chain_selector: chain_selector, - dest_chain_config, - }); - Ok(()) -} + fn update_dest_chain_config( + &self, + ctx: Context, + chain_selector: u64, + dest_chain_config: DestChainConfig, + ) -> Result<()> { + validate_dest_chain_config(chain_selector, &dest_chain_config)?; + ctx.accounts.dest_chain.config = dest_chain_config.clone(); + emit!(DestChainConfigUpdated { + dest_chain_selector: chain_selector, + dest_chain_config, + }); + Ok(()) + } -pub fn add_price_updater(_ctx: Context, price_updater: Pubkey) -> Result<()> { - emit!(PriceUpdaterAdded { price_updater }); - Ok(()) -} + fn add_price_updater( + &self, + _ctx: Context, + price_updater: Pubkey, + ) -> Result<()> { + emit!(PriceUpdaterAdded { price_updater }); + Ok(()) + } -pub fn remove_price_updater( - _ctx: Context, - price_updater: Pubkey, -) -> Result<()> { - emit!(PriceUpdaterRemoved { price_updater }); - Ok(()) -} + fn remove_price_updater( + &self, + _ctx: Context, + price_updater: Pubkey, + ) -> Result<()> { + emit!(PriceUpdaterRemoved { price_updater }); + Ok(()) + } -pub fn set_token_transfer_fee_config( - ctx: Context, - chain_selector: u64, - mint: Pubkey, - cfg: TokenTransferFeeConfig, -) -> Result<()> { - ctx.accounts - .per_chain_per_token_config - .set_inner(PerChainPerTokenConfig { - version: 1, // update this if we change the account struct - chain_selector, - mint, - token_transfer_config: cfg.clone(), + fn set_token_transfer_fee_config( + &self, + ctx: Context, + chain_selector: u64, + mint: Pubkey, + cfg: TokenTransferFeeConfig, + ) -> Result<()> { + ctx.accounts + .per_chain_per_token_config + .set_inner(PerChainPerTokenConfig { + version: 1, // update this if we change the account struct + chain_selector, + mint, + token_transfer_config: cfg.clone(), + }); + emit!(TokenTransferFeeConfigUpdated { + dest_chain_selector: chain_selector, + token: mint, + token_transfer_fee_config: cfg, }); - emit!(TokenTransferFeeConfigUpdated { - dest_chain_selector: chain_selector, - token: mint, - token_transfer_fee_config: cfg, - }); - Ok(()) + Ok(()) + } } // --- helpers --- diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/messages.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/messages.rs index 115434969..013e21924 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/messages.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/messages.rs @@ -495,6 +495,7 @@ pub mod tests { state: crate::DestChainState { usd_per_unit_gas }, config: crate::DestChainConfig { is_enabled: true, + lane_code_version: crate::state::CodeVersion::Default, max_number_of_tokens_per_msg: 1, max_data_bytes: 30000, max_per_msg_gas_limit: 3000000, diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/mod.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/mod.rs index 3e70e5ec7..5b6ab5ec1 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/mod.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/mod.rs @@ -1,13 +1,13 @@ -//////////////////////////////////////////////////////////////////// -// Public modules, to be exposed in lib.rs as program entrypoints // -//////////////////////////////////////////////////////////////////// -pub mod admin; // to be invoked by admin only -pub mod prices; // to be invoked by price updaters (e.g. offramp) only -pub mod public; // to be invoked by users directly & onramp +//////////////////////////////////////////////////////////////////////////////// +// Public modules, to be exposed via routers to lib.rs as program entrypoints // +//////////////////////////////////////////////////////////////////////////////// +pub(super) mod admin; // to be invoked by admin only +pub(super) mod prices; // to be invoked by price updaters (e.g. offramp) only +pub(super) mod public; // to be invoked by users directly & onramp -//////////////////////////////////////////////////////////////////////// -// Private modules, just to be used within the instructions submodule // -//////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////// +// Private modules, just to be used within the instructions versioned submodule // +////////////////////////////////////////////////////////////////////////////////// mod messages; mod price_math; mod safe_deserialize; diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/prices.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/prices.rs index 9ed6d38fa..61225f9f1 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/prices.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/prices.rs @@ -3,48 +3,53 @@ use anchor_lang::prelude::*; use crate::context::seed::{DEST_CHAIN, FEE_BILLING_TOKEN_CONFIG}; use crate::context::{GasPriceUpdate, TokenPriceUpdate, UpdatePrices}; use crate::event::{UsdPerTokenUpdated, UsdPerUnitGasUpdated}; +use crate::instructions::interfaces::Prices; use crate::state::{BillingTokenConfigWrapper, DestChain, TimestampedPackedU224}; use crate::FeeQuoterError; -pub fn update_prices<'info>( - ctx: Context<'_, '_, 'info, 'info, UpdatePrices<'info>>, - token_updates: Vec, - gas_updates: Vec, -) -> Result<()> { - require!( - !token_updates.is_empty() || !gas_updates.is_empty(), - FeeQuoterError::InvalidInputsNoUpdates - ); - - // Remaining accounts represent: - // - the accounts to update BillingTokenConfig for token prices - // - the accounts to update DestChain for gas prices - // They must be in order: - // 1. token_accounts[] - // 2. gas_accounts[] - // matching the order of the price updates. - // They must also all be writable so they can be updated. - require_eq!( - ctx.remaining_accounts.len(), - token_updates.len() + gas_updates.len(), - FeeQuoterError::InvalidInputsAccountCount - ); - - // For each token price update, unpack the corresponding remaining_account and update the price. - // Keep in mind that the remaining_accounts are sorted in the same order as tokens and gas price updates in the report. - for (i, update) in token_updates.iter().enumerate() { - apply_token_price_update(update, &ctx.remaining_accounts[i])?; +pub struct Impl; +impl Prices for Impl { + fn update_prices<'info>( + &self, + ctx: Context<'_, '_, 'info, 'info, UpdatePrices<'info>>, + token_updates: Vec, + gas_updates: Vec, + ) -> Result<()> { + require!( + !token_updates.is_empty() || !gas_updates.is_empty(), + FeeQuoterError::InvalidInputsNoUpdates + ); + + // Remaining accounts represent: + // - the accounts to update BillingTokenConfig for token prices + // - the accounts to update DestChain for gas prices + // They must be in order: + // 1. token_accounts[] + // 2. gas_accounts[] + // matching the order of the price updates. + // They must also all be writable so they can be updated. + require_eq!( + ctx.remaining_accounts.len(), + token_updates.len() + gas_updates.len(), + FeeQuoterError::InvalidInputsAccountCount + ); + + // For each token price update, unpack the corresponding remaining_account and update the price. + // Keep in mind that the remaining_accounts are sorted in the same order as tokens and gas price updates in the report. + for (i, update) in token_updates.iter().enumerate() { + apply_token_price_update(update, &ctx.remaining_accounts[i])?; + } + + // Skip the first state account and the ones for token updates + let offset = token_updates.len(); + + // Do the same for gas price updates + for (i, update) in gas_updates.iter().enumerate() { + apply_gas_price_update(update, &ctx.remaining_accounts[i + offset])?; + } + + Ok(()) } - - // Skip the first state account and the ones for token updates - let offset = token_updates.len(); - - // Do the same for gas price updates - for (i, update) in gas_updates.iter().enumerate() { - apply_gas_price_update(update, &ctx.remaining_accounts[i + offset])?; - } - - Ok(()) } ///////////// diff --git a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/public.rs b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/public.rs index ae608b335..71568617c 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/public.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/instructions/v1/public.rs @@ -11,6 +11,8 @@ use crate::messages::{ use crate::state::{BillingTokenConfig, DestChain, PerChainPerTokenConfig, TimestampedPackedU224}; use crate::FeeQuoterError; +use super::super::interfaces::Public; + use super::messages::validate_svm2any; use super::price_math::{get_validated_token_price, Exponential, Usd18Decimals}; @@ -33,85 +35,91 @@ pub const SVM_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN: U256 = U256::new(32 * ((2 * 3 pub const CCIP_LOCK_OR_BURN_V1_RET_BYTES: u32 = 32; -pub fn get_fee<'info>( - ctx: Context<'_, '_, 'info, 'info, GetFee>, - dest_chain_selector: u64, - message: SVM2AnyMessage, -) -> Result { - let remaining_accounts = &ctx.remaining_accounts; - let message = &message; - require_eq!( - remaining_accounts.len(), - 2 * message.token_amounts.len(), - FeeQuoterError::InvalidInputsTokenAccounts - ); +pub struct Impl; +impl Public for Impl { + fn get_fee<'info>( + &self, + ctx: Context<'_, '_, 'info, 'info, GetFee>, + dest_chain_selector: u64, + message: SVM2AnyMessage, + ) -> Result { + let remaining_accounts = &ctx.remaining_accounts; + let message = &message; + require_eq!( + remaining_accounts.len(), + 2 * message.token_amounts.len(), + FeeQuoterError::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, SVMTokenAmount { token, .. })| safe_deserialize::billing_token_config(a, *token)) - .collect::>>()?; - let per_chain_per_token_config_accounts = per_chain_per_token_config_accounts - .iter() - .zip(message.token_amounts.iter()) - .map(|(a, SVMTokenAmount { token, .. })| { - safe_deserialize::per_chain_per_token_config(a, *token, dest_chain_selector) - }) - .collect::>>()?; + let (token_billing_config_accounts, per_chain_per_token_config_accounts) = + remaining_accounts.split_at(message.token_amounts.len()); - let (fee, processed_extra_args) = fee_for_msg( - message, - &ctx.accounts.dest_chain, - &ctx.accounts.billing_token_config.config, - &token_billing_config_accounts, - &per_chain_per_token_config_accounts, - )?; - - let juels = convert( - &fee, - &ctx.accounts.billing_token_config.config, - &ctx.accounts.link_token_config.config, - )? - .amount; - - require_gte!( - ctx.accounts.config.max_fee_juels_per_msg, - juels as u128, - FeeQuoterError::MessageFeeTooHigh - ); + let token_billing_config_accounts = token_billing_config_accounts + .iter() + .zip(message.token_amounts.iter()) + .map(|(a, SVMTokenAmount { token, .. })| { + safe_deserialize::billing_token_config(a, *token) + }) + .collect::>>()?; + let per_chain_per_token_config_accounts = per_chain_per_token_config_accounts + .iter() + .zip(message.token_amounts.iter()) + .map(|(a, SVMTokenAmount { token, .. })| { + safe_deserialize::per_chain_per_token_config(a, *token, dest_chain_selector) + }) + .collect::>>()?; + + let (fee, processed_extra_args) = fee_for_msg( + message, + &ctx.accounts.dest_chain, + &ctx.accounts.billing_token_config.config, + &token_billing_config_accounts, + &per_chain_per_token_config_accounts, + )?; + + let juels = convert( + &fee, + &ctx.accounts.billing_token_config.config, + &ctx.accounts.link_token_config.config, + )? + .amount; + + require_gte!( + ctx.accounts.config.max_fee_juels_per_msg, + juels as u128, + FeeQuoterError::MessageFeeTooHigh + ); - let token_transfer_additional_data = per_chain_per_token_config_accounts - .iter() - .map( - |per_chain_per_token_config| match per_chain_per_token_config { - Some(config) if config.token_transfer_config.is_enabled => { - TokenTransferAdditionalData { - dest_bytes_overhead: config.token_transfer_config.dest_bytes_overhead, - dest_gas_overhead: config.token_transfer_config.dest_gas_overhead, + let token_transfer_additional_data = per_chain_per_token_config_accounts + .iter() + .map( + |per_chain_per_token_config| match per_chain_per_token_config { + Some(config) if config.token_transfer_config.is_enabled => { + TokenTransferAdditionalData { + dest_bytes_overhead: config.token_transfer_config.dest_bytes_overhead, + dest_gas_overhead: config.token_transfer_config.dest_gas_overhead, + } } - } - _ => TokenTransferAdditionalData { - dest_bytes_overhead: ctx - .accounts - .dest_chain - .config - .default_token_dest_gas_overhead, - dest_gas_overhead: CCIP_LOCK_OR_BURN_V1_RET_BYTES, + _ => TokenTransferAdditionalData { + dest_bytes_overhead: ctx + .accounts + .dest_chain + .config + .default_token_dest_gas_overhead, + dest_gas_overhead: CCIP_LOCK_OR_BURN_V1_RET_BYTES, + }, }, - }, - ) - .collect(); + ) + .collect(); - Ok(GetFeeResult { - token: fee.token, - amount: fee.amount, - juels, - token_transfer_additional_data, - processed_extra_args, - }) + Ok(GetFeeResult { + token: fee.token, + amount: fee.amount, + juels, + token_transfer_additional_data, + processed_extra_args, + }) + } } // Converts a token amount to one denominated in another token (e.g. from WSOL to LINK) diff --git a/chains/solana/contracts/programs/fee-quoter/src/lib.rs b/chains/solana/contracts/programs/fee-quoter/src/lib.rs index dd464e4bf..3ed5c4996 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/lib.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/lib.rs @@ -17,7 +17,7 @@ pub mod event; pub mod extra_args; mod instructions; -use instructions::v1; +use instructions::router; #[program] pub mod fee_quoter { @@ -48,6 +48,7 @@ pub mod fee_quoter { max_fee_juels_per_msg, link_token_mint, onramp, + default_code_version: CodeVersion::V1, }); Ok(()) @@ -62,7 +63,7 @@ pub mod fee_quoter { /// * `ctx` - The context containing the accounts required for the transfer. /// * `proposed_owner` - The public key of the new proposed owner. pub fn transfer_ownership(ctx: Context, new_owner: Pubkey) -> Result<()> { - v1::admin::transfer_ownership(ctx, new_owner) + router::admin(ctx.accounts.config.default_code_version).transfer_ownership(ctx, new_owner) } /// Accepts the ownership of the fee quoter by the proposed owner. @@ -74,7 +75,15 @@ pub mod fee_quoter { /// * `ctx` - The context containing the accounts required for accepting ownership. /// The new owner must be a signer of the transaction. pub fn accept_ownership(ctx: Context) -> Result<()> { - v1::admin::accept_ownership(ctx) + router::admin(ctx.accounts.config.default_code_version).accept_ownership(ctx) + } + + pub fn set_default_code_version( + ctx: Context, + code_version: CodeVersion, + ) -> Result<()> { + router::admin(ctx.accounts.config.default_code_version) + .set_default_code_version(ctx, code_version) } /// Adds a billing token configuration. @@ -88,7 +97,8 @@ pub mod fee_quoter { ctx: Context, config: BillingTokenConfig, ) -> Result<()> { - v1::admin::add_billing_token_config(ctx, config) + router::admin(ctx.accounts.config.default_code_version) + .add_billing_token_config(ctx, config) } /// Updates the billing token configuration. @@ -102,7 +112,8 @@ pub mod fee_quoter { ctx: Context, config: BillingTokenConfig, ) -> Result<()> { - v1::admin::update_billing_token_config(ctx, config) + router::admin(ctx.accounts.config.default_code_version) + .update_billing_token_config(ctx, config) } /// Adds a new destination chain selector to the fee quoter. @@ -120,7 +131,11 @@ pub mod fee_quoter { chain_selector: u64, dest_chain_config: DestChainConfig, ) -> Result<()> { - v1::admin::add_dest_chain(ctx, chain_selector, dest_chain_config) + router::admin(ctx.accounts.config.default_code_version).add_dest_chain( + ctx, + chain_selector, + dest_chain_config, + ) } /// Disables the destination chain selector. @@ -135,7 +150,8 @@ pub mod fee_quoter { ctx: Context, chain_selector: u64, ) -> Result<()> { - v1::admin::disable_dest_chain(ctx, chain_selector) + router::admin(ctx.accounts.config.default_code_version) + .disable_dest_chain(ctx, chain_selector) } /// Updates the configuration of the destination chain selector. @@ -152,7 +168,11 @@ pub mod fee_quoter { chain_selector: u64, dest_chain_config: DestChainConfig, ) -> Result<()> { - v1::admin::update_dest_chain_config(ctx, chain_selector, dest_chain_config) + router::admin(ctx.accounts.config.default_code_version).update_dest_chain_config( + ctx, + chain_selector, + dest_chain_config, + ) } /// Sets the token transfer fee configuration for a particular token when it's transferred to a particular dest chain. @@ -173,7 +193,12 @@ pub mod fee_quoter { mint: Pubkey, cfg: TokenTransferFeeConfig, ) -> Result<()> { - v1::admin::set_token_transfer_fee_config(ctx, chain_selector, mint, cfg) + router::admin(ctx.accounts.config.default_code_version).set_token_transfer_fee_config( + ctx, + chain_selector, + mint, + cfg, + ) } /// Add a price updater address to the list of allowed price updaters. @@ -184,7 +209,8 @@ pub mod fee_quoter { /// * `ctx` - The context containing the accounts required for this operation. /// * `price_updater` - The price updater address. pub fn add_price_updater(ctx: Context, price_updater: Pubkey) -> Result<()> { - v1::admin::add_price_updater(ctx, price_updater) + router::admin(ctx.accounts.config.default_code_version) + .add_price_updater(ctx, price_updater) } /// Remove a price updater address from the list of allowed price updaters. @@ -197,7 +223,8 @@ pub mod fee_quoter { ctx: Context, price_updater: Pubkey, ) -> Result<()> { - v1::admin::remove_price_updater(ctx, price_updater) + router::admin(ctx.accounts.config.default_code_version) + .remove_price_updater(ctx, price_updater) } /// Calculates the fee for sending a message to the destination chain. @@ -232,7 +259,13 @@ pub mod fee_quoter { dest_chain_selector: u64, message: SVM2AnyMessage, ) -> Result { - v1::public::get_fee(ctx, dest_chain_selector, message) + let default_code_version = ctx.accounts.config.default_code_version; + let lane_code_version = ctx.accounts.dest_chain.config.lane_code_version; + router::public(lane_code_version, default_code_version).get_fee( + ctx, + dest_chain_selector, + message, + ) } /// Updates prices for tokens and gas. This method may only be called by an allowed price updater. @@ -257,7 +290,11 @@ pub mod fee_quoter { token_updates: Vec, gas_updates: Vec, ) -> Result<()> { - v1::prices::update_prices(ctx, token_updates, gas_updates) + router::prices(ctx.accounts.config.default_code_version).update_prices( + ctx, + token_updates, + gas_updates, + ) } } diff --git a/chains/solana/contracts/programs/fee-quoter/src/state.rs b/chains/solana/contracts/programs/fee-quoter/src/state.rs index 1dbf50712..15fd6e1be 100644 --- a/chains/solana/contracts/programs/fee-quoter/src/state.rs +++ b/chains/solana/contracts/programs/fee-quoter/src/state.rs @@ -1,5 +1,13 @@ +use anchor_lang::prelude::borsh::{BorshDeserialize, BorshSerialize}; use anchor_lang::prelude::*; +#[derive(Debug, PartialEq, Eq, Clone, Copy, InitSpace, BorshSerialize, BorshDeserialize)] +#[repr(u8)] +pub enum CodeVersion { + Default = 0, + V1, +} + #[account] #[derive(InitSpace, Debug)] pub struct Config { @@ -14,12 +22,17 @@ pub struct Config { // TODO The following field is unused until the day we integrate with feeds to fetch fresh values // pub token_price_staleness_threshold: u32, pub onramp: Pubkey, + + pub default_code_version: CodeVersion, } #[derive(Clone, AnchorSerialize, AnchorDeserialize, InitSpace, Debug)] pub struct DestChainConfig { pub is_enabled: bool, // Whether this destination chain is enabled + // The code version of the lane, in case we need to shift traffic to new logic for a single lane to test an upgrade + pub lane_code_version: CodeVersion, + pub max_number_of_tokens_per_msg: u16, // Maximum number of distinct ERC20 token transferred per message pub max_data_bytes: u32, // Maximum payload data size in bytes pub max_per_msg_gas_limit: u32, // Maximum gas limit for messages targeting EVMs diff --git a/chains/solana/contracts/target/idl/fee_quoter.json b/chains/solana/contracts/target/idl/fee_quoter.json index 5f4b6ad99..fd11968e0 100644 --- a/chains/solana/contracts/target/idl/fee_quoter.json +++ b/chains/solana/contracts/target/idl/fee_quoter.json @@ -116,6 +116,29 @@ ], "args": [] }, + { + "name": "setDefaultCodeVersion", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "codeVersion", + "type": { + "defined": "CodeVersion" + } + } + ] + }, { "name": "addBillingTokenConfig", "docs": [ @@ -642,6 +665,12 @@ { "name": "onramp", "type": "publicKey" + }, + { + "name": "defaultCodeVersion", + "type": { + "defined": "CodeVersion" + } } ] } @@ -955,6 +984,12 @@ "name": "isEnabled", "type": "bool" }, + { + "name": "laneCodeVersion", + "type": { + "defined": "CodeVersion" + } + }, { "name": "maxNumberOfTokensPerMsg", "type": "u16" @@ -1128,6 +1163,20 @@ ] } }, + { + "name": "CodeVersion", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Default" + }, + { + "name": "V1" + } + ] + } + }, { "name": "FeeQuoterError", "type": { diff --git a/chains/solana/contracts/tests/ccip/ccip_router_test.go b/chains/solana/contracts/tests/ccip/ccip_router_test.go index e85ba31e3..3656e7334 100644 --- a/chains/solana/contracts/tests/ccip/ccip_router_test.go +++ b/chains/solana/contracts/tests/ccip/ccip_router_test.go @@ -148,6 +148,8 @@ func TestCCIPRouter(t *testing.T) { validFqDestChainConfig := fee_quoter.DestChainConfig{ IsEnabled: true, + LaneCodeVersion: fee_quoter.Default_CodeVersion, + // minimal valid config DefaultTxGasLimit: 200000, MaxPerMsgGasLimit: 3000000, diff --git a/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion.go b/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion.go new file mode 100644 index 000000000..2b4fe42e7 --- /dev/null +++ b/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion.go @@ -0,0 +1,146 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package fee_quoter + +import ( + "errors" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// SetDefaultCodeVersion is the `setDefaultCodeVersion` instruction. +type SetDefaultCodeVersion struct { + CodeVersion *CodeVersion + + // [0] = [WRITE] config + // + // [1] = [SIGNER] authority + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewSetDefaultCodeVersionInstructionBuilder creates a new `SetDefaultCodeVersion` instruction builder. +func NewSetDefaultCodeVersionInstructionBuilder() *SetDefaultCodeVersion { + nd := &SetDefaultCodeVersion{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } + return nd +} + +// SetCodeVersion sets the "codeVersion" parameter. +func (inst *SetDefaultCodeVersion) SetCodeVersion(codeVersion CodeVersion) *SetDefaultCodeVersion { + inst.CodeVersion = &codeVersion + return inst +} + +// SetConfigAccount sets the "config" account. +func (inst *SetDefaultCodeVersion) SetConfigAccount(config ag_solanago.PublicKey) *SetDefaultCodeVersion { + inst.AccountMetaSlice[0] = ag_solanago.Meta(config).WRITE() + return inst +} + +// GetConfigAccount gets the "config" account. +func (inst *SetDefaultCodeVersion) GetConfigAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetAuthorityAccount sets the "authority" account. +func (inst *SetDefaultCodeVersion) SetAuthorityAccount(authority ag_solanago.PublicKey) *SetDefaultCodeVersion { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +// GetAuthorityAccount gets the "authority" account. +func (inst *SetDefaultCodeVersion) GetAuthorityAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +func (inst SetDefaultCodeVersion) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: Instruction_SetDefaultCodeVersion, + }} +} + +// ValidateAndBuild validates the instruction parameters and accounts; +// if there is a validation error, it returns the error. +// Otherwise, it builds and returns the instruction. +func (inst SetDefaultCodeVersion) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *SetDefaultCodeVersion) Validate() error { + // Check whether all (required) parameters are set: + { + if inst.CodeVersion == nil { + return errors.New("CodeVersion parameter is not set") + } + } + + // Check whether all (required) accounts are set: + { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Config is not set") + } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Authority is not set") + } + } + return nil +} + +func (inst *SetDefaultCodeVersion) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + // + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("SetDefaultCodeVersion")). + // + ParentFunc(func(instructionBranch ag_treeout.Branches) { + + // Parameters of the instruction: + instructionBranch.Child("Params[len=1]").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("CodeVersion", *inst.CodeVersion)) + }) + + // Accounts of the instruction: + instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +func (obj SetDefaultCodeVersion) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `CodeVersion` param: + err = encoder.Encode(obj.CodeVersion) + if err != nil { + return err + } + return nil +} +func (obj *SetDefaultCodeVersion) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `CodeVersion`: + err = decoder.Decode(&obj.CodeVersion) + if err != nil { + return err + } + return nil +} + +// NewSetDefaultCodeVersionInstruction declares a new SetDefaultCodeVersion instruction with the provided parameters and accounts. +func NewSetDefaultCodeVersionInstruction( + // Parameters: + codeVersion CodeVersion, + // Accounts: + config ag_solanago.PublicKey, + authority ag_solanago.PublicKey) *SetDefaultCodeVersion { + return NewSetDefaultCodeVersionInstructionBuilder(). + SetCodeVersion(codeVersion). + SetConfigAccount(config). + SetAuthorityAccount(authority) +} diff --git a/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion_test.go b/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion_test.go new file mode 100644 index 000000000..87b5fcd45 --- /dev/null +++ b/chains/solana/gobindings/fee_quoter/SetDefaultCodeVersion_test.go @@ -0,0 +1,32 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package fee_quoter + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_SetDefaultCodeVersion(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("SetDefaultCodeVersion"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(SetDefaultCodeVersion) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(SetDefaultCodeVersion) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/chains/solana/gobindings/fee_quoter/accounts.go b/chains/solana/gobindings/fee_quoter/accounts.go index 43014cd5b..8eb3a2595 100644 --- a/chains/solana/gobindings/fee_quoter/accounts.go +++ b/chains/solana/gobindings/fee_quoter/accounts.go @@ -9,12 +9,13 @@ import ( ) type Config struct { - Version uint8 - Owner ag_solanago.PublicKey - ProposedOwner ag_solanago.PublicKey - MaxFeeJuelsPerMsg ag_binary.Uint128 - LinkTokenMint ag_solanago.PublicKey - Onramp ag_solanago.PublicKey + Version uint8 + Owner ag_solanago.PublicKey + ProposedOwner ag_solanago.PublicKey + MaxFeeJuelsPerMsg ag_binary.Uint128 + LinkTokenMint ag_solanago.PublicKey + Onramp ag_solanago.PublicKey + DefaultCodeVersion CodeVersion } var ConfigDiscriminator = [8]byte{155, 12, 170, 224, 30, 250, 204, 130} @@ -55,6 +56,11 @@ func (obj Config) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { if err != nil { return err } + // Serialize `DefaultCodeVersion` param: + err = encoder.Encode(obj.DefaultCodeVersion) + if err != nil { + return err + } return nil } @@ -102,6 +108,11 @@ func (obj *Config) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) if err != nil { return err } + // Deserialize `DefaultCodeVersion`: + err = decoder.Decode(&obj.DefaultCodeVersion) + if err != nil { + return err + } return nil } diff --git a/chains/solana/gobindings/fee_quoter/instructions.go b/chains/solana/gobindings/fee_quoter/instructions.go index 70a2ec690..5c5eea530 100644 --- a/chains/solana/gobindings/fee_quoter/instructions.go +++ b/chains/solana/gobindings/fee_quoter/instructions.go @@ -61,6 +61,8 @@ var ( // The new owner must be a signer of the transaction. Instruction_AcceptOwnership = ag_binary.TypeID([8]byte{172, 23, 43, 13, 238, 213, 85, 150}) + Instruction_SetDefaultCodeVersion = ag_binary.TypeID([8]byte{47, 151, 233, 254, 121, 82, 206, 152}) + // Adds a billing token configuration. // Only CCIP Admin can add a billing token configuration. // @@ -201,6 +203,8 @@ func InstructionIDToName(id ag_binary.TypeID) string { return "TransferOwnership" case Instruction_AcceptOwnership: return "AcceptOwnership" + case Instruction_SetDefaultCodeVersion: + return "SetDefaultCodeVersion" case Instruction_AddBillingTokenConfig: return "AddBillingTokenConfig" case Instruction_UpdateBillingTokenConfig: @@ -250,6 +254,9 @@ var InstructionImplDef = ag_binary.NewVariantDefinition( { "accept_ownership", (*AcceptOwnership)(nil), }, + { + "set_default_code_version", (*SetDefaultCodeVersion)(nil), + }, { "add_billing_token_config", (*AddBillingTokenConfig)(nil), }, diff --git a/chains/solana/gobindings/fee_quoter/types.go b/chains/solana/gobindings/fee_quoter/types.go index c58d7a5a5..93f66b195 100644 --- a/chains/solana/gobindings/fee_quoter/types.go +++ b/chains/solana/gobindings/fee_quoter/types.go @@ -416,6 +416,7 @@ func (obj *ProcessedExtraArgs) UnmarshalWithDecoder(decoder *ag_binary.Decoder) type DestChainConfig struct { IsEnabled bool + LaneCodeVersion CodeVersion MaxNumberOfTokensPerMsg uint16 MaxDataBytes uint32 MaxPerMsgGasLimit uint32 @@ -442,6 +443,11 @@ func (obj DestChainConfig) MarshalWithEncoder(encoder *ag_binary.Encoder) (err e if err != nil { return err } + // Serialize `LaneCodeVersion` param: + err = encoder.Encode(obj.LaneCodeVersion) + if err != nil { + return err + } // Serialize `MaxNumberOfTokensPerMsg` param: err = encoder.Encode(obj.MaxNumberOfTokensPerMsg) if err != nil { @@ -541,6 +547,11 @@ func (obj *DestChainConfig) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (er if err != nil { return err } + // Deserialize `LaneCodeVersion`: + err = decoder.Decode(&obj.LaneCodeVersion) + if err != nil { + return err + } // Deserialize `MaxNumberOfTokensPerMsg`: err = decoder.Decode(&obj.MaxNumberOfTokensPerMsg) if err != nil { @@ -821,6 +832,24 @@ func (obj *TokenTransferFeeConfig) UnmarshalWithDecoder(decoder *ag_binary.Decod return nil } +type CodeVersion ag_binary.BorshEnum + +const ( + Default_CodeVersion CodeVersion = iota + V1_CodeVersion +) + +func (value CodeVersion) String() string { + switch value { + case Default_CodeVersion: + return "Default" + case V1_CodeVersion: + return "V1" + default: + return "" + } +} + type FeeQuoterError ag_binary.BorshEnum const (