diff --git a/chains/solana/contracts/Cargo.lock b/chains/solana/contracts/Cargo.lock index c0b1a372..685bf8bf 100644 --- a/chains/solana/contracts/Cargo.lock +++ b/chains/solana/contracts/Cargo.lock @@ -674,6 +674,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "bytemuck", + "ethnum", "hex", "solana-program", ] @@ -943,6 +944,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + [[package]] name = "external-program-cpi-stub" version = "0.0.0-dev" @@ -2395,8 +2402,10 @@ version = "0.0.1-dev" dependencies = [ "access-controller", "anchor-lang", + "arrayvec 1.0.0", "bytemuck", "hex", + "static_assertions", ] [[package]] diff --git a/chains/solana/contracts/programs/ccip-router/Cargo.toml b/chains/solana/contracts/programs/ccip-router/Cargo.toml index f3b26d42..3edbf234 100644 --- a/chains/solana/contracts/programs/ccip-router/Cargo.toml +++ b/chains/solana/contracts/programs/ccip-router/Cargo.toml @@ -20,6 +20,7 @@ solana-program = "1.17.25" # pin solana to 1.17 anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } anchor-spl = "0.29.0" bytemuck = "1.7" +ethnum = "1.5" [dev-dependencies] hex = "0.4.3" diff --git a/chains/solana/contracts/programs/ccip-router/src/context.rs b/chains/solana/contracts/programs/ccip-router/src/context.rs index 205ab14c..427e689c 100644 --- a/chains/solana/contracts/programs/ccip-router/src/context.rs +++ b/chains/solana/contracts/programs/ccip-router/src/context.rs @@ -1,14 +1,15 @@ use anchor_lang::{prelude::*, Ids}; -use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::associated_token::{get_associated_token_address_with_program_id, AssociatedToken}; +use anchor_spl::token::spl_token::native_mint; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use solana_program::sysvar::instructions; use crate::ocr3base::Ocr3Report; use crate::program::CcipRouter; -use crate::state::{ChainState, CommitReport, Config, ExternalExecutionConfig, Nonce}; +use crate::state::{CommitReport, Config, ExternalExecutionConfig, Nonce}; use crate::{ - BillingTokenConfig, BillingTokenConfigWrapper, CcipRouterError, ExecutionReportSingleChain, - ReportContext, + BillingTokenConfig, BillingTokenConfigWrapper, CcipRouterError, DestChain, + ExecutionReportSingleChain, GlobalState, ReportContext, Solana2AnyMessage, SourceChain, }; pub const ANCHOR_DISCRIMINATOR: usize = 8; @@ -31,10 +32,12 @@ pub fn uninitialized(v: u8) -> bool { } // Fixed seeds - different contexts must use different PDA seeds -pub const CHAIN_STATE_SEED: &[u8] = b"chain_state"; +pub const DEST_CHAIN_STATE_SEED: &[u8] = b"dest_chain_state"; +pub const SOURCE_CHAIN_STATE_SEED: &[u8] = b"source_chain_state"; pub const COMMIT_REPORT_SEED: &[u8] = b"commit_report"; pub const NONCE_SEED: &[u8] = b"nonce"; pub const CONFIG_SEED: &[u8] = b"config"; +pub const STATE_SEED: &[u8] = b"state"; pub const EXTERNAL_EXECUTION_CONFIG_SEED: &[u8] = b"external_execution_config"; // arbitrary messaging signer pub const EXTERNAL_TOKEN_POOL_SEED: &[u8] = b"external_token_pools_signer"; // token pool interaction signer pub const FEE_BILLING_SIGNER_SEEDS: &[u8] = b"fee_billing_signer"; // signer for billing fee token transfer @@ -112,51 +115,24 @@ impl MerkleRoot { } } -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct Solana2AnyMessage { - pub receiver: Vec, - pub data: Vec, - pub token_amounts: Vec, - pub fee_token: Pubkey, // pass zero address if native SOL - pub extra_args: ExtraArgsInput, - - // solana specific parameter for mapping tokens to set of accounts - pub token_indexes: Vec, -} - -#[derive(Clone, AnchorSerialize, AnchorDeserialize, Default)] -pub struct SolanaTokenAmount { - pub token: Pubkey, - pub amount: u64, // u64 - amount local to solana -} - -#[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize)] -pub struct ExtraArgsInput { - pub gas_limit: Option, - pub allow_out_of_order_execution: Option, -} - -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct Any2SolanaMessage { - pub message_id: [u8; 32], - pub source_chain_selector: u64, - pub sender: Vec, - pub data: Vec, - pub token_amounts: Vec, -} - #[derive(Accounts)] #[instruction(destination_chain_selector: u64, message: Solana2AnyMessage)] pub struct GetFee<'info> { #[account( - seeds = [CHAIN_STATE_SEED, destination_chain_selector.to_le_bytes().as_ref()], + seeds = [DEST_CHAIN_STATE_SEED, destination_chain_selector.to_le_bytes().as_ref()], bump, - constraint = valid_version(chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + constraint = valid_version(dest_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] - pub chain_state: Account<'info, ChainState>, + pub dest_chain_state: Account<'info, DestChain>, #[account( - seeds = [FEE_BILLING_TOKEN_CONFIG, message.fee_token.as_ref()], + 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>, @@ -172,6 +148,14 @@ pub struct InitializeCCIPRouter<'info> { space = ANCHOR_DISCRIMINATOR + Config::INIT_SPACE, )] pub config: AccountLoader<'info, Config>, + #[account( + init, + seeds = [STATE_SEED], + bump, + payer = authority, + space = ANCHOR_DISCRIMINATOR + GlobalState::INIT_SPACE, + )] + pub state: Account<'info, GlobalState>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, @@ -229,13 +213,22 @@ pub struct AcceptOwnership<'info> { pub struct AddChainSelector<'info> { #[account( init, - seeds = [CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], + seeds = [SOURCE_CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], bump, payer = authority, - space = ANCHOR_DISCRIMINATOR + ChainState::INIT_SPACE, - constraint = uninitialized(chain_state.version) @ CcipRouterError::InvalidInputs, // validate uninitialized + space = ANCHOR_DISCRIMINATOR + SourceChain::INIT_SPACE, + constraint = uninitialized(source_chain_state.version) @ CcipRouterError::InvalidInputs, // validate uninitialized )] - pub chain_state: Account<'info, ChainState>, + pub source_chain_state: Account<'info, SourceChain>, + #[account( + init, + seeds = [DEST_CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], + bump, + payer = authority, + space = ANCHOR_DISCRIMINATOR + DestChain::INIT_SPACE, + constraint = uninitialized(dest_chain_state.version) @ CcipRouterError::InvalidInputs, // validate uninitialized + )] + pub dest_chain_state: Account<'info, DestChain>, #[account( seeds = [CONFIG_SEED], bump, @@ -249,14 +242,34 @@ pub struct AddChainSelector<'info> { #[derive(Accounts)] #[instruction(new_chain_selector: u64)] -pub struct UpdateChainSelectorConfig<'info> { +pub struct UpdateSourceChainSelectorConfig<'info> { + #[account( + mut, + seeds = [SOURCE_CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], + bump, + constraint = valid_version(source_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + )] + pub source_chain_state: Account<'info, SourceChain>, + #[account( + seeds = [CONFIG_SEED], + bump, + constraint = valid_version(config.load()?.version, MAX_CONFIG_V) @ CcipRouterError::InvalidInputs, // validate state version + )] + pub config: AccountLoader<'info, Config>, + #[account(mut, address = config.load()?.owner @ CcipRouterError::Unauthorized)] + pub authority: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(new_chain_selector: u64)] +pub struct UpdateDestChainSelectorConfig<'info> { #[account( mut, - seeds = [CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], + seeds = [DEST_CHAIN_STATE_SEED, new_chain_selector.to_le_bytes().as_ref()], bump, - constraint = valid_version(chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + constraint = valid_version(dest_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] - pub chain_state: Account<'info, ChainState>, + pub dest_chain_state: Account<'info, DestChain>, #[account( seeds = [CONFIG_SEED], bump, @@ -304,6 +317,12 @@ pub struct SetOcrConfig<'info> { constraint = valid_version(config.load()?.version, MAX_CONFIG_V) @ CcipRouterError::InvalidInputs, // validate state version )] pub config: AccountLoader<'info, Config>, + #[account( + mut, + seeds = [STATE_SEED], + bump, + )] + pub state: Account<'info, GlobalState>, #[account(address = config.load()?.owner @ CcipRouterError::Unauthorized)] pub authority: Signer<'info>, } @@ -345,7 +364,7 @@ pub struct AddBillingTokenConfig<'info> { init, payer = authority, associated_token::mint = fee_token_mint, - associated_token::authority = fee_billing_signer, // use the signer account as the authority // TODO discuss? + associated_token::authority = fee_billing_signer, // use the signer account as the authority associated_token::token_program = token_program, )] pub fee_token_receiver: InterfaceAccount<'info, TokenAccount>, @@ -422,7 +441,7 @@ pub struct RemoveBillingTokenConfig<'info> { #[account( mut, associated_token::mint = fee_token_mint, - associated_token::authority = fee_billing_signer, // use the config account as the authority // TODO discuss? + associated_token::authority = fee_billing_signer, // use the signer account as the authority associated_token::token_program = token_program, constraint = fee_token_receiver.amount == 0 @ CcipRouterError::InvalidInputs, // ensure the account is empty // TODO improve error )] @@ -456,11 +475,11 @@ pub struct CcipSend<'info> { pub config: AccountLoader<'info, Config>, #[account( mut, - seeds = [CHAIN_STATE_SEED, destination_chain_selector.to_le_bytes().as_ref()], + seeds = [DEST_CHAIN_STATE_SEED, destination_chain_selector.to_le_bytes().as_ref()], bump, - constraint = valid_version(chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + constraint = valid_version(dest_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] - pub chain_state: Account<'info, ChainState>, + pub dest_chain_state: Account<'info, DestChain>, #[account( init_if_needed, seeds = [NONCE_SEED, destination_chain_selector.to_le_bytes().as_ref(), authority.key().as_ref()], @@ -488,35 +507,45 @@ pub struct CcipSend<'info> { #[account( owner = fee_token_program.key() @ CcipRouterError::InvalidInputs, - constraint = message.fee_token.key() == fee_token_mint.key() @ CcipRouterError::InvalidInputs, + constraint = (message.fee_token == Pubkey::default() && fee_token_mint.key() == native_mint::ID) + || message.fee_token.key() == fee_token_mint.key() @ CcipRouterError::InvalidInputs, )] - pub fee_token_mint: InterfaceAccount<'info, Mint>, + pub fee_token_mint: InterfaceAccount<'info, Mint>, // pass pre-2022 wSOL if using native SOL #[account( // `message.fee_token` would ideally be named `message.fee_mint` in Solana, // but using the `token` nomenclature is more compatible with EVM - seeds = [FEE_BILLING_TOKEN_CONFIG, message.fee_token.as_ref()], // the arg would ideally be named mint, but message.fee_token was set for EVM consistency + seeds = [FEE_BILLING_TOKEN_CONFIG, fee_token_mint.key().as_ref()], // the arg would ideally be named mint, but message.fee_token was set for EVM consistency bump, )] - pub fee_token_config: Account<'info, BillingTokenConfigWrapper>, // pass wSOL config if using native SOL + pub fee_token_config: Account<'info, BillingTokenConfigWrapper>, // pass pre-2022 wSOL config if using native SOL + /// CHECK this is the associated token account for the user paying the fee. + /// If paying with native SOL, this must be the zero address. #[account( - mut, - owner = fee_token_program.key() @ CcipRouterError::InvalidInputs, - constraint = fee_token_mint.key() == fee_token_user_associated_account.mint.key() @ CcipRouterError::InvalidInputs, - constraint = authority.key() == fee_token_user_associated_account.owner @ CcipRouterError::InvalidInputs, + // address must be either zero (paying with native SOL) or must be a WRITABLE associated token account + constraint = (message.fee_token == Pubkey::default() && fee_token_user_associated_account.key() == Pubkey::default()) + || fee_token_user_associated_account.is_writable @ CcipRouterError::InvalidInputsAtaWritable, + // address must be either zero (paying with native SOL) or + // the associated token account address for the caller and fee token used + constraint = (message.fee_token == Pubkey::default() && fee_token_user_associated_account.key() == Pubkey::default()) + || fee_token_user_associated_account.key() == get_associated_token_address_with_program_id( + &authority.key(), + &fee_token_mint.key(), + &fee_token_program.key(), + ) @ CcipRouterError::InvalidInputsAtaAddress, )] - pub fee_token_user_associated_account: InterfaceAccount<'info, TokenAccount>, // pass zero address is using native SOL // TODO check this is possible or alternatives + pub fee_token_user_associated_account: UncheckedAccount<'info>, // pass zero address is using native SOL #[account( mut, associated_token::mint = fee_token_mint, - associated_token::authority = fee_billing_signer, // use the config account as the authority // TODO discuss? + associated_token::authority = fee_billing_signer, // use the signer account as the authority associated_token::token_program = fee_token_program, )] - pub fee_token_receiver: InterfaceAccount<'info, TokenAccount>, // pass wSOL config if using native SOL + pub fee_token_receiver: InterfaceAccount<'info, TokenAccount>, // pass pre-2022 wSOL receiver if using native SOL - /// CHECK: This is the signer for the billing transfer CPI. // TODO improve comment + /// CHECK: This is the signer for the billing transfer CPI. #[account( seeds = [FEE_BILLING_SIGNER_SEEDS], bump @@ -554,7 +583,6 @@ pub struct CcipSend<'info> { #[instruction(report_context: ReportContext, report: CommitInput)] pub struct CommitReportContext<'info> { #[account( - mut, seeds = [CONFIG_SEED], bump, constraint = valid_version(config.load()?.version, MAX_CONFIG_V) @ CcipRouterError::InvalidInputs, // validate state version @@ -562,11 +590,11 @@ pub struct CommitReportContext<'info> { pub config: AccountLoader<'info, Config>, #[account( mut, - seeds = [CHAIN_STATE_SEED, report.merkle_root.source_chain_selector.to_le_bytes().as_ref()], + seeds = [SOURCE_CHAIN_STATE_SEED, report.merkle_root.source_chain_selector.to_le_bytes().as_ref()], bump, - constraint = valid_version(chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + constraint = valid_version(source_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] - pub chain_state: Account<'info, ChainState>, + pub source_chain_state: Account<'info, SourceChain>, #[account( init, seeds = [COMMIT_REPORT_SEED, report.merkle_root.source_chain_selector.to_le_bytes().as_ref(), report.merkle_root.merkle_root.as_ref()], @@ -583,6 +611,7 @@ pub struct CommitReportContext<'info> { #[account(address = instructions::ID @ CcipRouterError::InvalidInputs)] pub sysvar_instructions: UncheckedAccount<'info>, // remaining accounts + // global state account (to update the price sequence number) // [...billingTokenConfig accounts] // [...chainConfig accounts] } @@ -597,11 +626,11 @@ pub struct ExecuteReportContext<'info> { )] pub config: AccountLoader<'info, Config>, #[account( - seeds = [CHAIN_STATE_SEED, report.source_chain_selector.to_le_bytes().as_ref()], + seeds = [SOURCE_CHAIN_STATE_SEED, report.source_chain_selector.to_le_bytes().as_ref()], bump, - constraint = valid_version(chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version + constraint = valid_version(source_chain_state.version, MAX_CHAINSTATE_V) @ CcipRouterError::InvalidInputs, // validate state version )] - pub chain_state: Account<'info, ChainState>, + pub source_chain_state: Account<'info, SourceChain>, #[account( mut, seeds = [COMMIT_REPORT_SEED, report.source_chain_selector.to_le_bytes().as_ref(), report.root.as_ref()], 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 0a97c293..33a048ca 100644 --- a/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs +++ b/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs @@ -1,46 +1,114 @@ use anchor_lang::prelude::*; -use anchor_spl::token_interface; -use bytemuck::Zeroable; -use spl_token_2022::native_mint; -use token_interface::spl_token_2022; +use anchor_spl::{token::spl_token::native_mint, token_interface}; +use ethnum::U256; +use solana_program::{program::invoke_signed, system_instruction}; use crate::{ BillingTokenConfig, CcipRouterError, DestChain, Solana2AnyMessage, SolanaTokenAmount, - FEE_BILLING_SIGNER_SEEDS, + UnpackedDoubleU224, FEE_BILLING_SIGNER_SEEDS, }; // TODO change args and implement pub fn fee_for_msg( _dest_chain_selector: u64, - message: Solana2AnyMessage, + message: &Solana2AnyMessage, dest_chain: &DestChain, token_config: &BillingTokenConfig, ) -> Result { - let token = if message.fee_token == Pubkey::zeroed() { + // TODO: Add all validations from lib.rs over the message here as well + message.validate(dest_chain, token_config)?; + + let token = if message.fee_token == Pubkey::default() { native_mint::ID // Wrapped SOL } else { message.fee_token }; + let token_price = get_validated_token_price(token_config)?; + let _packed_gas_price = get_validated_gas_price(dest_chain)?; + + // TODO un-hardcode + let network_fee = U256::new(1); + let execution_cost = U256::new(1); + let data_availability_cost = U256::new(1); + + let amount = (network_fee + execution_cost + data_availability_cost) / token_price; + let amount: u64 = amount + .try_into() + .map_err(|_| CcipRouterError::InvalidTokenPrice)?; + + Ok(SolanaTokenAmount { amount, token }) +} + +pub struct PackedPrice { + pub execution_cost: u128, + pub gas_price: u128, +} + +impl From for PackedPrice { + fn from(value: UnpackedDoubleU224) -> Self { + Self { + execution_cost: value.low, + gas_price: value.high, + } + } +} + +fn get_validated_gas_price(dest_chain: &DestChain) -> Result { + let timestamp = dest_chain.state.usd_per_unit_gas.timestamp; + let price = dest_chain.state.usd_per_unit_gas.unpack().into(); + let threshold = dest_chain.config.gas_price_staleness_threshold as i64; + let elapsed_time = Clock::get()?.unix_timestamp - timestamp; + require!( - dest_chain.config.is_enabled, - CcipRouterError::DestinationChainDisabled + threshold == 0 || threshold > elapsed_time, + CcipRouterError::StaleGasPrice ); - require!(token_config.enabled, CcipRouterError::FeeTokenDisabled); - // TODO validate that dest_chain_selector is whitelisted and not cursed + Ok(price) +} - //? /Users/tobi/dev/work/ccip/contracts/src/v0.8/ccip/FeeQuoter.sol:518 - //? 1. Get dest chain config - //? 1. validate message - //? 1. retrieve fee token's current price - //? 1. retrieve gas price on the dest chain - //? 1. get cost of transfer _of each token in the message_ to the dest chain, including premium, gas, and bytes overhead - //? 1. get data availability cost on the dest chain - //? 1. Calculate and return the total fee +fn get_validated_token_price(token_config: &BillingTokenConfig) -> Result { + let timestamp = token_config.usd_per_token.timestamp; + let price = token_config.usd_per_token.as_single(); - // TODO un-hardcode - Ok(SolanaTokenAmount { amount: 1, token }) + // NOTE: There's no validation done with respect to token price staleness since data feeds are not + // supported in solana. Only the existence of `any` timestamp is checked, to ensure the price + // was set at least once. + require!( + price != 0 && timestamp != 0, + CcipRouterError::InvalidTokenPrice + ); + + Ok(price) +} + +pub fn wrap_native_sol<'info>( + token_program: &AccountInfo<'info>, + from: &mut Signer<'info>, + to: &mut InterfaceAccount<'info, token_interface::TokenAccount>, + amount: u64, + signer_bump: u8, +) -> Result<()> { + require!( + // guarantee that if caller is a PDA it won't get garbage-collected + *from.owner == System::id() || from.get_lamports() > amount, + CcipRouterError::InsufficientLamports + ); + + invoke_signed( + &system_instruction::transfer(&from.key(), &to.key(), amount), + &[from.to_account_info(), to.to_account_info()], + &[&[FEE_BILLING_SIGNER_SEEDS, &[signer_bump]]], + )?; + + let seeds = &[FEE_BILLING_SIGNER_SEEDS, &[signer_bump]]; + let signer_seeds = &[&seeds[..]]; + let account = to.to_account_info(); + let sync: anchor_spl::token_2022::SyncNative = anchor_spl::token_2022::SyncNative { account }; + let cpi_ctx = CpiContext::new_with_signer(token_program.to_account_info(), sync, signer_seeds); + + token_interface::sync_native(cpi_ctx) } pub fn transfer_fee<'info>( @@ -63,79 +131,73 @@ pub fn transfer_fee<'info>( #[cfg(test)] mod tests { + use solana_program::{ + entrypoint::SUCCESS, + program_stubs::{set_syscall_stubs, SyscallStubs}, + }; + use super::*; - use crate::DestChain; + use crate::tests::{sample_billing_config, sample_dest_chain, sample_message}; - #[test] - fn fees_not_returned_for_disabled_destination_chain() { - let mut chain = sample_dest_chain(); - chain.config.is_enabled = false; + struct TestStubs; - assert!(fee_for_msg(0, sample_message(), &chain, &sample_billing_config()).is_err()); + impl SyscallStubs for TestStubs { + fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { + // This causes the syscall to return a default-initialized + // clock when the build target is off-chain. Good enough for tests. + SUCCESS + } } #[test] - fn fees_not_returned_for_disabled_token() { - let mut billing_config = sample_billing_config(); - billing_config.enabled = false; - - assert!(fee_for_msg(0, sample_message(), &sample_dest_chain(), &billing_config).is_err()); + fn retrieving_fee_from_valid_message() { + set_syscall_stubs(Box::new(TestStubs)); + assert_eq!( + fee_for_msg( + 0, + &sample_message(), + &sample_dest_chain(), + &sample_billing_config(), + ) + .unwrap(), + SolanaTokenAmount { + token: native_mint::ID, + amount: 1 + } + ); } - fn sample_message() -> Solana2AnyMessage { - Solana2AnyMessage { - receiver: vec![], - data: vec![], - token_amounts: vec![], - fee_token: Pubkey::new_unique(), - extra_args: crate::ExtraArgsInput { - gas_limit: None, - allow_out_of_order_execution: None, - }, - token_indexes: vec![], - } + #[test] + fn fee_cannot_be_retrieved_when_token_price_is_not_timestamped() { + let mut billing_config = sample_billing_config(); + billing_config.usd_per_token.timestamp = 0; + assert_eq!( + fee_for_msg(0, &sample_message(), &sample_dest_chain(), &billing_config).unwrap_err(), + CcipRouterError::InvalidTokenPrice.into() + ); } - fn sample_billing_config() -> BillingTokenConfig { - BillingTokenConfig { - enabled: true, - mint: Pubkey::new_unique(), - usd_per_token: crate::TimestampedPackedU224 { - value: [0; 28], - timestamp: 0, - }, - premium_multiplier_wei_per_eth: 0, - } + #[test] + fn fee_cannot_be_retrieved_when_token_price_is_zero() { + let mut billing_config = sample_billing_config(); + billing_config.usd_per_token.value = [0u8; 28]; + assert_eq!( + fee_for_msg(0, &sample_message(), &sample_dest_chain(), &billing_config).unwrap_err(), + CcipRouterError::InvalidTokenPrice.into() + ); } - fn sample_dest_chain() -> DestChain { - DestChain { - state: crate::DestChainState { - sequence_number: 0, - usd_per_unit_gas: crate::TimestampedPackedU224 { - value: [0; 28], - timestamp: 0, - }, - }, - config: crate::DestChainConfig { - is_enabled: true, - max_number_of_tokens_per_msg: 0, - max_data_bytes: 0, - max_per_msg_gas_limit: 0, - dest_gas_overhead: 0, - 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, - default_token_dest_gas_overhead: 0, - default_tx_gas_limit: 0, - gas_multiplier_wei_per_eth: 0, - network_fee_usdcents: 0, - gas_price_staleness_threshold: 0, - enforce_out_of_order: false, - chain_family_selector: [0; 4], - }, - } + #[test] + fn fee_cannot_be_retrieved_when_gas_price_is_stale() { + // This will make the unix timestamp be zero, so we'll adjust + // the timestamp accordingly to a negative one. + set_syscall_stubs(Box::new(TestStubs)); + let mut chain = sample_dest_chain(); + chain.state.usd_per_unit_gas.timestamp = + -2 * chain.config.gas_price_staleness_threshold as i64; + assert_eq!( + fee_for_msg(0, &sample_message(), &chain, &sample_billing_config()).unwrap_err(), + CcipRouterError::StaleGasPrice.into() + ); } } diff --git a/chains/solana/contracts/programs/ccip-router/src/lib.rs b/chains/solana/contracts/programs/ccip-router/src/lib.rs index 8077d1f8..6f5484e0 100644 --- a/chains/solana/contracts/programs/ccip-router/src/lib.rs +++ b/chains/solana/contracts/programs/ccip-router/src/lib.rs @@ -3,6 +3,7 @@ use std::cell::Ref; use anchor_lang::error_code; use anchor_lang::prelude::*; use anchor_spl::token_interface; +use bytemuck::Zeroable; use solana_program::{instruction::Instruction, program::invoke_signed}; mod context; @@ -88,7 +89,7 @@ pub mod ccip_router { Ocr3Config::new(OcrPluginType::Execution as u8), ]; - config.latest_price_sequence_number = 0; + ctx.accounts.state.latest_price_sequence_number = 0; Ok(()) } @@ -147,18 +148,19 @@ pub mod ccip_router { source_chain_config: SourceChainConfig, dest_chain_config: DestChainConfig, ) -> Result<()> { - let chain_state = &mut ctx.accounts.chain_state; - chain_state.version = 1; - // Set source chain config & state + let source_chain_state = &mut ctx.accounts.source_chain_state; validate_source_chain_config(new_chain_selector, &source_chain_config)?; - chain_state.source_chain.config = source_chain_config.clone(); - chain_state.source_chain.state = SourceChainState { min_seq_nr: 1 }; + source_chain_state.version = 1; + source_chain_state.config = source_chain_config.clone(); + source_chain_state.state = SourceChainState { min_seq_nr: 1 }; // Set dest chain config & state + let dest_chain_state = &mut ctx.accounts.dest_chain_state; validate_dest_chain_config(new_chain_selector, &dest_chain_config)?; - chain_state.dest_chain.config = dest_chain_config.clone(); - chain_state.dest_chain.state = DestChainState { + dest_chain_state.version = 1; + dest_chain_state.config = dest_chain_config.clone(); + dest_chain_state.state = DestChainState { sequence_number: 0, usd_per_unit_gas: TimestampedPackedU224 { value: [0; 28], @@ -187,16 +189,16 @@ pub mod ccip_router { /// * `ctx` - The context containing the accounts required for disabling the chain selector. /// * `source_chain_selector` - The source chain selector to be disabled. pub fn disable_source_chain_selector( - ctx: Context, + ctx: Context, source_chain_selector: u64, ) -> Result<()> { - let chain_state = &mut ctx.accounts.chain_state; + let chain_state = &mut ctx.accounts.source_chain_state; - chain_state.source_chain.config.is_enabled = false; + chain_state.config.is_enabled = false; emit!(SourceChainConfigUpdated { source_chain_selector, - source_chain_config: chain_state.source_chain.config.clone(), + source_chain_config: chain_state.config.clone(), }); Ok(()) @@ -204,23 +206,23 @@ pub mod ccip_router { /// Disables the destination chain selector. /// - /// The Admin is the only one able to disable the chain selector as destination. This method is thought of as an emergency kill-switch. + /// The Admin is the only one able to disable the chain selector as destination. This method is thought of as an emergency kill-switch. /// /// # Arguments /// /// * `ctx` - The context containing the accounts required for disabling the chain selector. /// * `dest_chain_selector` - The destination chain selector to be disabled. pub fn disable_dest_chain_selector( - ctx: Context, + ctx: Context, dest_chain_selector: u64, ) -> Result<()> { - let chain_state = &mut ctx.accounts.chain_state; + let chain_state = &mut ctx.accounts.dest_chain_state; - chain_state.dest_chain.config.is_enabled = false; + chain_state.config.is_enabled = false; emit!(DestChainConfigUpdated { dest_chain_selector, - dest_chain_config: chain_state.dest_chain.config.clone(), + dest_chain_config: chain_state.config.clone(), }); Ok(()) @@ -236,13 +238,13 @@ pub mod ccip_router { /// * `source_chain_selector` - The source chain selector to be updated. /// * `source_chain_config` - The new configuration for the source chain. pub fn update_source_chain_config( - ctx: Context, + ctx: Context, source_chain_selector: u64, source_chain_config: SourceChainConfig, ) -> Result<()> { validate_source_chain_config(source_chain_selector, &source_chain_config)?; - ctx.accounts.chain_state.source_chain.config = source_chain_config.clone(); + ctx.accounts.source_chain_state.config = source_chain_config.clone(); emit!(SourceChainConfigUpdated { source_chain_selector, @@ -261,13 +263,13 @@ pub mod ccip_router { /// * `dest_chain_selector` - The destination chain selector to be updated. /// * `dest_chain_config` - The new configuration for the destination chain. pub fn update_dest_chain_config( - ctx: Context, + ctx: Context, dest_chain_selector: u64, dest_chain_config: DestChainConfig, ) -> Result<()> { validate_dest_chain_config(dest_chain_selector, &dest_chain_config)?; - ctx.accounts.chain_state.dest_chain.config = dest_chain_config.clone(); + ctx.accounts.dest_chain_state.config = dest_chain_config.clone(); emit!(DestChainConfigUpdated { dest_chain_selector, @@ -560,7 +562,7 @@ pub mod ccip_router { // When the OCR config changes, we reset the sequence number since it is scoped per config digest. // Note that s_minSeqNr/roots do not need to be reset as the roots persist // across reconfigurations and are de-duplicated separately. - config.latest_price_sequence_number = 0; + ctx.accounts.state.latest_price_sequence_number = 0; } Ok(()) @@ -660,8 +662,8 @@ pub mod ccip_router { ) -> Result { Ok(fee_for_msg( dest_chain_selector, - message, - &ctx.accounts.chain_state.dest_chain, + &message, + &ctx.accounts.dest_chain_state, &ctx.accounts.billing_token_config.config, )? .amount) @@ -686,75 +688,66 @@ pub mod ccip_router { dest_chain_selector: u64, message: Solana2AnyMessage, ) -> Result<()> { - // TODO: Limit send size data to 256 - - let fee = fee_for_msg( - dest_chain_selector, - message.clone(), - &ctx.accounts.chain_state.dest_chain, - &ctx.accounts.fee_token_config.config, - )?; - - let transfer = token_interface::TransferChecked { - from: ctx - .accounts - .fee_token_user_associated_account - .to_account_info(), - to: ctx.accounts.fee_token_receiver.to_account_info(), - mint: ctx.accounts.fee_token_mint.to_account_info(), - authority: ctx.accounts.fee_billing_signer.to_account_info(), - }; - - transfer_fee( - fee, - ctx.accounts.fee_token_program.to_account_info(), - transfer, - ctx.accounts.fee_token_mint.decimals, - ctx.bumps.fee_billing_signer, - )?; - - let nonce_counter_account = &mut ctx.accounts.nonce; - - // Avoid Re-initialization attack - if nonce_counter_account.version == 0 { - // The authority must be the owner of the PDA, as it's their Public Key in the seed - // If the account is not initialized, initialize it - nonce_counter_account.version = 1; - nonce_counter_account.counter = 0; - } - // The Config Account stores the default values for the Router, the Solana Chain Selector, the Default Gas Limit and the Default Allow Out Of Order Execution and Admin Ownership let config = ctx.accounts.config.load()?; - // The Chain State Account stores onramp and offramp information of other chains: - // - for the Destination Chain Selector: the latest sequence number sent from Solana to that Lane - // - for the Source Chain Selector: the latest sequence number received from that Lane to Solana - let dest_chain = &mut ctx.accounts.chain_state.dest_chain; + 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 is_paying_with_native_sol = message.fee_token == Pubkey::zeroed(); + if is_paying_with_native_sol { + wrap_native_sol( + &ctx.accounts.fee_token_program.to_account_info(), + &mut ctx.accounts.authority, + &mut ctx.accounts.fee_token_receiver, + fee.amount, + ctx.bumps.fee_billing_signer, + )?; + } else { + let transfer = token_interface::TransferChecked { + from: ctx + .accounts + .fee_token_user_associated_account + .to_account_info(), + to: ctx.accounts.fee_token_receiver.to_account_info(), + mint: ctx.accounts.fee_token_mint.to_account_info(), + authority: ctx.accounts.fee_billing_signer.to_account_info(), + }; + + transfer_fee( + fee, + ctx.accounts.fee_token_program.to_account_info(), + transfer, + ctx.accounts.fee_token_mint.decimals, + ctx.bumps.fee_billing_signer, + )?; + } let overflow_add = dest_chain.state.sequence_number.checked_add(1); - require!( overflow_add.is_some(), - CcipRouterError::ReachedMaxSequenceNumber // TODO: Can this really happen? Should we manage it differently? + CcipRouterError::ReachedMaxSequenceNumber ); - dest_chain.state.sequence_number = overflow_add.unwrap(); let sender = ctx.accounts.authority.key.to_owned(); + let receiver = message.receiver.clone(); let source_chain_selector = config.solana_chain_selector; - let final_extra_args = calculate_extra_args_from(config, message.extra_args); + let extra_args = extra_args_or_default(config, message.extra_args); - let mut final_nonce = 0; - if !final_extra_args.allow_out_of_order_execution { - let nonce = &mut ctx.accounts.nonce; - nonce.counter = nonce.counter.checked_add(1).unwrap(); - final_nonce = nonce.counter; - } + let nonce_counter_account: &mut Account<'info, Nonce> = &mut ctx.accounts.nonce; + let final_nonce = bump_nonce(nonce_counter_account, extra_args).unwrap(); let token_count = message.token_amounts.len(); + require!( + message.token_indexes.len() == token_count, + CcipRouterError::InvalidInputs, + ); + let mut new_message: Solana2AnyRampMessage = Solana2AnyRampMessage { sender, - receiver: message.receiver.clone(), + receiver, data: message.data, header: RampMessageHeader { message_id: [0; 32], @@ -763,15 +756,11 @@ pub mod ccip_router { sequence_number: dest_chain.state.sequence_number, nonce: final_nonce, }, - extra_args: final_extra_args, + extra_args, fee_token: message.fee_token, token_amounts: vec![Solana2AnyTokenTransfer::default(); token_count], }; - require!( - message.token_indexes.len() == message.token_amounts.len(), - CcipRouterError::InvalidInputs, - ); let seeds = &[EXTERNAL_TOKEN_POOL_SEED, &[ctx.bumps.token_pools_signer]]; for (i, token_amount) in message.token_amounts.iter().enumerate() { require!( @@ -779,27 +768,32 @@ pub mod ccip_router { 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 acc_list = &ctx.remaining_accounts[start..end]; - let accs = parse_token_accounts( + + let current_token_accounts = validate_and_parse_token_accounts( ctx.accounts.authority.key(), dest_chain_selector, ctx.program_id.key(), - acc_list, + &ctx.remaining_accounts[start..end], )?; + let router_token_pool_signer = &ctx.accounts.token_pools_signer; - // CPI: transfer token + amount from user to token pool + 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, - accs.token_program, - accs.mint, - accs.user_token_account, - accs.pool_token_account, + current_token_accounts.token_program, + current_token_accounts.mint, + current_token_accounts.user_token_account, + current_token_accounts.pool_token_account, router_token_pool_signer, seeds, )?; @@ -813,18 +807,20 @@ pub mod ccip_router { amount: token_amount.amount, local_token: token_amount.token, }; + let mut acc_infos = router_token_pool_signer.to_account_infos(); acc_infos.extend_from_slice(&[ - accs.pool_config.to_account_info(), - accs.token_program.to_account_info(), - accs.mint.to_account_info(), - accs.pool_signer.to_account_info(), - accs.pool_token_account.to_account_info(), - accs.pool_chain_config.to_account_info(), + current_token_accounts.pool_config.to_account_info(), + current_token_accounts.token_program.to_account_info(), + current_token_accounts.mint.to_account_info(), + current_token_accounts.pool_signer.to_account_info(), + current_token_accounts.pool_token_account.to_account_info(), + current_token_accounts.pool_chain_config.to_account_info(), ]); - acc_infos.extend_from_slice(accs.remaining_accounts); + acc_infos.extend_from_slice(current_token_accounts.remaining_accounts); + let return_data = interact_with_pool( - accs.pool_program.key(), + current_token_accounts.pool_program.key(), router_token_pool_signer.key(), acc_infos, lock_or_burn, @@ -833,11 +829,14 @@ pub mod ccip_router { let data = LockOrBurnOutV1::try_from_slice(&return_data)?; new_message.token_amounts[i] = Solana2AnyTokenTransfer { - source_pool_address: accs.pool_config.key(), + source_pool_address: current_token_accounts.pool_config.key(), dest_token_address: data.dest_token_address, extra_data: data.dest_pool_data, amount: u64_to_le_u256(token_amount.amount), // pool on receiver chain handles decimals - dest_exec_data: vec![0; 0], // TODO: part of fee quoter + dest_exec_data: vec![0; 0], // TODO: Implement this + // Destination chain specific execution data encoded in bytes + // for an EVM destination, it consists of the amount of gas available for the releaseOrMint + // and transfer calls made by the offRamp }; } } @@ -886,16 +885,76 @@ pub mod ccip_router { let report_context = ReportContext::from_byte_words(report_context_byte_words); // The Config Account stores the default values for the Router, the Solana Chain Selector, the Default Gas Limit and the Default Allow Out Of Order Execution and Admin Ownership - let mut config = ctx.accounts.config.load_mut()?; + let config = ctx.accounts.config.load()?; // Check if the report contains price updates - let has_token_price_updates = !report.price_updates.token_price_updates.is_empty(); - let has_gas_price_updates = !report.price_updates.gas_price_updates.is_empty(); - if has_token_price_updates || has_gas_price_updates { + let empty_token_price_updates = report.price_updates.token_price_updates.is_empty(); + let empty_gas_price_updates = report.price_updates.gas_price_updates.is_empty(); + + if empty_token_price_updates && empty_gas_price_updates { + // If the report does not contain any price updates, then there is nothing to update. + // Thus, as no price accounts have to be updated, the remaining accounts must be empty. + require_eq!( + ctx.remaining_accounts.len(), + 0, + CcipRouterError::InvalidInputs + ); + } else { + // There are price updates in the report. + // Remaining accounts represent: + // - The state account to store the price sequence updates + // - the accounts to update BillingTokenConfig for token prices + // - the accounts to update DestChain for gas prices + // They must be in order: + // 1. state_account + // 2. token_accounts[] + // 3. gas_accounts[] + // matching the order of the price updates in the CommitInput. + // They must also all be writable so they can be updated. + let minimum_remaining_accounts = 1 + + report.price_updates.token_price_updates.len() + + report.price_updates.gas_price_updates.len(); + require_eq!( + ctx.remaining_accounts.len(), + minimum_remaining_accounts, + CcipRouterError::InvalidInputs + ); + let ocr_sequence_number = report_context.sequence_number(); - if config.latest_price_sequence_number < ocr_sequence_number { + + // The Global state PDA is sent as a remaining_account as it is optional to avoid having the lock when not modifying it, so all validations need to be done manually + let (expected_state_key, _) = Pubkey::find_program_address(&[STATE_SEED], &crate::ID); + require_keys_eq!( + ctx.remaining_accounts[0].key(), + expected_state_key, + CcipRouterError::InvalidInputs + ); + require!( + ctx.remaining_accounts[0].is_writable, + CcipRouterError::InvalidInputs + ); + + let mut global_state: Account = + Account::try_from(&ctx.remaining_accounts[0])?; + + if global_state.latest_price_sequence_number < ocr_sequence_number { // Update the persisted sequence number - config.latest_price_sequence_number = ocr_sequence_number; + global_state.latest_price_sequence_number = ocr_sequence_number; + global_state.exit(&crate::ID)?; // as it is manually loaded, it also has to be manually written back + + // 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 report.price_updates.token_price_updates.iter().enumerate() { + apply_token_price_update(update, &ctx.remaining_accounts[i + 1])?; + } + + // Skip the first state account and the ones for token updates + let offset = report.price_updates.token_price_updates.len() + 1; + + // Do the same for gas price updates + for (i, update) in report.price_updates.gas_price_updates.iter().enumerate() { + apply_gas_price_update(update, &ctx.remaining_accounts[i + offset])?; + } } else { // TODO check if this is really necessary. EVM has this validation checking that the // array of merkle roots in the report is not empty. But here, considering we only have 1 root per report, @@ -906,40 +965,13 @@ pub mod ccip_router { CcipRouterError::StaleCommitReport ); } - - // Remaining account represent the accounts to update (BillingTokenConfig for token price & ChainState for gas price). - // They must be in order, first all token accounts, then all gas accounts, matching the order of the price updates in the CommitInput. - // They must also all be writable so they can be updated. - require!( - ctx.remaining_accounts.len() - >= report.price_updates.token_price_updates.len() - + report.price_updates.gas_price_updates.len(), - CcipRouterError::InvalidInputs - ); // TODO consider requiring exact length match, if this is the only source of dynamic account length - - // 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 report.price_updates.token_price_updates.iter().enumerate() { - apply_token_price_update(update, &ctx.remaining_accounts[i])?; - } - - let offset = report.price_updates.token_price_updates.len(); - - // Do the same for gas price updates - for (i, update) in report.price_updates.gas_price_updates.iter().enumerate() { - apply_gas_price_update( - update, - &ctx.remaining_accounts[i + offset], - &mut ctx.accounts.chain_state, - )?; - } } // The Config and State for the Source Chain, containing if it is enabled, the on ramp address and the min sequence number expected for future messages - let source_chain_state = &mut ctx.accounts.chain_state; + let source_chain_state = &mut ctx.accounts.source_chain_state; require!( - source_chain_state.source_chain.config.is_enabled, + source_chain_state.config.is_enabled, CcipRouterError::UnsupportedSourceChainSelector ); @@ -963,7 +995,7 @@ pub mod ccip_router { CcipRouterError::InvalidSequenceInterval ); // As we have 64 slots to store the execution state require!( - source_chain_state.source_chain.state.min_seq_nr == root.min_seq_nr, + source_chain_state.state.min_seq_nr == root.min_seq_nr, CcipRouterError::InvalidSequenceInterval ); require!(root.merkle_root != [0; 32], CcipRouterError::InvalidProof); @@ -979,7 +1011,7 @@ pub mod ccip_router { CcipRouterError::ReachedMaxSequenceNumber ); - source_chain_state.source_chain.state.min_seq_nr = next_seq_nr.unwrap(); + source_chain_state.state.min_seq_nr = next_seq_nr.unwrap(); let clock: Clock = Clock::get()?; commit_report.version = 1; @@ -1125,43 +1157,38 @@ fn apply_token_price_update<'info>( fn apply_gas_price_update<'info>( gas_update: &GasPriceUpdate, - chain_state_account_info: &'info AccountInfo<'info>, - current_chain_state: &mut Account, + dest_chain_state_account_info: &'info AccountInfo<'info>, ) -> Result<()> { let (expected, _) = Pubkey::find_program_address( &[ - CHAIN_STATE_SEED, + DEST_CHAIN_STATE_SEED, gas_update.dest_chain_selector.to_le_bytes().as_ref(), ], &crate::ID, ); require_keys_eq!( - chain_state_account_info.key(), + dest_chain_state_account_info.key(), expected, CcipRouterError::InvalidInputs ); require!( - chain_state_account_info.is_writable, + dest_chain_state_account_info.is_writable, CcipRouterError::InvalidInputs ); - if chain_state_account_info.key() == current_chain_state.key() { - // The passed-in chain_state account info via remaining_accounts may already be the same one as in the context. - // If that's the case, we have to just use the one in the context to avoid overwriting one with the other. - update_chain_state_gas_price(current_chain_state, gas_update)?; - } else { - // if updating a different chain's state, then Anchor won't automatically (de)serialize the account - // as it is not the one in the context, so we have to do it manually load it and write it back - let chain_state_account = &mut Account::try_from(chain_state_account_info)?; - update_chain_state_gas_price(chain_state_account, gas_update)?; - chain_state_account.exit(&crate::ID)?; - }; + // The passed-in chain_state account may refer to the same chain but it only corresponds to source. + // To update the price that values correspond to the destination, which is a different account. + // As the account is sent as additional accounts, then Anchor won't automatically (de)serialize the account + // as it is not the one in the context, so we have to do it manually load it and write it back + let dest_chain_state_account = &mut Account::try_from(dest_chain_state_account_info)?; + update_chain_state_gas_price(dest_chain_state_account, gas_update)?; + dest_chain_state_account.exit(&crate::ID)?; Ok(()) } fn update_chain_state_gas_price( - chain_state_account: &mut Account, + chain_state_account: &mut Account, gas_update: &GasPriceUpdate, ) -> Result<()> { require!( @@ -1169,19 +1196,15 @@ fn update_chain_state_gas_price( CcipRouterError::InvalidInputs ); - chain_state_account.dest_chain.state.usd_per_unit_gas = TimestampedPackedU224 { + chain_state_account.state.usd_per_unit_gas = TimestampedPackedU224 { value: gas_update.usd_per_unit_gas, timestamp: Clock::get()?.unix_timestamp, }; emit!(UsdPerUnitGasUpdated { dest_chain: gas_update.dest_chain_selector, - value: chain_state_account.dest_chain.state.usd_per_unit_gas.value, - timestamp: chain_state_account - .dest_chain - .state - .usd_per_unit_gas - .timestamp, + value: chain_state_account.state.usd_per_unit_gas.value, + timestamp: chain_state_account.state.usd_per_unit_gas.timestamp, }); Ok(()) @@ -1199,8 +1222,8 @@ fn internal_execute<'info>( let solana_chain_selector = config.solana_chain_selector; // The Config and State for the Source Chain, containing if it is enabled, the on ramp address and the min sequence number expected for future messages - let source_chain_state = &ctx.accounts.chain_state; - let on_ramp_address = &source_chain_state.source_chain.config.on_ramp; + let source_chain_state = &ctx.accounts.source_chain_state; + let on_ramp_address = &source_chain_state.config.on_ramp; // The Commit Report Account stores the information of 1 Commit Report: // - Merkle Root @@ -1251,7 +1274,7 @@ fn internal_execute<'info>( ctx.remaining_accounts.len(), )?; let acc_list = &ctx.remaining_accounts[start..end]; - let accs = parse_token_accounts( + let accs = validate_and_parse_token_accounts( execution_report.message.receiver, execution_report.message.header.source_chain_selector, ctx.program_id.key(), @@ -1455,11 +1478,8 @@ fn parse_messaging_accounts<'info>( Ok((msg_program, msg_accounts)) } -fn calculate_extra_args_from( - default_config: Ref, - extra_args: ExtraArgsInput, -) -> EvmExtraArgs { - let mut result_args = EvmExtraArgs { +fn extra_args_or_default(default_config: Ref, extra_args: ExtraArgsInput) -> AnyExtraArgs { + let mut result_args = AnyExtraArgs { gas_limit: default_config.default_gas_limit.to_owned(), allow_out_of_order_execution: default_config.default_allow_out_of_order_execution != 0, }; @@ -1516,7 +1536,6 @@ fn validate_dest_chain_config(dest_chain_selector: u64, config: &DestChainConfig } #[error_code] - pub enum CcipRouterError { #[msg("The given sequence interval is invalid")] InvalidSequenceInterval, @@ -1564,11 +1583,32 @@ pub enum CcipRouterError { DestinationChainDisabled, #[msg("Fee token disabled")] FeeTokenDisabled, + #[msg("Message exceeds maximum data size")] + MessageTooLarge, + #[msg("Message contains an unsupported number of tokens")] + UnsupportedNumberOfTokens, + #[msg("Chain family selector not supported")] + UnsupportedChainFamilySelector, + #[msg("Invalid EVM address")] + InvalidEVMAddress, + #[msg("Invalid encoding")] + InvalidEncoding, + #[msg("Invalid Associated Token Account address")] + InvalidInputsAtaAddress, + #[msg("Invalid Associated Token Account writable flag")] + InvalidInputsAtaWritable, + #[msg("Invalid token price")] + InvalidTokenPrice, + #[msg("Stale gas price")] + StaleGasPrice, + #[msg("Insufficient lamports")] + InsufficientLamports, } +// TODO: Refactor this to use the same structure as messages: execution_report.validate(..) pub fn validate_execution_report<'info>( execution_report: &ExecutionReportSingleChain, - source_chain_state: &Account<'info, ChainState>, + source_chain_state: &Account<'info, SourceChain>, commit_report: &Account<'info, CommitReport>, message_header: &RampMessageHeader, solana_chain_selector: u64, @@ -1579,7 +1619,7 @@ pub fn validate_execution_report<'info>( ); require!( - source_chain_state.source_chain.config.is_enabled, + source_chain_state.config.is_enabled, CcipRouterError::UnsupportedSourceChainSelector ); @@ -1618,3 +1658,22 @@ pub fn verify_merkle_root( ); Ok(hashed_leaf) } + +fn bump_nonce(nonce_counter_account: &mut Account, extra_args: AnyExtraArgs) -> Result { + // Avoid Re-initialization attack as the account is init_if_needed + // https://solana.com/developers/courses/program-security/reinitialization-attacks#add-is-initialized-check + if nonce_counter_account.version == 0 { + // The authority must be the owner of the PDA, as it's their Public Key in the seed + // If the account is not initialized, initialize it + nonce_counter_account.version = 1; + nonce_counter_account.counter = 0; + } + + // TODO: Require config.enforce_out_of_order => extra_args.allow_out_of_order_execution + let mut final_nonce = 0; + if !extra_args.allow_out_of_order_execution { + nonce_counter_account.counter = nonce_counter_account.counter.checked_add(1).unwrap(); + final_nonce = nonce_counter_account.counter; + } + Ok(final_nonce) +} diff --git a/chains/solana/contracts/programs/ccip-router/src/messages.rs b/chains/solana/contracts/programs/ccip-router/src/messages.rs index 42eb826d..7dd65f92 100644 --- a/chains/solana/contracts/programs/ccip-router/src/messages.rs +++ b/chains/solana/contracts/programs/ccip-router/src/messages.rs @@ -1,8 +1,16 @@ -use crate::{ReportContext, TOKENPOOL_RELEASE_OR_MINT_DISCRIMINATOR}; +use crate::{ + BillingTokenConfig, CcipRouterError, DestChain, ReportContext, + TOKENPOOL_RELEASE_OR_MINT_DISCRIMINATOR, +}; use anchor_lang::prelude::*; +use ethnum::U256; use crate::{ocr3base::Ocr3Report, ToTxData, TOKENPOOL_LOCK_OR_BURN_DISCRIMINATOR}; +pub const CHAIN_FAMILY_SELECTOR_EVM: u32 = 0x2812d52c; + +const U160_MAX: U256 = U256::from_words(u32::MAX as u128, u128::MAX); + #[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize)] // Family-agnostic header for OnRamp & OffRamp messages. // The messageId is not expected to match hash(message), since it may originate from another ramp family @@ -76,7 +84,7 @@ impl SolanaExtraArgs { } #[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize)] -pub struct EvmExtraArgs { +pub struct AnyExtraArgs { pub gas_limit: u128, pub allow_out_of_order_execution: bool, } @@ -161,13 +169,23 @@ pub struct Solana2AnyRampMessage { pub sender: Pubkey, // sender address on the source chain pub data: Vec, // arbitrary data payload supplied by the message sender pub receiver: Vec, // receiver address on the destination chain - pub extra_args: EvmExtraArgs, // destination-chain specific extra args, such as the gasLimit for EVM chains + pub extra_args: AnyExtraArgs, // destination-chain specific extra args, such as the gasLimit for EVM chains pub fee_token: Pubkey, pub token_amounts: Vec, } impl Solana2AnyRampMessage { pub fn hash(&self) -> [u8; 32] { + // TODO: Modify this hash to be similar to the one in EVM + // https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/libraries/Internal.sol#L129 + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // - metadata_hash = sha256("Solana2AnyMessageHashV1", solana_chain_selector, dest_chain_selector, ccip_router_program_id)) + // - first_part = sha256(sender, sequence_number, nonce, fee_token, fee_token_amount) + // - receiver + // - message.data + // - token_amounts + // - extra_args + use anchor_lang::solana_program::hash; // Push Data Size to ensure that the hash is unique @@ -297,11 +315,106 @@ pub struct ReleaseOrMintOutV1 { pub destination_amount: u64, // TODO: u256 on EVM? } -#[cfg(test)] -mod tests { +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct Solana2AnyMessage { + pub receiver: Vec, + pub data: Vec, + pub token_amounts: Vec, + pub fee_token: Pubkey, // pass zero address if native SOL + pub extra_args: ExtraArgsInput, + + // solana specific parameter for mapping tokens to set of accounts + pub token_indexes: Vec, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize, Default, Debug, PartialEq, Eq)] +pub struct SolanaTokenAmount { + pub token: Pubkey, + pub amount: u64, // u64 - amount local to solana +} + +#[derive(Clone, Copy, AnchorSerialize, AnchorDeserialize)] +pub struct ExtraArgsInput { + pub gas_limit: Option, + pub allow_out_of_order_execution: Option, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct Any2SolanaMessage { + pub message_id: [u8; 32], + pub source_chain_selector: u64, + pub sender: Vec, + pub data: Vec, + pub token_amounts: Vec, +} + +impl Solana2AnyMessage { + pub fn validate( + &self, + dest_chain: &DestChain, + token_config: &BillingTokenConfig, + ) -> Result<()> { + require!( + dest_chain.config.is_enabled, + CcipRouterError::DestinationChainDisabled + ); + + require!(token_config.enabled, CcipRouterError::FeeTokenDisabled); + + require_gte!( + dest_chain.config.max_data_bytes, + self.data.len() as u32, + CcipRouterError::MessageTooLarge + ); + + require_gte!( + dest_chain.config.max_number_of_tokens_per_msg as usize, + self.token_amounts.len(), + CcipRouterError::UnsupportedNumberOfTokens + ); + + self.validate_dest_family_address(dest_chain.config.chain_family_selector) + } + + pub fn validate_dest_family_address(&self, chain_family_selector: [u8; 4]) -> Result<()> { + const PRECOMPILE_SPACE: u32 = 1024; + + let selector = u32::from_be_bytes(chain_family_selector); + // Only EVM is supported as a destination family. + require_eq!( + selector, + CHAIN_FAMILY_SELECTOR_EVM, + CcipRouterError::UnsupportedChainFamilySelector + ); + + require_eq!(self.receiver.len(), 32, CcipRouterError::InvalidEVMAddress); + + let address: U256 = U256::from_be_bytes( + self.receiver + .clone() + .try_into() + .map_err(|_| CcipRouterError::InvalidEncoding)?, + ); + + require!(address <= U160_MAX, CcipRouterError::InvalidEVMAddress); + + if let Ok(small_address) = TryInto::::try_into(address) { + require_gte!( + small_address, + PRECOMPILE_SPACE, + CcipRouterError::InvalidEVMAddress + ) + }; + + Ok(()) + } +} +#[cfg(test)] +pub(crate) mod tests { use super::*; use anchor_lang::solana_program::pubkey::Pubkey; + use bytemuck::Zeroable; /// Builds a message and hash it, it's compared with a known hash #[test] @@ -343,4 +456,154 @@ mod tests { hex::encode(hash_result) ); } + + #[test] + fn message_not_validated_for_disabled_destination_chain() { + let mut chain = sample_dest_chain(); + chain.config.is_enabled = false; + + assert_eq!( + sample_message() + .validate(&chain, &sample_billing_config()) + .unwrap_err(), + CcipRouterError::DestinationChainDisabled.into() + ); + } + + #[test] + fn message_not_validated_for_disabled_token() { + let mut billing_config = sample_billing_config(); + billing_config.enabled = false; + + assert_eq!( + sample_message() + .validate(&sample_dest_chain(), &billing_config) + .unwrap_err(), + CcipRouterError::FeeTokenDisabled.into() + ); + } + + #[test] + fn large_message_fails_to_validate() { + let dest_chain = sample_dest_chain(); + let mut message = sample_message(); + message.data = vec![0; dest_chain.config.max_data_bytes as usize + 1]; + assert_eq!( + message + .validate(&sample_dest_chain(), &sample_billing_config()) + .unwrap_err(), + CcipRouterError::MessageTooLarge.into() + ); + } + + #[test] + fn invalid_addresses_fail_to_validate() { + let mut address_bigger_than_u160_max = vec![0u8; 32]; + address_bigger_than_u160_max[11] = 1; + let mut address_in_precompile_space = vec![0u8; 32]; + address_in_precompile_space[30] = 1; + let incorrect_length_address = vec![1u8, 12]; + + let invalid_addresses = [ + address_bigger_than_u160_max, + address_in_precompile_space, + incorrect_length_address, + ]; + + let mut message = sample_message(); + for address in invalid_addresses { + message.receiver = address; + assert_eq!( + message + .validate(&sample_dest_chain(), &sample_billing_config()) + .unwrap_err(), + CcipRouterError::InvalidEVMAddress.into() + ); + } + } + + #[test] + fn message_with_too_many_tokens_fails_to_validate() { + let dest_chain = sample_dest_chain(); + let mut message = sample_message(); + message.token_amounts = vec![ + SolanaTokenAmount { + token: Pubkey::new_unique(), + amount: 1 + }; + dest_chain.config.max_number_of_tokens_per_msg as usize + 1 + ]; + assert_eq!( + message + .validate(&sample_dest_chain(), &sample_billing_config()) + .unwrap_err(), + CcipRouterError::UnsupportedNumberOfTokens.into() + ); + } + + pub fn sample_message() -> Solana2AnyMessage { + let mut receiver = vec![0u8; 32]; + + // Arbitrary value that pushes the address to the right EVM range + // (above precompile space, under u160::max) + receiver[20] = 0xA; + + Solana2AnyMessage { + receiver, + data: vec![], + token_amounts: vec![], + fee_token: Pubkey::zeroed(), + extra_args: crate::ExtraArgsInput { + gas_limit: None, + allow_out_of_order_execution: None, + }, + token_indexes: vec![], + } + } + + pub fn sample_billing_config() -> BillingTokenConfig { + let mut value = [0; 28]; + value[27] = 3; + BillingTokenConfig { + enabled: true, + mint: Pubkey::new_unique(), + usd_per_token: crate::TimestampedPackedU224 { + value, + timestamp: 100, + }, + premium_multiplier_wei_per_eth: 0, + } + } + + pub fn sample_dest_chain() -> DestChain { + DestChain { + version: 1, + state: crate::DestChainState { + sequence_number: 0, + usd_per_unit_gas: crate::TimestampedPackedU224 { + value: [0; 28], + timestamp: 0, + }, + }, + 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_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, + default_token_dest_gas_overhead: 0, + default_tx_gas_limit: 0, + gas_multiplier_wei_per_eth: 0, + network_fee_usdcents: 0, + gas_price_staleness_threshold: 10, + enforce_out_of_order: false, + chain_family_selector: CHAIN_FAMILY_SELECTOR_EVM.to_be_bytes(), + }, + } + } } diff --git a/chains/solana/contracts/programs/ccip-router/src/pools.rs b/chains/solana/contracts/programs/ccip-router/src/pools.rs index 4bc61540..c15985c8 100644 --- a/chains/solana/contracts/programs/ccip-router/src/pools.rs +++ b/chains/solana/contracts/programs/ccip-router/src/pools.rs @@ -46,7 +46,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>, // TODO: enable with token billing + 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>, @@ -57,7 +57,7 @@ pub struct TokenAccounts<'a> { pub remaining_accounts: &'a [AccountInfo<'a>], } -pub fn parse_token_accounts<'info>( +pub fn validate_and_parse_token_accounts<'info>( user: Pubkey, chain_selector: u64, router: Pubkey, @@ -155,7 +155,7 @@ pub fn parse_token_accounts<'info>( CcipRouterError::InvalidInputsConfigAccounts ); - // Check Lookup Table Address + // Check Lookup Table Address configured in TokenAdminRegistry let token_admin_registry_account: Account = Account::try_from(token_admin_registry)?; require!( @@ -196,7 +196,7 @@ pub fn parse_token_accounts<'info>( Ok(TokenAccounts { user_token_account, - // token_billing_config, // TODO: enable with token billing + _token_billing_config: token_billing_config, pool_chain_config, pool_program, pool_config, diff --git a/chains/solana/contracts/programs/ccip-router/src/state.rs b/chains/solana/contracts/programs/ccip-router/src/state.rs index 6b8c1a73..c3b88efd 100644 --- a/chains/solana/contracts/programs/ccip-router/src/state.rs +++ b/chains/solana/contracts/programs/ccip-router/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use ethnum::U256; use crate::ocr3base::Ocr3Config; @@ -20,11 +21,16 @@ pub struct Config { _padding2: [u8; 8], pub ocr3: [Ocr3Config; 2], - // TODO: token pool global config // TODO: billing global configs' - _padding_before_billing: [u8; 8], +} + +#[account] +#[derive(InitSpace)] +pub struct GlobalState { + // This holds global variables for the contract that are not manually set by the admin. + // They are auto-updated by the contract during regular usage of CCIP. pub latest_price_sequence_number: u64, } @@ -40,9 +46,12 @@ pub struct SourceChainState { pub min_seq_nr: u64, // The min sequence number expected for future messages } -#[derive(Clone, AnchorSerialize, AnchorDeserialize, InitSpace, Debug)] +#[account] +#[derive(InitSpace, Debug)] pub struct SourceChain { - pub state: SourceChainState, // values that are updated automatically + // Config for Any2Solana + pub version: u8, + pub state: SourceChainState, // values that are updated automatically pub config: SourceChainConfig, // values configured by an admin } @@ -76,18 +85,13 @@ pub struct DestChainConfig { pub chain_family_selector: [u8; 4], // Selector that identifies the destination chain's family. Used to determine the correct validations to perform for the dest chain. } -#[derive(Clone, AnchorSerialize, AnchorDeserialize, InitSpace, Debug)] -pub struct DestChain { - pub state: DestChainState, // values that are updated automatically - pub config: DestChainConfig, // values configured by an admin -} - #[account] #[derive(InitSpace, Debug)] -pub struct ChainState { +pub struct DestChain { + // Config for Solana2Any pub version: u8, - pub source_chain: SourceChain, // Config for Any2Solana - pub dest_chain: DestChain, // Config for Solana2Any + pub state: DestChainState, // values that are updated automatically + pub config: DestChainConfig, // values configured by an admin } #[account] @@ -216,6 +220,29 @@ pub struct TimestampedPackedU224 { pub timestamp: i64, // maintaining the type that Solana returns for the time (solana_program::clock::UnixTimestamp = i64) } +impl TimestampedPackedU224 { + pub fn as_single(&self) -> U256 { + let mut u256_buffer = [0u8; 32]; + u256_buffer[4..32].clone_from_slice(&self.value); + U256::from_be_bytes(u256_buffer) + } + + pub fn unpack(&self) -> UnpackedDoubleU224 { + let mut u128_buffer = [0u8; 16]; + u128_buffer[2..16].clone_from_slice(&self.value[14..]); + let high = u128::from_be_bytes(u128_buffer); + u128_buffer[2..16].clone_from_slice(&self.value[..14]); + let low = u128::from_be_bytes(u128_buffer); + UnpackedDoubleU224 { high, low } + } +} + +#[derive(Debug, Clone)] +pub struct UnpackedDoubleU224 { + pub high: u128, + pub low: u128, +} + #[cfg(test)] mod tests { use super::*; diff --git a/chains/solana/contracts/programs/mcm/src/error.rs b/chains/solana/contracts/programs/mcm/src/error.rs index e951b54d..0df0249d 100644 --- a/chains/solana/contracts/programs/mcm/src/error.rs +++ b/chains/solana/contracts/programs/mcm/src/error.rs @@ -1,33 +1,39 @@ use anchor_lang::error_code; -// error range +// customizable error range // note: custom numeric error codes start from 6000 unless specified like #[error_code(offset = 1000)] // https://github.com/coral-xyz/anchor/blob/c25bd7b7ebbcaf12f6b8cbd3e6f34ae4e2833cb2/lang/syn/src/codegen/error.rs#L72 // Anchor built-in errors: https://anchor.so/errors // // [0:100] Global errors // [100:N] Function errors -// todo: anchor-go error generation support update? - -// todo: align message with EVM +// this "AuthError" is separated from the "McmError" for error type generation from "anchor-go" tool +// Known issue: only the first error_code block is included in idl.errors field, and go bindings for this first errors not generated. +// anchor-go generates types for error from the second error_code block onwards. +// This might be a bug in anchor-go, should be revisited once program functionality is stable. +// Workaround: keep errors that not likely to change during development in the first error_code block(keeping hardcoded error types for this), +// and other errors in the second block. #[error_code] -pub enum McmError { - #[msg("Invalid multisig")] - WrongMultiSig = 0, // 6000 - - #[msg("Invalid chainID")] - WrongChainId, - +pub enum AuthError { #[msg("The signer is unauthorized")] Unauthorized, +} +#[error_code] +pub enum McmError { #[msg("Invalid inputs")] InvalidInputs, #[msg("overflow occurred.")] Overflow, + #[msg("Invalid multisig")] + WrongMultiSig, + + #[msg("Invalid chainID")] + WrongChainId, + #[msg("Invalid signature")] InvalidSignature, diff --git a/chains/solana/contracts/programs/mcm/src/event.rs b/chains/solana/contracts/programs/mcm/src/event.rs index 8779f54e..07871b08 100644 --- a/chains/solana/contracts/programs/mcm/src/event.rs +++ b/chains/solana/contracts/programs/mcm/src/event.rs @@ -19,11 +19,11 @@ pub struct NewRoot { #[event] /// @dev Emitted when a new config is set. pub struct ConfigSet { - // todo: emitting all signers causes a memory overflow, need to find a way to emit them - // pub signers: Vec, pub group_parents: [u8; NUM_GROUPS], pub group_quorums: [u8; NUM_GROUPS], pub is_root_cleared: bool, + // todo: emitting all signers causes a memory overflow, need to find a way to emit them + // pub signers: Vec, } #[event] diff --git a/chains/solana/contracts/programs/mcm/src/instructions/set_config.rs b/chains/solana/contracts/programs/mcm/src/instructions/set_config.rs index fa4d62b3..1d6d7fe4 100644 --- a/chains/solana/contracts/programs/mcm/src/instructions/set_config.rs +++ b/chains/solana/contracts/programs/mcm/src/instructions/set_config.rs @@ -111,11 +111,11 @@ pub fn set_config( config.group_parents = group_parents; emit!(ConfigSet { - // todo: memory inefficient, finding workaround - // signers: config.signers.clone(), group_parents, group_quorums, is_root_cleared: clear_root, + // todo: memory inefficient, finding workaround + // signers: config.signers.clone(), }); Ok(()) @@ -130,9 +130,8 @@ pub fn init_signers( total_signers > 0 && total_signers <= MAX_NUM_SIGNERS as u8, McmError::OutOfBoundsNumOfSigners ); - let evm_signers_acc = &mut ctx.accounts.config_signers; - evm_signers_acc.bump = ctx.bumps.config_signers; - evm_signers_acc.total_signers = total_signers; + let config_signers = &mut ctx.accounts.config_signers; + config_signers.total_signers = total_signers; // Note: is_finalized stays false until finalization Ok(()) @@ -143,17 +142,17 @@ pub fn append_signers( _multisig_name: [u8; MULTISIG_NAME_PADDED], signers_batch: Vec<[u8; 20]>, ) -> Result<()> { - let evm_signers_acc = &mut ctx.accounts.config_signers; + let config_signers = &mut ctx.accounts.config_signers; // check bounds require!( - evm_signers_acc.signer_addresses.len() + signers_batch.len() - <= evm_signers_acc.total_signers as usize, + config_signers.signer_addresses.len() + signers_batch.len() + <= config_signers.total_signers as usize, McmError::OutOfBoundsNumOfSigners ); // check if the signers are strictly increasing from the last signer - let mut prev_signer = evm_signers_acc + let mut prev_signer = config_signers .signer_addresses .last() .copied() @@ -165,17 +164,17 @@ pub fn append_signers( McmError::SignersAddressesMustBeStrictlyIncreasing ); prev_signer = sig; - evm_signers_acc.signer_addresses.push(sig); + config_signers.signer_addresses.push(sig); } Ok(()) } pub fn clear_signers( - ctx: Context, + _ctx: Context, _multisig_name: [u8; MULTISIG_NAME_PADDED], ) -> Result<()> { - let config_signers = &mut ctx.accounts.config_signers; - config_signers.signer_addresses.clear(); + // NOTE: ctx.accounts.config_signers is closed to be able to re-initialized, + // also allow finalized config_signers to be cleared Ok(()) } @@ -183,15 +182,15 @@ pub fn finalize_signers( ctx: Context, _multisig_name: [u8; MULTISIG_NAME_PADDED], ) -> Result<()> { - let evm_signers_acc = &mut ctx.accounts.config_signers; + let config_signers = &mut ctx.accounts.config_signers; require!( - !evm_signers_acc.signer_addresses.is_empty() - && evm_signers_acc.signer_addresses.len() == evm_signers_acc.total_signers as usize, + !config_signers.signer_addresses.is_empty() + && config_signers.signer_addresses.len() == config_signers.total_signers as usize, McmError::OutOfBoundsNumOfSigners ); - evm_signers_acc.is_finalized = true; + config_signers.is_finalized = true; Ok(()) } @@ -219,7 +218,7 @@ pub struct SetConfig<'info> { )] pub config_signers: Account<'info, ConfigSigners>, // preloaded signers account - #[account(mut, address = multisig_config.owner @ McmError::Unauthorized)] + #[account(mut, address = multisig_config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, @@ -240,7 +239,7 @@ pub struct InitSigners<'info> { )] pub config_signers: Account<'info, ConfigSigners>, - #[account(mut, address = multisig_config.owner @ McmError::Unauthorized)] + #[account(mut, address = multisig_config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, @@ -260,7 +259,7 @@ pub struct AppendSigners<'info> { )] pub config_signers: Account<'info, ConfigSigners>, - #[account(mut, address = multisig_config.owner @ McmError::Unauthorized)] + #[account(mut, address = multisig_config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } @@ -274,11 +273,11 @@ pub struct ClearSigners<'info> { mut, seeds = [CONFIG_SIGNERS_SEED, multisig_name.as_ref()], bump, - constraint = !config_signers.is_finalized @ McmError::SignersNotFinalized, + close = authority // close so that it can be re-initialized )] pub config_signers: Account<'info, ConfigSigners>, - #[account(mut, address = multisig_config.owner @ McmError::Unauthorized)] + #[account(mut, address = multisig_config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } @@ -296,6 +295,6 @@ pub struct FinalizeSigners<'info> { )] pub config_signers: Account<'info, ConfigSigners>, - #[account(mut, address = multisig_config.owner @ McmError::Unauthorized)] + #[account(mut, address = multisig_config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } diff --git a/chains/solana/contracts/programs/mcm/src/instructions/set_root.rs b/chains/solana/contracts/programs/mcm/src/instructions/set_root.rs index c02150ee..999c4639 100644 --- a/chains/solana/contracts/programs/mcm/src/instructions/set_root.rs +++ b/chains/solana/contracts/programs/mcm/src/instructions/set_root.rs @@ -21,7 +21,12 @@ pub fn set_root( ); // verify ECDSA signatures on (root, validUntil) and ensure that the root group is successful - let verified = verify_ecdsa_signatures(&ctx, &root, valid_until); + let verified = verify_ecdsa_signatures( + &ctx.accounts.root_signatures.signatures, + &ctx.accounts.multisig_config, + &root, + valid_until, + ); #[allow(clippy::unnecessary_unwrap)] if verified.is_err() { return Err(verified.unwrap_err()); @@ -32,8 +37,8 @@ pub fn set_root( McmError::ValidUntilHasAlreadyPassed ); + // verify metadataProof { - // verify metadataProof let calculated_root = calculate_merkle_root(metadata_proof.clone(), &metadata.hash_leaf()); require!(root == calculated_root, McmError::ProofCannotBeVerified); } @@ -109,16 +114,14 @@ pub fn set_root( } fn verify_ecdsa_signatures( - ctx: &Context, + signatures: &Vec, + multisig_config: &MultisigConfig, root: &[u8; 32], valid_until: u32, ) -> Result<()> { let signed_hash = compute_eth_message_hash(root, valid_until); - // get preloaded signatures - let signatures = &ctx.accounts.root_signatures.signatures; let mut previous_addr: [u8; EVM_ADDRESS_BYTES] = [0; EVM_ADDRESS_BYTES]; let mut group_vote_counts: [u8; NUM_GROUPS] = [0; NUM_GROUPS]; - let multisig_config = &ctx.accounts.multisig_config; for sig in signatures { let signer_addr = ecdsa_recover_evm_addr(&signed_hash.to_bytes(), sig); @@ -166,7 +169,7 @@ fn verify_ecdsa_signatures( McmError::MissingConfig ); - // did the root group reach its quorum? + // check if the root group reach its quorum require!( group_vote_counts[0] >= multisig_config.group_quorums[0], McmError::InsufficientSigners @@ -183,7 +186,6 @@ pub fn init_signatures( total_signatures: u8, ) -> Result<()> { let signatures_account = &mut ctx.accounts.signatures; - signatures_account.bump = ctx.bumps.signatures; signatures_account.total_signatures = total_signatures; // Note: is_finalized stays false until finalization @@ -212,13 +214,13 @@ pub fn append_signatures( } pub fn clear_signatures( - ctx: Context, + _ctx: Context, _multisig_name: [u8; MULTISIG_NAME_PADDED], _root: [u8; 32], _valid_until: u32, ) -> Result<()> { - let signatures_account = &mut ctx.accounts.signatures; - signatures_account.signatures.clear(); + // NOTE: ctx.accounts.signatures is closed to be able to re-initialized, + // also allow finalized signatures_account to be cleared Ok(()) } @@ -321,7 +323,7 @@ pub struct ClearSignatures<'info> { mut, seeds = [ROOT_SIGNATURES_SEED, multisig_name.as_ref(), root.as_ref(), valid_until.to_le_bytes().as_ref()], bump, - constraint = !signatures.is_finalized @ McmError::SignaturesAlreadyFinalized, + close = authority // close so that it can be re-initialized )] pub signatures: Account<'info, RootSignatures>, diff --git a/chains/solana/contracts/programs/mcm/src/lib.rs b/chains/solana/contracts/programs/mcm/src/lib.rs index 78a42741..cbea8517 100644 --- a/chains/solana/contracts/programs/mcm/src/lib.rs +++ b/chains/solana/contracts/programs/mcm/src/lib.rs @@ -39,13 +39,6 @@ pub mod mcm { config.chain_id = chain_id; config.multisig_name = multisig_name; config.owner = ctx.accounts.authority.key(); - - // convert bytes to string for logging - let name_str = String::from_utf8_lossy(&multisig_name) - .trim_end_matches(char::from(0)) - .to_string(); - - msg!("Initialized MCM {} with chain_id {}", name_str, chain_id); Ok(()) } @@ -208,7 +201,7 @@ pub struct Initialize<'info> { #[account(constraint = program.programdata_address()? == Some(program_data.key()))] pub program: Program<'info, Mcm>, // initialization only allowed by program upgrade authority(in common cases, the initial deployer) - #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ McmError::Unauthorized)] + #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ AuthError::Unauthorized)] pub program_data: Account<'info, ProgramData>, #[account( @@ -236,7 +229,7 @@ pub struct TransferOwnership<'info> { #[account(mut, seeds = [CONFIG_SEED, multisig_name.as_ref()], bump)] pub config: Account<'info, config::MultisigConfig>, - #[account(address = config.owner @ McmError::Unauthorized)] + #[account(address = config.owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } @@ -246,6 +239,6 @@ pub struct AcceptOwnership<'info> { #[account(mut, seeds = [CONFIG_SEED, multisig_name.as_ref()], bump)] pub config: Account<'info, config::MultisigConfig>, - #[account(address = config.proposed_owner @ McmError::Unauthorized)] + #[account(address = config.proposed_owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } diff --git a/chains/solana/contracts/programs/mcm/src/state/config.rs b/chains/solana/contracts/programs/mcm/src/state/config.rs index 5c961957..8a6bac20 100644 --- a/chains/solana/contracts/programs/mcm/src/state/config.rs +++ b/chains/solana/contracts/programs/mcm/src/state/config.rs @@ -7,13 +7,12 @@ pub struct ConfigSigners { pub signer_addresses: Vec<[u8; 20]>, pub total_signers: u8, pub is_finalized: bool, - pub bump: u8, } impl ConfigSigners { - // 8 (discriminator) + 4 (vec len) + (20 * total_signers) +1(total_signers) + 1 (is_finalized) + 1 (bump) + // 8 (discriminator) + 4 (vec len) + (20 * total_signers) +1(total_signers) + 1 (is_finalized) pub const fn space(total_signers: usize) -> usize { - 8 + 4 + (20 * total_signers) + 1 + 1 + 1 + 8 + 4 + (20 * total_signers) + 1 + 1 } } @@ -37,7 +36,7 @@ pub struct MultisigConfig { // Keep variable-length data at the end of the account struct // https://solana.com/developers/courses/program-optimization/program-architecture#data-order - pub signers: Vec, // unable to store as hashmap in Solana + pub signers: Vec, } impl MultisigConfig { diff --git a/chains/solana/contracts/programs/mcm/src/state/root.rs b/chains/solana/contracts/programs/mcm/src/state/root.rs index 64addc88..2859df5f 100644 --- a/chains/solana/contracts/programs/mcm/src/state/root.rs +++ b/chains/solana/contracts/programs/mcm/src/state/root.rs @@ -7,13 +7,12 @@ pub struct RootSignatures { pub total_signatures: u8, pub signatures: Vec, pub is_finalized: bool, - pub bump: u8, } impl RootSignatures { - // 8 (discriminator) + 4 (vec len) + (65 * max_sigs) + 32 (root) + 4 (valid_until) + 1 (is_finalized) + 1 (bump) + // 8 (discriminator) + 4 (vec len) + (65 * max_sigs) + 32 (root) + 4 (valid_until) + 1 (is_finalized) pub const fn space(total_signatures: usize) -> usize { - 8 + 4 + (65 * total_signatures) + 32 + 4 + 1 + 1 + 1 + 8 + 4 + (65 * total_signatures) + 32 + 4 + 1 + 1 } } diff --git a/chains/solana/contracts/programs/timelock/Cargo.toml b/chains/solana/contracts/programs/timelock/Cargo.toml index d73caf86..c7c6f035 100644 --- a/chains/solana/contracts/programs/timelock/Cargo.toml +++ b/chains/solana/contracts/programs/timelock/Cargo.toml @@ -19,6 +19,8 @@ default = [] anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } access-controller = { version = "1.0.1", path = "../access-controller", default-features = false, features = ["cpi"] } bytemuck = "1.7" +static_assertions = "1.1.0" +arrayvec = { version = "1.0.0", path = "../../crates/arrayvec" } [dev-dependencies] hex = "0.4.3" diff --git a/chains/solana/contracts/programs/timelock/src/access.rs b/chains/solana/contracts/programs/timelock/src/access.rs index 576240da..2f24783a 100644 --- a/chains/solana/contracts/programs/timelock/src/access.rs +++ b/chains/solana/contracts/programs/timelock/src/access.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use access_controller::AccessController; -use crate::error::TimelockError; +use crate::error::{AuthError, TimelockError}; use crate::state::{Config, Role}; // NOTE: This macro is used to check if the authority is the owner @@ -40,7 +40,7 @@ pub fn only_role_or_admin_role( // check if the authority has access to the role require!( access_controller::has_access(role_controller, &authority.key())?, - TimelockError::Unauthorized + AuthError::Unauthorized ); Ok(()) @@ -56,6 +56,6 @@ macro_rules! only_admin { } pub fn only_admin(config: &Account, authority: &Signer) -> Result<()> { - require_keys_eq!(authority.key(), config.owner, TimelockError::Unauthorized); + require_keys_eq!(authority.key(), config.owner, AuthError::Unauthorized); Ok(()) } diff --git a/chains/solana/contracts/programs/timelock/src/constants.rs b/chains/solana/contracts/programs/timelock/src/constants.rs index 256f9cf2..63496ffe 100644 --- a/chains/solana/contracts/programs/timelock/src/constants.rs +++ b/chains/solana/contracts/programs/timelock/src/constants.rs @@ -1,8 +1,11 @@ +/// PDA seeds pub const TIMELOCK_CONFIG_SEED: &[u8] = b"timelock_config"; pub const TIMELOCK_OPERATION_SEED: &[u8] = b"timelock_operation"; pub const TIMELOCK_SIGNER_SEED: &[u8] = b"timelock_signer"; pub const TIMELOCK_BLOCKED_FUNCITON_SELECTOR_SEED: &[u8] = b"timelock_blocked_function_selector"; +/// constants pub const ANCHOR_DISCRIMINATOR: usize = 8; pub const DONE_TIMESTAMP: u64 = 1; pub const EMPTY_PREDECESSOR: [u8; 32] = [0; 32]; +pub const MAX_SELECTORS: usize = 32; // todo: temp arbitary max, tested with max 128 diff --git a/chains/solana/contracts/programs/timelock/src/error.rs b/chains/solana/contracts/programs/timelock/src/error.rs index aa585a4b..852b9204 100644 --- a/chains/solana/contracts/programs/timelock/src/error.rs +++ b/chains/solana/contracts/programs/timelock/src/error.rs @@ -1,16 +1,19 @@ use anchor_lang::error_code; +// this "AuthError" is separated from the "TimelockError" for error type generation from "anchor-go" tool +// Known issue: only the first error_code block is included in idl.errors field, and go bindings for this first errors not generated. +// anchor-go generates types for error from the second error_code block onwards. +// This might be a bug in anchor-go, should be revisited once program functionality is stable. +// Workaround: keep errors that not likely to change during development in the first error_code block(keeping hardcoded error types for this), +// and other errors in the second block. #[error_code] -pub enum PlaceholderError { - #[msg("Todo generate error type with anchor")] - Placeholder, +pub enum AuthError { + #[msg("The signer is unauthorized")] + Unauthorized = 0, } #[error_code] pub enum TimelockError { - #[msg("The signer is unauthorized")] - Unauthorized = 0, - #[msg("Invalid inputs")] InvalidInput, @@ -46,9 +49,21 @@ pub enum TimelockError { #[msg("Predecessor operation is not found")] MissingDependency, + #[msg("RBACTimelock: Provided access controller is invalid")] + InvalidAccessController, + #[msg("RBACTimelock: selector is blocked")] BlockedSelector, - #[msg("RBACTimelock: Provided access controller is invalid")] - InvalidAccessController, + #[msg("RBACTimelock: selector is already blocked")] + AlreadyBlocked, + + #[msg("RBACTimelock: selector not found")] + SelectorNotFound, + + #[msg("RBACTimelock: invalid instruction data")] + InvalidInstructionData, // todo: update this with solid fn blocker policy + + #[msg("RBACTimelock: maximum capacity reached for function blocker")] + MaxCapacityReached, } diff --git a/chains/solana/contracts/programs/timelock/src/event.rs b/chains/solana/contracts/programs/timelock/src/event.rs index 0736e08f..7cb9378b 100644 --- a/chains/solana/contracts/programs/timelock/src/event.rs +++ b/chains/solana/contracts/programs/timelock/src/event.rs @@ -6,10 +6,10 @@ pub struct CallScheduled { pub id: [u8; 32], pub index: u64, pub target: Pubkey, - pub data: Vec, pub predecessor: [u8; 32], pub salt: [u8; 32], pub delay: u64, + pub data: Vec, } #[event] diff --git a/chains/solana/contracts/programs/timelock/src/instructions/cancel.rs b/chains/solana/contracts/programs/timelock/src/instructions/cancel.rs index 5952b1bf..e967b2ca 100644 --- a/chains/solana/contracts/programs/timelock/src/instructions/cancel.rs +++ b/chains/solana/contracts/programs/timelock/src/instructions/cancel.rs @@ -17,9 +17,6 @@ pub fn cancel<'info>(_ctx: Context<'_, '_, '_, 'info, Cancel<'info>>, id: [u8; 3 #[derive(Accounts)] #[instruction(id: [u8; 32])] pub struct Cancel<'info> { - #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] - pub config: Account<'info, Config>, - #[account( mut, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], @@ -29,6 +26,10 @@ pub struct Cancel<'info> { constraint = operation.is_pending() @ TimelockError::OperationNotCancellable, )] pub operation: Account<'info, Operation>, + + #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + // NOTE: access controller check happens in only_role_or_admin_role macro pub role_access_controller: AccountLoader<'info, AccessController>, diff --git a/chains/solana/contracts/programs/timelock/src/instructions/execute.rs b/chains/solana/contracts/programs/timelock/src/instructions/execute.rs index 5707e62e..1a486db2 100644 --- a/chains/solana/contracts/programs/timelock/src/instructions/execute.rs +++ b/chains/solana/contracts/programs/timelock/src/instructions/execute.rs @@ -129,16 +129,6 @@ fn execute( #[derive(Accounts)] #[instruction(id: [u8; 32])] pub struct ExecuteBatch<'info> { - #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] - pub config: Account<'info, Config>, - - /// CHECK: program signer PDA that can hold balance - #[account( - seeds = [TIMELOCK_SIGNER_SEED], - bump - )] - pub timelock_signer: UncheckedAccount<'info>, - #[account( mut, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], @@ -149,6 +139,17 @@ pub struct ExecuteBatch<'info> { /// CHECK: Will be validated in handler if predecessor exists pub predecessor_operation: UncheckedAccount<'info>, + + #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + + /// CHECK: program signer PDA that can hold balance + #[account( + seeds = [TIMELOCK_SIGNER_SEED], + bump + )] + pub timelock_signer: UncheckedAccount<'info>, + // NOTE: access controller check happens in only_role_or_admin_role macro pub role_access_controller: AccountLoader<'info, AccessController>, @@ -159,6 +160,14 @@ pub struct ExecuteBatch<'info> { #[derive(Accounts)] #[instruction(id: [u8; 32])] pub struct BypasserExecuteBatch<'info> { + #[account( + mut, + seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], + bump, + constraint = operation.is_finalized @ TimelockError::OperationNotFinalized, + )] + pub operation: Account<'info, Operation>, + #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] pub config: Account<'info, Config>, @@ -169,13 +178,6 @@ pub struct BypasserExecuteBatch<'info> { )] pub timelock_signer: UncheckedAccount<'info>, - #[account( - mut, - seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], - bump, - constraint = operation.is_finalized @ TimelockError::OperationNotFinalized, - )] - pub operation: Account<'info, Operation>, // NOTE: access controller check happens in only_role_or_admin_role macro pub role_access_controller: AccountLoader<'info, AccessController>, diff --git a/chains/solana/contracts/programs/timelock/src/instructions/initialize.rs b/chains/solana/contracts/programs/timelock/src/instructions/initialize.rs index d440bb0a..4aabf199 100644 --- a/chains/solana/contracts/programs/timelock/src/instructions/initialize.rs +++ b/chains/solana/contracts/programs/timelock/src/instructions/initialize.rs @@ -3,9 +3,10 @@ use anchor_lang::prelude::*; use access_controller::AccessController; use crate::constants::{ANCHOR_DISCRIMINATOR, TIMELOCK_CONFIG_SEED}; -use crate::error::TimelockError; +use crate::error::AuthError; use crate::program::Timelock; use crate::state::{Config, Role}; +use crate::TimelockError; /// initialize Timelock config with owner(admin), /// role access controller keys and global configuration value. @@ -69,7 +70,7 @@ pub struct Initialize<'info> { #[account(constraint = program.programdata_address()? == Some(program_data.key()))] pub program: Program<'info, Timelock>, // NOTE: initialization only allowed by program upgrade authority - #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ TimelockError::Unauthorized)] + #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ AuthError::Unauthorized)] pub program_data: Account<'info, ProgramData>, // access controller program and states per role diff --git a/chains/solana/contracts/programs/timelock/src/instructions/schedule.rs b/chains/solana/contracts/programs/timelock/src/instructions/schedule.rs index df8b7274..dc072e3b 100644 --- a/chains/solana/contracts/programs/timelock/src/instructions/schedule.rs +++ b/chains/solana/contracts/programs/timelock/src/instructions/schedule.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use access_controller::AccessController; use crate::constants::{ANCHOR_DISCRIMINATOR, TIMELOCK_CONFIG_SEED, TIMELOCK_OPERATION_SEED}; -use crate::error::TimelockError; +use crate::error::{AuthError, TimelockError}; use crate::event::*; use crate::state::{Config, InstructionData, Operation}; @@ -14,8 +14,6 @@ pub fn schedule_batch<'info>( _id: [u8; 32], delay: u64, ) -> Result<()> { - let op = &mut ctx.accounts.operation; - // delay should greater than min_delay let config = &ctx.accounts.config; require!(delay >= config.min_delay, TimelockError::DelayInsufficient); @@ -23,26 +21,37 @@ pub fn schedule_batch<'info>( let current_time = Clock::get()?.unix_timestamp as u64; let scheduled_time = current_time .checked_add(delay) - .ok_or(TimelockError::InvalidInput)?; + .ok_or(TimelockError::Overflow)?; require!( scheduled_time > current_time && scheduled_time < u64::MAX, - TimelockError::InvalidInput + TimelockError::Overflow ); + let op = &mut ctx.accounts.operation; op.timestamp = scheduled_time; for (i, ix) in op.instructions.iter().enumerate() { - // todo: check function is not blocked + if ix.data.len() >= ANCHOR_DISCRIMINATOR { + let selector: [u8; ANCHOR_DISCRIMINATOR] = + // extract the first 8 bytes from ix.data as the selector + ix.data[..ANCHOR_DISCRIMINATOR].try_into().unwrap(); + + if config.blocked_selectors.is_blocked(&selector) { + return err!(TimelockError::BlockedSelector); + } + } else { + // todo: in discussion (allow empty data) + } emit!(CallScheduled { id: op.id, index: i as u64, target: ix.program_id, - data: ix.data.clone(), predecessor: op.predecessor, salt: op.salt, delay, + data: ix.data.clone(), }); } @@ -68,12 +77,11 @@ pub fn initialize_operation<'info>( timestamp: 0, // not scheduled operation should have timestamp 0, see src/state/operation.rs id, // id should be matched with hashed instructions(will be verified in finalize_operation) predecessor, // required for dependency check - salt, - total_instructions: instruction_count, + salt, // random salt for hash + total_instructions: instruction_count, // total number of instructions, used for space calculation and finalization validation instructions: Vec::with_capacity(instruction_count as usize), // create empty vector with total instruction capacity - is_finalized: false, - // operation.authority is the one who will schedule the operation(in mcm use case, the proposer msig signer PDA) - authority: ctx.accounts.proposer.key(), + is_finalized: false, // operation should be finalized before scheduling + authority: ctx.accounts.authority.key(), // authority of the operation }); Ok(()) @@ -91,12 +99,6 @@ pub fn append_instructions<'info>( Ok(()) } -pub fn clear_operation(_ctx: Context, _id: [u8; 32]) -> Result<()> { - // NOTE: ctx.accounts.operation is closed to be able to re-initialized, - // also allow finalized operation to be cleared - Ok(()) -} - /// finalize the operation, this is required to schedule the operation /// verify the operation status and id before mark it as finalized pub fn finalize_operation<'info>( @@ -109,14 +111,17 @@ pub fn finalize_operation<'info>( Ok(()) } +pub fn clear_operation(_ctx: Context, _id: [u8; 32]) -> Result<()> { + // NOTE: ctx.accounts.operation is closed to be able to re-initialized, + // also allow finalized operation to be cleared + Ok(()) +} + #[derive(Accounts)] #[instruction( id: [u8; 32], )] pub struct ScheduleBatch<'info> { - #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] - pub config: Account<'info, Config>, - #[account( mut, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], @@ -126,13 +131,13 @@ pub struct ScheduleBatch<'info> { )] pub operation: Box>, - // NOTE: access controller check happens in only_role_or_admin_role macro + #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + + // NOTE: access controller check and access happens in only_role_or_admin_role macro pub role_access_controller: AccountLoader<'info, AccessController>, - #[account( - mut, - address = operation.authority.key() @ TimelockError::Unauthorized - )] + #[account(mut)] pub authority: Signer<'info>, } @@ -144,9 +149,6 @@ pub struct ScheduleBatch<'info> { instruction_count: u32, )] pub struct InitializeOperation<'info> { - #[account( seeds = [TIMELOCK_CONFIG_SEED], bump)] - pub config: Account<'info, Config>, - #[account( init, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], @@ -157,12 +159,12 @@ pub struct InitializeOperation<'info> { )] pub operation: Account<'info, Operation>, + #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + #[account(mut)] pub authority: Signer<'info>, - /// CHECK: This is the proposer that will be allowed to schedule - pub proposer: AccountInfo<'info>, - pub system_program: Program<'info, System>, } @@ -189,7 +191,16 @@ pub struct AppendInstructions<'info> { )] pub operation: Account<'info, Operation>, - #[account(mut)] + #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + + #[account( + mut, + constraint = ( + operation.authority == authority.key() || + config.owner == authority.key() + ) @ AuthError::Unauthorized + )] pub authority: Signer<'info>, pub system_program: Program<'info, System>, @@ -197,34 +208,52 @@ pub struct AppendInstructions<'info> { #[derive(Accounts)] #[instruction(id: [u8; 32])] -pub struct ClearOperation<'info> { +pub struct FinalizeOperation<'info> { #[account( mut, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], bump, - close = authority, constraint = !operation.is_finalized @ TimelockError::OperationAlreadyFinalized, + constraint = !operation.is_scheduled() @ TimelockError::OperationAlreadyScheduled, + constraint = operation.instructions.len() == operation.total_instructions as usize @ TimelockError::TooManyInstructions, + constraint = operation.verify_id() @ TimelockError::InvalidId )] pub operation: Account<'info, Operation>, - #[account(mut)] + #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + + #[account( + mut, + constraint = ( + operation.authority == authority.key() || + config.owner == authority.key() + ) @ AuthError::Unauthorized + )] pub authority: Signer<'info>, } #[derive(Accounts)] #[instruction(id: [u8; 32])] -pub struct FinalizeOperation<'info> { +pub struct ClearOperation<'info> { #[account( mut, seeds = [TIMELOCK_OPERATION_SEED, id.as_ref()], bump, - constraint = !operation.is_finalized @ TimelockError::OperationAlreadyFinalized, - constraint = !operation.is_scheduled() @ TimelockError::OperationAlreadyScheduled, - constraint = operation.instructions.len() == operation.total_instructions as usize @ TimelockError::TooManyInstructions, - constraint = operation.verify_id() @ TimelockError::InvalidId + close = authority, + constraint = !operation.is_scheduled() @ TimelockError::OperationAlreadyScheduled, // restrict clearing of scheduled operation )] pub operation: Account<'info, Operation>, - #[account(mut)] + #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + pub config: Account<'info, Config>, + + #[account( + mut, + constraint = ( + operation.authority == authority.key() || + config.owner == authority.key() + ) @ AuthError::Unauthorized + )] pub authority: Signer<'info>, } diff --git a/chains/solana/contracts/programs/timelock/src/lib.rs b/chains/solana/contracts/programs/timelock/src/lib.rs index dffd3041..fefe8d11 100644 --- a/chains/solana/contracts/programs/timelock/src/lib.rs +++ b/chains/solana/contracts/programs/timelock/src/lib.rs @@ -112,7 +112,6 @@ pub mod timelock { new_duration: delay, }); config.min_delay = delay; - Ok(()) } @@ -121,7 +120,8 @@ pub mod timelock { ctx: Context, selector: [u8; 8], ) -> Result<()> { - // todo: implement function blocker related methods + let config = &mut ctx.accounts.config; + config.blocked_selectors.block_selector(selector)?; emit!(FunctionSelectorBlocked { selector }); Ok(()) } @@ -131,7 +131,8 @@ pub mod timelock { ctx: Context, selector: [u8; 8], ) -> Result<()> { - // todo: implement function blocker related methods + let config = &mut ctx.accounts.config; + config.blocked_selectors.unblock_selector(selector)?; emit!(FunctionSelectorUnblocked { selector }); Ok(()) } @@ -166,7 +167,7 @@ pub struct TransferOwnership<'info> { pub struct AcceptOwnership<'info> { #[account(mut, seeds = [TIMELOCK_CONFIG_SEED], bump)] pub config: Account<'info, Config>, - #[account(address = config.proposed_owner @ TimelockError::Unauthorized)] + #[account(address = config.proposed_owner @ AuthError::Unauthorized)] pub authority: Signer<'info>, } @@ -180,7 +181,7 @@ pub struct UpdateDelay<'info> { #[derive(Accounts)] pub struct BlockFunctionSelector<'info> { - #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + #[account(mut, seeds = [TIMELOCK_CONFIG_SEED], bump)] pub config: Account<'info, Config>, // owner(admin) only, access control with only_admin macro pub authority: Signer<'info>, @@ -188,7 +189,7 @@ pub struct BlockFunctionSelector<'info> { #[derive(Accounts)] pub struct UnblockFunctionSelector<'info> { - #[account(seeds = [TIMELOCK_CONFIG_SEED], bump)] + #[account(mut, seeds = [TIMELOCK_CONFIG_SEED], bump)] pub config: Account<'info, Config>, // owner(admin) only, access control with only_admin macro pub authority: Signer<'info>, diff --git a/chains/solana/contracts/programs/timelock/src/state/config.rs b/chains/solana/contracts/programs/timelock/src/state/config.rs index d5e36f4f..f60b2529 100644 --- a/chains/solana/contracts/programs/timelock/src/state/config.rs +++ b/chains/solana/contracts/programs/timelock/src/state/config.rs @@ -1,4 +1,11 @@ use anchor_lang::prelude::*; +use static_assertions::const_assert; +use std::mem; + +use arrayvec::arrayvec; + +use crate::constants::MAX_SELECTORS; +use crate::error::TimelockError; #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, PartialEq)] pub enum Role { @@ -21,6 +28,8 @@ pub struct Config { pub bypasser_role_access_controller: Pubkey, pub min_delay: u64, // initial minimum delay for operations + + pub blocked_selectors: BlockedSelectors, } impl Config { @@ -30,7 +39,57 @@ impl Config { Role::Executor => self.executor_role_access_controller, Role::Canceller => self.canceller_role_access_controller, Role::Bypasser => self.bypasser_role_access_controller, - _ => panic!("Invalid role"), + _ => panic!("invalid role"), + } + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct BlockedSelectors { + pub xs: [[u8; 8]; MAX_SELECTORS], + pub len: u64, +} + +arrayvec!(BlockedSelectors, [u8; 8], u64); +const_assert!( + mem::size_of::() + == mem::size_of::() + mem::size_of::<[u8; 8]>() * MAX_SELECTORS +); + +impl Space for BlockedSelectors { + const INIT_SPACE: usize = 8 + (8 * MAX_SELECTORS); +} + +impl BlockedSelectors { + pub fn is_blocked(&self, selector: &[u8; 8]) -> bool { + self.as_slice().binary_search(selector).is_ok() + } + + pub fn block_selector(&mut self, selector: [u8; 8]) -> Result<()> { + match self.as_slice().binary_search(&selector) { + Ok(_) => { + return err!(TimelockError::AlreadyBlocked); + } + Err(pos) => { + require!( + self.len() < MAX_SELECTORS, + TimelockError::MaxCapacityReached + ); + self.insert(pos, selector); + } + } + Ok(()) + } + + pub fn unblock_selector(&mut self, selector: [u8; 8]) -> Result<()> { + match self.as_slice().binary_search(&selector) { + Ok(pos) => { + self.remove(pos); + } + Err(_) => { + return err!(TimelockError::SelectorNotFound); + } } + Ok(()) } } diff --git a/chains/solana/contracts/programs/timelock/src/state/operation.rs b/chains/solana/contracts/programs/timelock/src/state/operation.rs index 01331d16..6f7ac747 100644 --- a/chains/solana/contracts/programs/timelock/src/state/operation.rs +++ b/chains/solana/contracts/programs/timelock/src/state/operation.rs @@ -6,22 +6,23 @@ use crate::constants::DONE_TIMESTAMP; #[account] pub struct Operation { - pub timestamp: u64, - pub id: [u8; 32], - pub predecessor: [u8; 32], - pub salt: [u8; 32], - - pub authority: Pubkey, - pub is_finalized: bool, - pub total_instructions: u32, - pub instructions: Vec, + pub timestamp: u64, // scheduled timestamp in unix time + pub id: [u8; 32], // hashed operation id + pub predecessor: [u8; 32], // hash of the previous operation + pub salt: [u8; 32], // random salt for the operation + pub authority: Pubkey, // authority of the operation + pub is_finalized: bool, // flag to indicate if the operation is finalized + pub total_instructions: u32, // total number of instructions in the operation + pub instructions: Vec, // list of instructions } impl Operation { + // before scheduling, timestamp should be 0 pub fn is_scheduled(&self) -> bool { self.timestamp > 0 } + // scheduled but not executed pub fn is_pending(&self) -> bool { self.timestamp > DONE_TIMESTAMP } @@ -64,7 +65,6 @@ impl Operation { encoded_data.extend_from_slice(&self.predecessor); encoded_data.extend_from_slice(&salt); - // hash everything with keccak256 hashv(&[&encoded_data]).to_bytes() } diff --git a/chains/solana/contracts/target/idl/ccip_router.json b/chains/solana/contracts/target/idl/ccip_router.json index 028e5885..ce65621d 100644 --- a/chains/solana/contracts/target/idl/ccip_router.json +++ b/chains/solana/contracts/target/idl/ccip_router.json @@ -39,6 +39,11 @@ "isMut": true, "isSigner": false }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, { "name": "authority", "isMut": true, @@ -164,7 +169,12 @@ ], "accounts": [ { - "name": "chainState", + "name": "sourceChainState", + "isMut": true, + "isSigner": false + }, + { + "name": "destChainState", "isMut": true, "isSigner": false }, @@ -217,7 +227,7 @@ ], "accounts": [ { - "name": "chainState", + "name": "sourceChainState", "isMut": true, "isSigner": false }, @@ -253,7 +263,7 @@ ], "accounts": [ { - "name": "chainState", + "name": "destChainState", "isMut": true, "isSigner": false }, @@ -290,7 +300,7 @@ ], "accounts": [ { - "name": "chainState", + "name": "sourceChainState", "isMut": true, "isSigner": false }, @@ -333,7 +343,7 @@ ], "accounts": [ { - "name": "chainState", + "name": "destChainState", "isMut": true, "isSigner": false }, @@ -761,6 +771,11 @@ "isMut": true, "isSigner": false }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, { "name": "authority", "isMut": false, @@ -980,7 +995,7 @@ ], "accounts": [ { - "name": "chainState", + "name": "destChainState", "isMut": false, "isSigner": false }, @@ -1029,7 +1044,7 @@ "isSigner": false }, { - "name": "chainState", + "name": "destChainState", "isMut": true, "isSigner": false }, @@ -1069,8 +1084,12 @@ }, { "name": "feeTokenUserAssociatedAccount", - "isMut": true, - "isSigner": false + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK this is the associated token account for the user paying the fee.", + "If paying with native SOL, this must be the zero address." + ] }, { "name": "feeTokenReceiver", @@ -1131,11 +1150,11 @@ "accounts": [ { "name": "config", - "isMut": true, + "isMut": false, "isSigner": false }, { - "name": "chainState", + "name": "sourceChainState", "isMut": true, "isSigner": false }, @@ -1227,7 +1246,7 @@ "isSigner": false }, { - "name": "chainState", + "name": "sourceChainState", "isMut": false, "isSigner": false }, @@ -1306,7 +1325,7 @@ "isSigner": false }, { - "name": "chainState", + "name": "sourceChainState", "isMut": false, "isSigner": false }, @@ -1422,25 +1441,48 @@ 2 ] } + } + ] + } + }, + { + "name": "GlobalState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "latestPriceSequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "SourceChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" }, { - "name": "paddingBeforeBilling", + "name": "state", "type": { - "array": [ - "u8", - 8 - ] + "defined": "SourceChainState" } }, { - "name": "latestPriceSequenceNumber", - "type": "u64" + "name": "config", + "type": { + "defined": "SourceChainConfig" + } } ] } }, { - "name": "ChainState", + "name": "DestChain", "type": { "kind": "struct", "fields": [ @@ -1449,15 +1491,15 @@ "type": "u8" }, { - "name": "sourceChain", + "name": "state", "type": { - "defined": "SourceChain" + "defined": "DestChainState" } }, { - "name": "destChain", + "name": "config", "type": { - "defined": "DestChain" + "defined": "DestChainConfig" } } ] @@ -1703,117 +1745,6 @@ ] } }, - { - "name": "Solana2AnyMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "receiver", - "type": "bytes" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "SolanaTokenAmount" - } - } - }, - { - "name": "feeToken", - "type": "publicKey" - }, - { - "name": "extraArgs", - "type": { - "defined": "ExtraArgsInput" - } - }, - { - "name": "tokenIndexes", - "type": "bytes" - } - ] - } - }, - { - "name": "SolanaTokenAmount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "token", - "type": "publicKey" - }, - { - "name": "amount", - "type": "u64" - } - ] - } - }, - { - "name": "ExtraArgsInput", - "type": { - "kind": "struct", - "fields": [ - { - "name": "gasLimit", - "type": { - "option": "u128" - } - }, - { - "name": "allowOutOfOrderExecution", - "type": { - "option": "bool" - } - } - ] - } - }, - { - "name": "Any2SolanaMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "messageId", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "sender", - "type": "bytes" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "SolanaTokenAmount" - } - } - } - ] - } - }, { "name": "RampMessageHeader", "type": { @@ -1935,7 +1866,7 @@ } }, { - "name": "EvmExtraArgs", + "name": "AnyExtraArgs", "type": { "kind": "struct", "fields": [ @@ -2016,7 +1947,7 @@ { "name": "extraArgs", "type": { - "defined": "EvmExtraArgs" + "defined": "AnyExtraArgs" } }, { @@ -2208,6 +2139,117 @@ ] } }, + { + "name": "Solana2AnyMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiver", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "SolanaTokenAmount" + } + } + }, + { + "name": "feeToken", + "type": "publicKey" + }, + { + "name": "extraArgs", + "type": { + "defined": "ExtraArgsInput" + } + }, + { + "name": "tokenIndexes", + "type": "bytes" + } + ] + } + }, + { + "name": "SolanaTokenAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "ExtraArgsInput", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasLimit", + "type": { + "option": "u128" + } + }, + { + "name": "allowOutOfOrderExecution", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "Any2SolanaMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "messageId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "sender", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "SolanaTokenAmount" + } + } + } + ] + } + }, { "name": "ReportContext", "type": { @@ -2333,26 +2375,6 @@ ] } }, - { - "name": "SourceChain", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": { - "defined": "SourceChainState" - } - }, - { - "name": "config", - "type": { - "defined": "SourceChainConfig" - } - } - ] - } - }, { "name": "DestChainState", "type": { @@ -2452,26 +2474,6 @@ ] } }, - { - "name": "DestChain", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": { - "defined": "DestChainState" - } - }, - { - "name": "config", - "type": { - "defined": "DestChainConfig" - } - } - ] - } - }, { "name": "TokenBilling", "type": { @@ -2697,6 +2699,36 @@ }, { "name": "FeeTokenDisabled" + }, + { + "name": "MessageTooLarge" + }, + { + "name": "UnsupportedNumberOfTokens" + }, + { + "name": "UnsupportedChainFamilySelector" + }, + { + "name": "InvalidEVMAddress" + }, + { + "name": "InvalidEncoding" + }, + { + "name": "InvalidInputsAtaAddress" + }, + { + "name": "InvalidInputsAtaWritable" + }, + { + "name": "InvalidTokenPrice" + }, + { + "name": "StaleGasPrice" + }, + { + "name": "InsufficientLamports" } ] } diff --git a/chains/solana/contracts/target/idl/mcm.json b/chains/solana/contracts/target/idl/mcm.json index 54cf4234..63128902 100644 --- a/chains/solana/contracts/target/idl/mcm.json +++ b/chains/solana/contracts/target/idl/mcm.json @@ -675,10 +675,6 @@ { "name": "isFinalized", "type": "bool" - }, - { - "name": "bump", - "type": "u8" } ] } @@ -758,10 +754,6 @@ { "name": "isFinalized", "type": "bool" - }, - { - "name": "bump", - "type": "u8" } ] } @@ -915,6 +907,116 @@ } ] } + }, + { + "name": "McmError", + "type": { + "kind": "enum", + "variants": [ + { + "name": "InvalidInputs" + }, + { + "name": "Overflow" + }, + { + "name": "WrongMultiSig" + }, + { + "name": "WrongChainId" + }, + { + "name": "InvalidSignature" + }, + { + "name": "FailedEcdsaRecover" + }, + { + "name": "InvalidRootLen" + }, + { + "name": "SignersNotFinalized" + }, + { + "name": "SignersAlreadyFinalized" + }, + { + "name": "SignaturesAlreadyFinalized" + }, + { + "name": "SignatureCountMismatch" + }, + { + "name": "TooManySignatures" + }, + { + "name": "SignaturesNotFinalized" + }, + { + "name": "SignaturesRootMismatch" + }, + { + "name": "SignaturesValidUntilMismatch" + }, + { + "name": "MismatchedInputSignerVectorsLength" + }, + { + "name": "OutOfBoundsNumOfSigners" + }, + { + "name": "MismatchedInputGroupArraysLength" + }, + { + "name": "GroupTreeNotWellFormed" + }, + { + "name": "SignerInDisabledGroup" + }, + { + "name": "OutOfBoundsGroupQuorum" + }, + { + "name": "SignersAddressesMustBeStrictlyIncreasing" + }, + { + "name": "SignedHashAlreadySeen" + }, + { + "name": "InvalidSigner" + }, + { + "name": "MissingConfig" + }, + { + "name": "InsufficientSigners" + }, + { + "name": "ValidUntilHasAlreadyPassed" + }, + { + "name": "ProofCannotBeVerified" + }, + { + "name": "PendingOps" + }, + { + "name": "WrongPreOpCount" + }, + { + "name": "WrongPostOpCount" + }, + { + "name": "PostOpCountReached" + }, + { + "name": "RootExpired" + }, + { + "name": "WrongNonce" + } + ] + } } ], "events": [ @@ -1017,178 +1119,8 @@ "errors": [ { "code": 6000, - "name": "WrongMultiSig", - "msg": "Invalid multisig" - }, - { - "code": 6001, - "name": "WrongChainId", - "msg": "Invalid chainID" - }, - { - "code": 6002, "name": "Unauthorized", "msg": "The signer is unauthorized" - }, - { - "code": 6003, - "name": "InvalidInputs", - "msg": "Invalid inputs" - }, - { - "code": 6004, - "name": "Overflow", - "msg": "overflow occurred." - }, - { - "code": 6005, - "name": "InvalidSignature", - "msg": "Invalid signature" - }, - { - "code": 6006, - "name": "FailedEcdsaRecover", - "msg": "Failed ECDSA recover" - }, - { - "code": 6007, - "name": "InvalidRootLen", - "msg": "Invalid root length" - }, - { - "code": 6008, - "name": "SignersNotFinalized", - "msg": "Config signers not finalized" - }, - { - "code": 6009, - "name": "SignersAlreadyFinalized", - "msg": "Config signers already finalized" - }, - { - "code": 6010, - "name": "SignaturesAlreadyFinalized", - "msg": "Signatures already finalized" - }, - { - "code": 6011, - "name": "SignatureCountMismatch", - "msg": "Uploaded signatures count mismatch" - }, - { - "code": 6012, - "name": "TooManySignatures", - "msg": "Too many signatures" - }, - { - "code": 6013, - "name": "SignaturesNotFinalized", - "msg": "Signatures not finalized" - }, - { - "code": 6014, - "name": "SignaturesRootMismatch", - "msg": "Signatures root mismatch" - }, - { - "code": 6015, - "name": "SignaturesValidUntilMismatch", - "msg": "Signatures valid until mismatch" - }, - { - "code": 6200, - "name": "MismatchedInputSignerVectorsLength", - "msg": "The input vectors for signer addresses and signer groups must have the same length" - }, - { - "code": 6201, - "name": "OutOfBoundsNumOfSigners", - "msg": "The number of signers is 0 or greater than MAX_NUM_SIGNERS" - }, - { - "code": 6202, - "name": "MismatchedInputGroupArraysLength", - "msg": "The input arrays for group parents and group quorums must be of length NUM_GROUPS" - }, - { - "code": 6203, - "name": "GroupTreeNotWellFormed", - "msg": "the group tree isn't well-formed." - }, - { - "code": 6204, - "name": "SignerInDisabledGroup", - "msg": "a disabled group contains a signer." - }, - { - "code": 6205, - "name": "OutOfBoundsGroupQuorum", - "msg": "the quorum of some group is larger than the number of signers in it." - }, - { - "code": 6206, - "name": "SignersAddressesMustBeStrictlyIncreasing", - "msg": "the signers' addresses are not a strictly increasing monotone sequence." - }, - { - "code": 6207, - "name": "SignedHashAlreadySeen", - "msg": "The combination of signature and valid_until has already been seen" - }, - { - "code": 6208, - "name": "InvalidSigner", - "msg": "Invalid signer" - }, - { - "code": 6209, - "name": "MissingConfig", - "msg": "Missing configuration" - }, - { - "code": 6210, - "name": "InsufficientSigners", - "msg": "Insufficient signers" - }, - { - "code": 6211, - "name": "ValidUntilHasAlreadyPassed", - "msg": "Valid until has already passed" - }, - { - "code": 6212, - "name": "ProofCannotBeVerified", - "msg": "Proof cannot be verified" - }, - { - "code": 6213, - "name": "PendingOps", - "msg": "Pending operations" - }, - { - "code": 6214, - "name": "WrongPreOpCount", - "msg": "Wrong pre-operation count" - }, - { - "code": 6215, - "name": "WrongPostOpCount", - "msg": "Wrong post-operation count" - }, - { - "code": 6216, - "name": "PostOpCountReached", - "msg": "Post-operation count reached" - }, - { - "code": 6217, - "name": "RootExpired", - "msg": "Root expired" - }, - { - "code": 6218, - "name": "WrongNonce", - "msg": "Wrong nonce" } ] } \ No newline at end of file diff --git a/chains/solana/contracts/target/idl/timelock.json b/chains/solana/contracts/target/idl/timelock.json index 0ed86a94..d9e5f1a2 100644 --- a/chains/solana/contracts/target/idl/timelock.json +++ b/chains/solana/contracts/target/idl/timelock.json @@ -100,13 +100,13 @@ "name": "scheduleBatch", "accounts": [ { - "name": "config", - "isMut": false, + "name": "operation", + "isMut": true, "isSigner": false }, { - "name": "operation", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false }, { @@ -140,13 +140,13 @@ "name": "initializeOperation", "accounts": [ { - "name": "config", - "isMut": false, + "name": "operation", + "isMut": true, "isSigner": false }, { - "name": "operation", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false }, { @@ -154,11 +154,6 @@ "isMut": true, "isSigner": true }, - { - "name": "proposer", - "isMut": false, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, @@ -207,6 +202,11 @@ "isMut": true, "isSigner": false }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, { "name": "authority", "isMut": true, @@ -246,6 +246,11 @@ "isMut": true, "isSigner": false }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, { "name": "authority", "isMut": true, @@ -272,6 +277,11 @@ "isMut": true, "isSigner": false }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, { "name": "authority", "isMut": true, @@ -294,13 +304,13 @@ "name": "cancel", "accounts": [ { - "name": "config", - "isMut": false, + "name": "operation", + "isMut": true, "isSigner": false }, { - "name": "operation", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false }, { @@ -330,22 +340,22 @@ "name": "executeBatch", "accounts": [ { - "name": "config", - "isMut": false, + "name": "operation", + "isMut": true, "isSigner": false }, { - "name": "timelockSigner", + "name": "predecessorOperation", "isMut": false, "isSigner": false }, { - "name": "operation", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false }, { - "name": "predecessorOperation", + "name": "timelockSigner", "isMut": false, "isSigner": false }, @@ -376,18 +386,18 @@ "name": "bypasserExecuteBatch", "accounts": [ { - "name": "config", - "isMut": false, + "name": "operation", + "isMut": true, "isSigner": false }, { - "name": "timelockSigner", + "name": "config", "isMut": false, "isSigner": false }, { - "name": "operation", - "isMut": true, + "name": "timelockSigner", + "isMut": false, "isSigner": false }, { @@ -439,7 +449,7 @@ "accounts": [ { "name": "config", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -465,7 +475,7 @@ "accounts": [ { "name": "config", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -557,6 +567,12 @@ { "name": "minDelay", "type": "u64" + }, + { + "name": "blockedSelectors", + "type": { + "defined": "BlockedSelectors" + } } ] } @@ -622,6 +638,32 @@ } ], "types": [ + { + "name": "BlockedSelectors", + "type": { + "kind": "struct", + "fields": [ + { + "name": "xs", + "type": { + "array": [ + { + "array": [ + "u8", + 8 + ] + }, + 32 + ] + } + }, + { + "name": "len", + "type": "u64" + } + ] + } + }, { "name": "InstructionData", "type": { @@ -671,9 +713,6 @@ "type": { "kind": "enum", "variants": [ - { - "name": "Unauthorized" - }, { "name": "InvalidInput" }, @@ -707,11 +746,23 @@ { "name": "MissingDependency" }, + { + "name": "InvalidAccessController" + }, { "name": "BlockedSelector" }, { - "name": "InvalidAccessController" + "name": "AlreadyBlocked" + }, + { + "name": "SelectorNotFound" + }, + { + "name": "InvalidInstructionData" + }, + { + "name": "MaxCapacityReached" } ] } @@ -764,11 +815,6 @@ "type": "publicKey", "index": false }, - { - "name": "data", - "type": "bytes", - "index": false - }, { "name": "predecessor", "type": { @@ -793,6 +839,11 @@ "name": "delay", "type": "u64", "index": false + }, + { + "name": "data", + "type": "bytes", + "index": false } ] }, @@ -910,8 +961,8 @@ "errors": [ { "code": 6000, - "name": "Placeholder", - "msg": "Todo generate error type with anchor" + "name": "Unauthorized", + "msg": "The signer is unauthorized" } ] } \ No newline at end of file diff --git a/chains/solana/contracts/tests/accesscontroller/access_controller.go b/chains/solana/contracts/tests/accesscontroller/access_controller.go new file mode 100644 index 00000000..88d6bd7c --- /dev/null +++ b/chains/solana/contracts/tests/accesscontroller/access_controller.go @@ -0,0 +1,35 @@ +package accesscontroller + +import ( + "bytes" + "context" + "fmt" + "slices" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/access_controller" +) + +func HasAccess(ctx context.Context, client *rpc.Client, accessController solana.PublicKey, address solana.PublicKey, commitment rpc.CommitmentType) (bool, error) { + var ac access_controller.AccessController + err := utils.GetAccountDataBorshInto( + ctx, + client, + accessController, + commitment, + &ac, + ) + if err != nil { + return false, fmt.Errorf("failed to get account data: %w", err) + } + findInSortedList := func(list []solana.PublicKey, target solana.PublicKey) (int, bool) { + return slices.BinarySearchFunc(list, target, func(a, b solana.PublicKey) int { + return bytes.Compare(a.Bytes(), b.Bytes()) + }) + } + _, found := findInSortedList(ac.AccessList.Xs[:ac.AccessList.Len], address) + return found, nil +} diff --git a/chains/solana/contracts/tests/ccip/ccip_messages.go b/chains/solana/contracts/tests/ccip/ccip_messages.go index 8909f94e..e0de7d25 100644 --- a/chains/solana/contracts/tests/ccip/ccip_messages.go +++ b/chains/solana/contracts/tests/ccip/ccip_messages.go @@ -44,8 +44,31 @@ func HashCommitReport(ctx [3][32]byte, report ccip_router.CommitInput) ([]byte, return hash.Sum(nil), nil } +var reportSequence uint64 = 1 + +func CreateReportContext(sequence uint64) [3][32]byte { + return [3][32]byte{ + config.ConfigDigest, + [32]byte(binary.BigEndian.AppendUint64(config.Empty24Byte[:], sequence)), + utils.MakeRandom32ByteArray(), + } +} + +func ParseSequenceNumber(ctx [3][32]byte) uint64 { + return binary.BigEndian.Uint64(ctx[1][24:]) +} + +func ReportSequence() uint64 { + return reportSequence +} + +func NextCommitReportContext() [3][32]byte { + reportSequence++ + return CreateReportContext(reportSequence) +} + func CreateNextMessage(ctx context.Context, solanaGoClient *rpc.Client, t *testing.T) (ccip_router.Any2SolanaRampMessage, [32]byte) { - nextSeq := NextSequenceNumber(ctx, solanaGoClient, config.EvmChainStatePDA, t) + nextSeq := NextSequenceNumber(ctx, solanaGoClient, config.EvmSourceChainStatePDA, t) msg := CreateDefaultMessageWith(config.EvmChainSelector, nextSeq) hash, err := HashEvmToSolanaMessage(msg, config.OnRampAddress) @@ -53,11 +76,11 @@ func CreateNextMessage(ctx context.Context, solanaGoClient *rpc.Client, t *testi return msg, [32]byte(hash) } -func NextSequenceNumber(ctx context.Context, solanaGoClient *rpc.Client, chainStatePDA solana.PublicKey, t *testing.T) uint64 { - var chainStateAccount ccip_router.ChainState - err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, chainStatePDA, config.DefaultCommitment, &chainStateAccount) +func NextSequenceNumber(ctx context.Context, solanaGoClient *rpc.Client, sourceChainStatePDA solana.PublicKey, t *testing.T) uint64 { + var chainStateAccount ccip_router.SourceChain + err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, sourceChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err) - return chainStateAccount.SourceChain.State.MinSeqNr + return chainStateAccount.State.MinSeqNr } func CreateDefaultMessageWith(sourceChainSelector uint64, sequenceNumber uint64) ccip_router.Any2SolanaRampMessage { diff --git a/chains/solana/contracts/tests/ccip/ccip_router_test.go b/chains/solana/contracts/tests/ccip/ccip_router_test.go index 8460d27c..df79eab8 100644 --- a/chains/solana/contracts/tests/ccip/ccip_router_test.go +++ b/chains/solana/contracts/tests/ccip/ccip_router_test.go @@ -38,6 +38,8 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, gerr) anotherUser, gerr := solana.NewRandomPrivateKey() require.NoError(t, gerr) + tokenlessUser, gerr := solana.NewRandomPrivateKey() + require.NoError(t, gerr) admin, gerr := solana.NewRandomPrivateKey() require.NoError(t, gerr) anotherAdmin, gerr := solana.NewRandomPrivateKey() @@ -57,6 +59,7 @@ func TestCCIPRouter(t *testing.T) { billingATA solana.PublicKey userATA solana.PublicKey anotherUserATA solana.PublicKey + tokenlessUserATA solana.PublicKey billingConfigPDA solana.PublicKey // add other accounts as needed } @@ -109,16 +112,22 @@ func TestCCIPRouter(t *testing.T) { IsEnabled: true, // minimal valid config - DefaultTxGasLimit: 1, - MaxPerMsgGasLimit: 100, - ChainFamilySelector: [4]uint8{0, 1, 2, 3}, + DefaultTxGasLimit: 1, + MaxPerMsgGasLimit: 100, + MaxDataBytes: 32, + MaxNumberOfTokensPerMsg: 1, + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + ChainFamilySelector: [4]uint8{40, 18, 213, 44}, } + // Small enough to fit in u160, big enough to not fall in the precompile space. + validReceiverAddress := [32]byte{} + validReceiverAddress[12] = 1 var commitLookupTable map[solana.PublicKey]solana.PublicKeySlice t.Run("setup", func(t *testing.T) { t.Run("funding", func(t *testing.T) { - utils.FundAccounts(ctx, append(transmitters, user, anotherUser, admin, anotherAdmin, tokenPoolAdmin, anotherTokenPoolAdmin), solanaGoClient, t) + utils.FundAccounts(ctx, append(transmitters, user, anotherUser, tokenlessUser, admin, anotherAdmin, tokenPoolAdmin, anotherTokenPoolAdmin), solanaGoClient, t) }) t.Run("receiver", func(t *testing.T) { @@ -217,6 +226,8 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, uerr) wsolAnotherUserATA, _, auerr := utils.FindAssociatedTokenAddress(solana.TokenProgramID, solana.SolMint, anotherUser.PublicKey()) require.NoError(t, auerr) + wsolTokenlessUserATA, _, tuerr := utils.FindAssociatedTokenAddress(solana.TokenProgramID, solana.SolMint, tokenlessUser.PublicKey()) + require.NoError(t, tuerr) // persist the WSOL config for later use wsol.program = solana.TokenProgramID @@ -224,6 +235,7 @@ func TestCCIPRouter(t *testing.T) { wsol.billingConfigPDA = wsolPDA wsol.userATA = wsolUserATA wsol.anotherUserATA = wsolAnotherUserATA + wsol.tokenlessUserATA = wsolTokenlessUserATA wsol.billingATA = wsolReceiver /////////////// @@ -247,6 +259,8 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, uerr) token2022AnotherUserATA, _, auerr := utils.FindAssociatedTokenAddress(config.Token2022Program, mintPubK, anotherUser.PublicKey()) require.NoError(t, auerr) + token2022TokenlessUserATA, _, tuerr := utils.FindAssociatedTokenAddress(config.Token2022Program, mintPubK, tokenlessUser.PublicKey()) + require.NoError(t, tuerr) // persist the Token2022 billing config for later use token2022.program = config.Token2022Program @@ -254,6 +268,7 @@ func TestCCIPRouter(t *testing.T) { token2022.billingConfigPDA = token2022PDA token2022.userATA = token2022UserATA token2022.anotherUserATA = token2022AnotherUserATA + token2022.tokenlessUserATA = token2022TokenlessUserATA token2022.billingATA = token2022Receiver }) @@ -266,15 +281,16 @@ func TestCCIPRouter(t *testing.T) { // static accounts that are always needed ccip_router.ProgramID, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.RouterStatePDA, + config.EvmSourceChainStatePDA, // for checking the seq numbers solana.SystemProgramID, solana.SysVarInstructionsPubkey, // remaining_accounts that are only sometimes needed wsol.billingConfigPDA, token2022.billingConfigPDA, - config.EvmChainStatePDA, - config.SolanaChainStatePDA, + config.EvmDestChainStatePDA, // to update prices + config.SolanaDestChainStatePDA, } lookupTableAddr, err := utils.SetupLookupTable(ctx, t, solanaGoClient, admin, lookupEntries) require.NoError(t, err) @@ -314,6 +330,7 @@ func TestCCIPRouter(t *testing.T) { allowOutOfOrderExecution, config.EnableExecutionAfter, config.RouterConfigPDA, + config.RouterStatePDA, admin.PublicKey(), solana.SystemProgramID, config.CcipRouterProgram, @@ -440,19 +457,20 @@ func TestCCIPRouter(t *testing.T) { }, }, } - getChainStatePDA := func(selector uint64) solana.PublicKey { - chainStatePDA, _, _ := solana.FindProgramAddress([][]byte{[]byte("chain_state"), binary.LittleEndian.AppendUint64([]byte{}, selector)}, config.CcipRouterProgram) - return chainStatePDA - } t.Run("When and admin adds a chain selector with invalid dest chain config, it fails", func(t *testing.T) { for _, test := range invalidInputTests { t.Run(test.Name, func(t *testing.T) { + sourceChainStatePDA, serr := GetSourceChainStatePDA(test.Selector) + require.NoError(t, serr) + destChainStatePDA, derr := GetDestChainStatePDA(test.Selector) + require.NoError(t, derr) instruction, err := ccip_router.NewAddChainSelectorInstruction( test.Selector, validSourceChainConfig, test.Conf, // here is the invalid dest config data - getChainStatePDA(test.Selector), + sourceChainStatePDA, + destChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), solana.SystemProgramID, @@ -469,7 +487,8 @@ func TestCCIPRouter(t *testing.T) { config.EvmChainSelector, validSourceChainConfig, validDestChainConfig, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, user.PublicKey(), // not an admin solana.SystemProgramID, @@ -484,7 +503,8 @@ func TestCCIPRouter(t *testing.T) { config.EvmChainSelector, validSourceChainConfig, validDestChainConfig, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), solana.SystemProgramID, @@ -493,14 +513,18 @@ func TestCCIPRouter(t *testing.T) { result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &chainStateAccount) + var sourceChainStateAccount ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &sourceChainStateAccount) + require.NoError(t, err, "failed to get account info") + require.Equal(t, uint64(1), sourceChainStateAccount.State.MinSeqNr) + require.Equal(t, true, sourceChainStateAccount.Config.IsEnabled) + require.Equal(t, config.OnRampAddress, sourceChainStateAccount.Config.OnRamp) + + var destChainStateAccount ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &destChainStateAccount) require.NoError(t, err, "failed to get account info") - require.Equal(t, uint64(1), chainStateAccount.SourceChain.State.MinSeqNr) - require.Equal(t, true, chainStateAccount.SourceChain.Config.IsEnabled) - require.Equal(t, config.OnRampAddress, chainStateAccount.SourceChain.Config.OnRamp) - require.Equal(t, uint64(0), chainStateAccount.DestChain.State.SequenceNumber) - require.Equal(t, validDestChainConfig, chainStateAccount.DestChain.Config) + require.Equal(t, uint64(0), destChainStateAccount.State.SequenceNumber) + require.Equal(t, validDestChainConfig, destChainStateAccount.Config) }) t.Run("When admin adds another chain selector it's also added on the list", func(t *testing.T) { @@ -518,7 +542,8 @@ func TestCCIPRouter(t *testing.T) { DefaultTxGasLimit: 1, MaxPerMsgGasLimit: 100, ChainFamilySelector: [4]uint8{3, 2, 1, 0}}, - config.SolanaChainStatePDA, + config.SolanaSourceChainStatePDA, + config.SolanaDestChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), solana.SystemProgramID, @@ -527,20 +552,24 @@ func TestCCIPRouter(t *testing.T) { result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaChainStatePDA, config.DefaultCommitment, &chainStateAccount) + var sourceChainStateAccount ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaSourceChainStatePDA, config.DefaultCommitment, &sourceChainStateAccount) require.NoError(t, err, "failed to get account info") - require.Equal(t, uint64(1), chainStateAccount.SourceChain.State.MinSeqNr) - require.Equal(t, true, chainStateAccount.SourceChain.Config.IsEnabled) - require.Equal(t, config.CcipRouterProgram[:], chainStateAccount.SourceChain.Config.OnRamp) - require.Equal(t, uint64(0), chainStateAccount.DestChain.State.SequenceNumber) + require.Equal(t, uint64(1), sourceChainStateAccount.State.MinSeqNr) + require.Equal(t, true, sourceChainStateAccount.Config.IsEnabled) + require.Equal(t, config.CcipRouterProgram[:], sourceChainStateAccount.Config.OnRamp) + + var destChainStateAccount ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaDestChainStatePDA, config.DefaultCommitment, &destChainStateAccount) + require.NoError(t, err, "failed to get account info") + require.Equal(t, uint64(0), destChainStateAccount.State.SequenceNumber) }) t.Run("When a non-admin tries to disable the chain selector, it fails", func(t *testing.T) { t.Run("Source", func(t *testing.T) { ix, err := ccip_router.NewDisableSourceChainSelectorInstruction( config.EvmChainSelector, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, config.RouterConfigPDA, user.PublicKey(), ).ValidateAndBuild() @@ -552,7 +581,7 @@ func TestCCIPRouter(t *testing.T) { t.Run("Dest", func(t *testing.T) { ix, err := ccip_router.NewDisableDestChainSelectorInstruction( config.EvmChainSelector, - config.EvmChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, user.PublicKey(), ).ValidateAndBuild() @@ -564,45 +593,45 @@ func TestCCIPRouter(t *testing.T) { t.Run("When an admin disables the chain selector, it is no longer enabled", func(t *testing.T) { t.Run("Source", func(t *testing.T) { - var initial ccip_router.ChainState - err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &initial) + var initial ccip_router.SourceChain + err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &initial) require.NoError(t, err, "failed to get account info") - require.Equal(t, true, initial.SourceChain.Config.IsEnabled) + require.Equal(t, true, initial.Config.IsEnabled) ix, err := ccip_router.NewDisableSourceChainSelectorInstruction( config.EvmChainSelector, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - var final ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &final) + var final ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &final) require.NoError(t, err, "failed to get account info") - require.Equal(t, false, final.SourceChain.Config.IsEnabled) + require.Equal(t, false, final.Config.IsEnabled) }) t.Run("Dest", func(t *testing.T) { - var initial ccip_router.ChainState - err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &initial) + var initial ccip_router.DestChain + err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &initial) require.NoError(t, err, "failed to get account info") - require.Equal(t, true, initial.DestChain.Config.IsEnabled) + require.Equal(t, true, initial.Config.IsEnabled) ix, err := ccip_router.NewDisableDestChainSelectorInstruction( config.EvmChainSelector, - config.EvmChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - var final ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &final) + var final ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &final) require.NoError(t, err, "failed to get account info") - require.Equal(t, false, final.DestChain.Config.IsEnabled) + require.Equal(t, false, final.Config.IsEnabled) }) }) @@ -612,10 +641,12 @@ func TestCCIPRouter(t *testing.T) { continue } t.Run(test.Name, func(t *testing.T) { + destChainStatePDA, derr := GetDestChainStatePDA(test.Selector) + require.NoError(t, derr) instruction, err := ccip_router.NewUpdateDestChainConfigInstruction( test.Selector, test.Conf, - getChainStatePDA(test.Selector), + destChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), ).ValidateAndBuild() @@ -631,7 +662,7 @@ func TestCCIPRouter(t *testing.T) { instruction, err := ccip_router.NewUpdateSourceChainConfigInstruction( config.EvmChainSelector, validSourceChainConfig, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, config.RouterConfigPDA, user.PublicKey(), // unauthorized ).ValidateAndBuild() @@ -644,7 +675,7 @@ func TestCCIPRouter(t *testing.T) { instruction, err := ccip_router.NewUpdateDestChainConfigInstruction( config.EvmChainSelector, validDestChainConfig, - config.EvmChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, user.PublicKey(), // unauthorized ).ValidateAndBuild() @@ -655,19 +686,19 @@ func TestCCIPRouter(t *testing.T) { }) t.Run("When an admin updates the chain state config, it is configured", func(t *testing.T) { - var initial ccip_router.ChainState - err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &initial) - require.NoError(t, err, "failed to get account info") + var initialSource ccip_router.SourceChain + serr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &initialSource) + require.NoError(t, serr, "failed to get account info") t.Run("Source", func(t *testing.T) { - updated := initial.SourceChain.Config + updated := initialSource.Config updated.IsEnabled = true - require.NotEqual(t, initial.SourceChain.Config, updated) // at this point, onchain is disabled and we'll re-enable it + require.NotEqual(t, initialSource.Config, updated) // at this point, onchain is disabled and we'll re-enable it instruction, err := ccip_router.NewUpdateSourceChainConfigInstruction( config.EvmChainSelector, updated, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), ).ValidateAndBuild() @@ -675,21 +706,25 @@ func TestCCIPRouter(t *testing.T) { result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) require.NotNil(t, result) - var final ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &final) + var final ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &final) require.NoError(t, err, "failed to get account info") - require.Equal(t, updated, final.SourceChain.Config) + require.Equal(t, updated, final.Config) }) + var initialDest ccip_router.DestChain + derr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &initialDest) + require.NoError(t, derr, "failed to get account info") + t.Run("Dest", func(t *testing.T) { - updated := initial.DestChain.Config + updated := initialDest.Config updated.IsEnabled = true - require.NotEqual(t, initial.DestChain.Config, updated) // at this point, onchain is disabled and we'll re-enable it + require.NotEqual(t, initialDest.Config, updated) // at this point, onchain is disabled and we'll re-enable it instruction, err := ccip_router.NewUpdateDestChainConfigInstruction( config.EvmChainSelector, updated, - config.EvmChainStatePDA, + config.EvmDestChainStatePDA, config.RouterConfigPDA, admin.PublicKey(), ).ValidateAndBuild() @@ -697,10 +732,10 @@ func TestCCIPRouter(t *testing.T) { result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) require.NotNil(t, result) - var chainState ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &chainState) + var final ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &final) require.NoError(t, err, "failed to get account info") - require.Equal(t, updated, chainState.DestChain.Config) + require.Equal(t, updated, final.Config) }) }) @@ -775,6 +810,11 @@ func TestCCIPRouter(t *testing.T) { Accounts AccountsPerToken } + // Any nonzero timestamp is valid (for now) + validTimestamp := int64(100) + validPriceValue := [28]uint8{} + validPriceValue[27] = 3 + testTokens := []TestToken{ { Accounts: wsol, @@ -782,8 +822,8 @@ func TestCCIPRouter(t *testing.T) { Enabled: true, Mint: solana.SolMint, UsdPerToken: ccip_router.TimestampedPackedU224{ - Value: [28]uint8{}, - Timestamp: 0, + Value: validPriceValue, + Timestamp: validTimestamp, }, PremiumMultiplierWeiPerEth: 0, }}, @@ -793,8 +833,8 @@ func TestCCIPRouter(t *testing.T) { Enabled: true, Mint: token2022.mint, UsdPerToken: ccip_router.TimestampedPackedU224{ - Value: [28]uint8{}, - Timestamp: 0, + Value: validPriceValue, + Timestamp: validTimestamp, }, PremiumMultiplierWeiPerEth: 0, }}, @@ -823,17 +863,29 @@ func TestCCIPRouter(t *testing.T) { t.Run("setup:funding_and_approvals", func(t *testing.T) { type Item struct { - user solana.PrivateKey - getATA func(apt *AccountsPerToken) solana.PublicKey + name string + user solana.PrivateKey + getATA func(apt *AccountsPerToken) solana.PublicKey + shouldFund bool } list := []Item{ { - user: user, - getATA: func(apt *AccountsPerToken) solana.PublicKey { return apt.userATA }, + name: "user", + user: user, + getATA: func(apt *AccountsPerToken) solana.PublicKey { return apt.userATA }, + shouldFund: true, + }, + { + name: "anotherUser", + user: anotherUser, + getATA: func(apt *AccountsPerToken) solana.PublicKey { return apt.anotherUserATA }, + shouldFund: true, }, { - user: anotherUser, - getATA: func(apt *AccountsPerToken) solana.PublicKey { return apt.anotherUserATA }, + name: "tokenlessUser", + user: tokenlessUser, + getATA: func(apt *AccountsPerToken) solana.PublicKey { return apt.tokenlessUserATA }, + shouldFund: false, // do not fund tokenless user }, } @@ -842,7 +894,7 @@ func TestCCIPRouter(t *testing.T) { // create ATA for user ixAtaUser, addrUser, uerr := utils.CreateAssociatedTokenAccount(token.program, token.mint, it.user.PublicKey(), it.user.PublicKey()) require.NoError(t, uerr) - require.Equal(t, it.getATA(token), addrUser) + require.Equal(t, it.getATA(token), addrUser, fmt.Sprintf("ATA for user %s and token %s", it.name, token.name)) // Approve CCIP to transfer the user's token for billing ixApprove, aerr := utils.TokenApproveChecked(1e9, 9, token.program, it.getATA(token), token.mint, config.BillingSignerPDA, it.user.PublicKey(), []solana.PublicKey{}) @@ -851,18 +903,20 @@ func TestCCIPRouter(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixAtaUser, ixApprove}, it.user, config.DefaultCommitment) } - // fund user token2022 (mint directly to user ATA) - ixMint, merr := utils.MintTo(1e9, token2022.program, token2022.mint, it.getATA(&token2022), admin.PublicKey()) - require.NoError(t, merr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixMint}, admin, config.DefaultCommitment) - - // fund user WSOL (transfer SOL + syncNative) - transferAmount := 1.0 * solana.LAMPORTS_PER_SOL - ixTransfer, terr := utils.NativeTransfer(wsol.program, transferAmount, it.user.PublicKey(), it.getATA(&wsol)) - require.NoError(t, terr) - ixSync, serr := utils.SyncNative(wsol.program, it.getATA(&wsol)) - require.NoError(t, serr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixTransfer, ixSync}, it.user, config.DefaultCommitment) + if it.shouldFund { + // fund user token2022 (mint directly to user ATA) + ixMint, merr := utils.MintTo(1e9, token2022.program, token2022.mint, it.getATA(&token2022), admin.PublicKey()) + require.NoError(t, merr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixMint}, admin, config.DefaultCommitment) + + // fund user WSOL (transfer SOL + syncNative) + transferAmount := 1.0 * solana.LAMPORTS_PER_SOL + ixTransfer, terr := utils.NativeTransfer(wsol.program, transferAmount, it.user.PublicKey(), it.getATA(&wsol)) + require.NoError(t, terr) + ixSync, serr := utils.SyncNative(wsol.program, it.getATA(&wsol)) + require.NoError(t, serr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixTransfer, ixSync}, it.user, config.DefaultCommitment) + } } }) @@ -1058,6 +1112,7 @@ func TestCCIPRouter(t *testing.T) { [][20]byte{}, []solana.PublicKey{}, config.RouterConfigPDA, + config.RouterStatePDA, user.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1095,6 +1150,7 @@ func TestCCIPRouter(t *testing.T) { v.signers, v.transmitters, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1154,6 +1210,7 @@ func TestCCIPRouter(t *testing.T) { signerAddresses, transmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1172,6 +1229,7 @@ func TestCCIPRouter(t *testing.T) { signerAddresses, transmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1194,6 +1252,7 @@ func TestCCIPRouter(t *testing.T) { signerAddresses, invalidTransmitters, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1217,6 +1276,7 @@ func TestCCIPRouter(t *testing.T) { invalidSigners, transmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1238,6 +1298,7 @@ func TestCCIPRouter(t *testing.T) { invalidSigners, transmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1262,6 +1323,7 @@ func TestCCIPRouter(t *testing.T) { signerAddresses, invalidTransmitters, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1286,6 +1348,7 @@ func TestCCIPRouter(t *testing.T) { repeatedSignerAddresses, oneTransmitter, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1306,6 +1369,7 @@ func TestCCIPRouter(t *testing.T) { signerAddresses, invalidTransmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1328,6 +1392,7 @@ func TestCCIPRouter(t *testing.T) { invalidSignerAddresses, transmitterPubKeys, config.RouterConfigPDA, + config.RouterStatePDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1828,15 +1893,15 @@ func TestCCIPRouter(t *testing.T) { // getFee Tests // ////////////////////////// t.Run("getFee", func(t *testing.T) { - t.Run("Basic test", func(t *testing.T) { + t.Run("Fee is retrieved for a correctly formatted message", func(t *testing.T) { message := ccip_router.Solana2AnyMessage{ - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], FeeToken: wsol.mint, } billingTokenConfigPDA := getTokenConfigPDA(wsol.mint) - instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() + instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() require.NoError(t, err) result := utils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user) @@ -1846,47 +1911,28 @@ func TestCCIPRouter(t *testing.T) { require.Equal(t, uint64(1), returned) }) - t.Run("Cannot get fee in invalid chain", func(t *testing.T) { - const ConstraintSeedsError = 2006 + t.Run("Cannot get fee for message with invalid address", func(t *testing.T) { + // Bigger than u160 + tooBigAddress := [32]byte{} + tooBigAddress[11] = 1 - message := ccip_router.Solana2AnyMessage{ - Receiver: []byte{1, 2, 3}, - FeeToken: wsol.mint, - } + // Falls within precompile region + tooSmallAddress := [32]byte{} + tooSmallAddress[31] = 1 - badChainSelector := 1234 - - billingTokenConfigPDA := getTokenConfigPDA(wsol.mint) - instruction, err := ccip_router.NewGetFeeInstruction(uint64(badChainSelector), message, config.EvmChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() - require.NoError(t, err) - - result := utils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user) - require.NotNil(t, result) - - returnedError := utils.ExtractReturnedError(ctx, t, result.Value.Logs, config.CcipRouterProgram.String()) - require.NotNil(t, returnedError) - require.Equal(t, ConstraintSeedsError, *returnedError) - }) + for _, address := range [][32]byte{tooBigAddress, tooSmallAddress} { + message := ccip_router.Solana2AnyMessage{ + Receiver: address[:], + FeeToken: wsol.mint, + } + billingTokenConfigPDA := getTokenConfigPDA(wsol.mint) - t.Run("Cannot get fee for invalid token", func(t *testing.T) { - const AccountNotInitializedError = 3012 + instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() + require.NoError(t, err) - unsupportedToken := token0 - message := ccip_router.Solana2AnyMessage{ - Receiver: []byte{1, 2, 3}, - FeeToken: solana.PublicKey(unsupportedToken.Mint), + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: InvalidEVMAddress"}) + require.NotNil(t, result) } - - billingTokenConfigPDA := getTokenConfigPDA(unsupportedToken.Mint.PublicKey()) - instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() - require.NoError(t, err) - - result := utils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user) - require.NotNil(t, result) - - returnedError := utils.ExtractReturnedError(ctx, t, result.Value.Logs, config.CcipRouterProgram.String()) - require.NotNil(t, returnedError) - require.Equal(t, AccountNotInitializedError, *returnedError) }) }) @@ -1895,16 +1941,15 @@ func TestCCIPRouter(t *testing.T) { ////////////////////////// t.Run("OnRamp ccipSend", func(t *testing.T) { - t.Parallel() t.Run("When sending to an invalid destination chain selector it fails", func(t *testing.T) { destinationChainSelector := uint64(189) - destinationChainStatePDA, err := getChainStatePDA(destinationChainSelector) + destinationChainStatePDA, err := GetDestChainStatePDA(destinationChainSelector) require.NoError(t, err) message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -1919,7 +1964,9 @@ func TestCCIPRouter(t *testing.T) { wsol.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: AccountNotInitialized"}) require.NotNil(t, result) @@ -1927,14 +1974,14 @@ func TestCCIPRouter(t *testing.T) { t.Run("When sending a Valid CCIP Message Emits CCIPMessageSent", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -1949,16 +1996,18 @@ func TestCCIPRouter(t *testing.T) { wsol.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState + var chainStateAccount ccip_router.DestChain err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") // Do not check source chain config, as it may have been updated by other tests in ccip offramp - require.Equal(t, uint64(1), chainStateAccount.DestChain.State.SequenceNumber) + require.Equal(t, uint64(1), chainStateAccount.State.SequenceNumber) var nonceCounterAccount ccip_router.Nonce err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) @@ -1970,11 +2019,11 @@ func TestCCIPRouter(t *testing.T) { require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) require.Equal(t, uint64(1), ccipMessageSentEvent.SequenceNumber) require.Equal(t, user.PublicKey(), ccipMessageSentEvent.Message.Sender) - require.Equal(t, []byte{1, 2, 3}, ccipMessageSentEvent.Message.Receiver) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) data := [3]uint8{4, 5, 6} require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) - require.Equal(t, bin.Uint128{Lo: 5000, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) - require.Equal(t, false, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) + require.Equal(t, bin.Uint128{Lo: 5000, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) // default gas limit + require.Equal(t, false, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) // default OOO Execution require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) require.Equal(t, uint64(1), ccipMessageSentEvent.Message.Header.SequenceNumber) @@ -1983,11 +2032,11 @@ func TestCCIPRouter(t *testing.T) { t.Run("When sending a CCIP Message with ExtraArgs overrides Emits CCIPMessageSent", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA trueValue := true message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, ExtraArgs: ccip_router.ExtraArgsInput{ GasLimit: &bin.Uint128{Lo: 99, Hi: 0}, @@ -1995,7 +2044,7 @@ func TestCCIPRouter(t *testing.T) { }, } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2010,16 +2059,18 @@ func TestCCIPRouter(t *testing.T) { wsol.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState + var chainStateAccount ccip_router.DestChain err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") // Do not check source chain config, as it may have been updated by other tests in ccip offramp - require.Equal(t, uint64(2), chainStateAccount.DestChain.State.SequenceNumber) + require.Equal(t, uint64(2), chainStateAccount.State.SequenceNumber) var nonceCounterAccount ccip_router.Nonce err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) @@ -2031,30 +2082,153 @@ func TestCCIPRouter(t *testing.T) { require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) require.Equal(t, uint64(2), ccipMessageSentEvent.SequenceNumber) require.Equal(t, user.PublicKey(), ccipMessageSentEvent.Message.Sender) - require.Equal(t, []byte{1, 2, 3}, ccipMessageSentEvent.Message.Receiver) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) data := [3]uint8{4, 5, 6} require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) - require.Equal(t, bin.Uint128{Lo: 99, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) - require.Equal(t, true, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) + require.Equal(t, bin.Uint128{Lo: 99, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) // check it's overwritten + require.Equal(t, true, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) // check it's overwritten require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) require.Equal(t, uint64(2), ccipMessageSentEvent.Message.Header.SequenceNumber) require.Equal(t, uint64(0), ccipMessageSentEvent.Message.Header.Nonce) // nonce is not incremented as it is OOO }) + t.Run("When sending a CCIP Message with only gas limit ExtraArgs overrides Emits CCIPMessageSent", func(t *testing.T) { + destinationChainSelector := config.EvmChainSelector + destinationChainStatePDA := config.EvmDestChainStatePDA + message := ccip_router.Solana2AnyMessage{ + FeeToken: wsol.mint, + Receiver: validReceiverAddress[:], + Data: []byte{4, 5, 6}, + ExtraArgs: ccip_router.ExtraArgsInput{ + GasLimit: &bin.Uint128{Lo: 99, Hi: 0}, + }, + } + + raw := ccip_router.NewCcipSendInstruction( + destinationChainSelector, + message, + config.RouterConfigPDA, + destinationChainStatePDA, + nonceEvmPDA, + user.PublicKey(), + solana.SystemProgramID, + wsol.program, + wsol.mint, + wsol.billingConfigPDA, + wsol.userATA, + wsol.billingATA, + config.BillingSignerPDA, + config.ExternalTokenPoolsSignerPDA, + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) + require.NotNil(t, result) + + var chainStateAccount ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) + require.NoError(t, err, "failed to get account info") + // Do not check source chain config, as it may have been updated by other tests in ccip offramp + require.Equal(t, uint64(3), chainStateAccount.State.SequenceNumber) + + var nonceCounterAccount ccip_router.Nonce + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) + require.NoError(t, err, "failed to get account info") + require.Equal(t, uint64(2), nonceCounterAccount.Counter) + + ccipMessageSentEvent := EventCCIPMessageSent{} + require.NoError(t, utils.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, config.PrintEvents)) + require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) + require.Equal(t, uint64(3), ccipMessageSentEvent.SequenceNumber) + require.Equal(t, user.PublicKey(), ccipMessageSentEvent.Message.Sender) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) + data := [3]uint8{4, 5, 6} + require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) + require.Equal(t, bin.Uint128{Lo: 99, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) // check it's overwritten + require.Equal(t, false, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) // check it's default value + require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) + require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) + require.Equal(t, uint64(3), ccipMessageSentEvent.Message.Header.SequenceNumber) + require.Equal(t, uint64(2), ccipMessageSentEvent.Message.Header.Nonce) // nonce is incremented + }) + + t.Run("When sending a CCIP Message with allow out of order ExtraArgs overrides Emits CCIPMessageSent", func(t *testing.T) { + destinationChainSelector := config.EvmChainSelector + destinationChainStatePDA := config.EvmDestChainStatePDA + trueValue := true + message := ccip_router.Solana2AnyMessage{ + FeeToken: wsol.mint, + Receiver: validReceiverAddress[:], + Data: []byte{4, 5, 6}, + ExtraArgs: ccip_router.ExtraArgsInput{ + AllowOutOfOrderExecution: &trueValue, + }, + } + + raw := ccip_router.NewCcipSendInstruction( + destinationChainSelector, + message, + config.RouterConfigPDA, + destinationChainStatePDA, + nonceEvmPDA, + user.PublicKey(), + solana.SystemProgramID, + wsol.program, + wsol.mint, + wsol.billingConfigPDA, + wsol.userATA, + wsol.billingATA, + config.BillingSignerPDA, + config.ExternalTokenPoolsSignerPDA, + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) + require.NotNil(t, result) + + var chainStateAccount ccip_router.DestChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) + require.NoError(t, err, "failed to get account info") + // Do not check source chain config, as it may have been updated by other tests in ccip offramp + require.Equal(t, uint64(4), chainStateAccount.State.SequenceNumber) + + var nonceCounterAccount ccip_router.Nonce + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) + require.NoError(t, err, "failed to get account info") + require.Equal(t, uint64(2), nonceCounterAccount.Counter) + + ccipMessageSentEvent := EventCCIPMessageSent{} + require.NoError(t, utils.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, config.PrintEvents)) + require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) + require.Equal(t, uint64(4), ccipMessageSentEvent.SequenceNumber) + require.Equal(t, user.PublicKey(), ccipMessageSentEvent.Message.Sender) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) + data := [3]uint8{4, 5, 6} + require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) + require.Equal(t, bin.Uint128{Lo: 5000, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) // default gas limit + require.Equal(t, true, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) // check it's overwritten + require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) + require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) + require.Equal(t, uint64(4), ccipMessageSentEvent.Message.Header.SequenceNumber) + require.Equal(t, uint64(0), ccipMessageSentEvent.Message.Header.Nonce) // nonce is not incremented as it is OOO + }) + t.Run("When gasLimit is set to zero, it overrides Emits CCIPMessageSent", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: token2022.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, ExtraArgs: ccip_router.ExtraArgsInput{ GasLimit: &bin.Uint128{Lo: 0, Hi: 0}, }, } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2069,48 +2243,50 @@ func TestCCIPRouter(t *testing.T) { token2022.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState + var chainStateAccount ccip_router.DestChain err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") // Do not check source chain config, as it may have been updated by other tests in ccip offramp - require.Equal(t, uint64(3), chainStateAccount.DestChain.State.SequenceNumber) + require.Equal(t, uint64(5), chainStateAccount.State.SequenceNumber) var nonceCounterAccount ccip_router.Nonce err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonceEvmPDA, config.DefaultCommitment, &nonceCounterAccount) require.NoError(t, err, "failed to get account info") - require.Equal(t, uint64(2), nonceCounterAccount.Counter) + require.Equal(t, uint64(3), nonceCounterAccount.Counter) ccipMessageSentEvent := EventCCIPMessageSent{} require.NoError(t, utils.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, config.PrintEvents)) require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) - require.Equal(t, uint64(3), ccipMessageSentEvent.SequenceNumber) + require.Equal(t, uint64(5), ccipMessageSentEvent.SequenceNumber) require.Equal(t, user.PublicKey(), ccipMessageSentEvent.Message.Sender) - require.Equal(t, []byte{1, 2, 3}, ccipMessageSentEvent.Message.Receiver) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) data := [3]uint8{4, 5, 6} require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) require.Equal(t, bin.Uint128{Lo: 0, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) require.Equal(t, false, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) - require.Equal(t, uint64(3), ccipMessageSentEvent.Message.Header.SequenceNumber) - require.Equal(t, uint64(2), ccipMessageSentEvent.Message.Header.Nonce) + require.Equal(t, uint64(5), ccipMessageSentEvent.Message.Header.SequenceNumber) + require.Equal(t, uint64(3), ccipMessageSentEvent.Message.Header.Nonce) }) t.Run("When sending a message with an invalid nonce account, it fails", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2125,7 +2301,9 @@ func TestCCIPRouter(t *testing.T) { wsol.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment, []string{"Error Message: A seeds constraint was violated"}) @@ -2134,14 +2312,14 @@ func TestCCIPRouter(t *testing.T) { t.Run("When sending a message impersonating another user, it fails", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2156,15 +2334,51 @@ func TestCCIPRouter(t *testing.T) { wsol.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) utils.SendAndFailWithRPCError(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment, []string{"Transaction signature verification failure"}) }) + t.Run("When sending a message without flagging the user ATA as writable, it fails", func(t *testing.T) { + destinationChainSelector := config.EvmChainSelector + destinationChainStatePDA := config.EvmDestChainStatePDA + message := ccip_router.Solana2AnyMessage{ + FeeToken: wsol.mint, + Receiver: validReceiverAddress[:], + Data: []byte{4, 5, 6}, + } + + raw := ccip_router.NewCcipSendInstruction( + destinationChainSelector, + message, + config.RouterConfigPDA, + destinationChainStatePDA, + nonceEvmPDA, + user.PublicKey(), + solana.SystemProgramID, + wsol.program, + wsol.mint, + wsol.billingConfigPDA, + wsol.userATA, + wsol.billingATA, + config.BillingSignerPDA, + config.ExternalTokenPoolsSignerPDA, + ) + + // do NOT mark the user ATA as writable + + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) + + utils.SendAndFailWithRPCError(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{ccip_router.InvalidInputsAtaWritable_CcipRouterError.String()}) + }) + t.Run("When sending a message and paying with inconsistent fee token accounts, it fails", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA // These testcases are a quite a lot, this obviously blows up combinatorially and adds many seconds to the suite. // We can remove/reduce this, but I used it during development so for now I'm keeping them here @@ -2181,11 +2395,11 @@ func TestCCIPRouter(t *testing.T) { testName := fmt.Sprintf("when using program %v, mint %v, message mint %v, configPDA %v, userATA %v, billingATA %v", i, j, k, l, m, n) t.Run(testName, func(t *testing.T) { t.Parallel() - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, ccip_router.Solana2AnyMessage{ FeeToken: messageMint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, }, config.RouterConfigPDA, @@ -2200,7 +2414,9 @@ func TestCCIPRouter(t *testing.T) { billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) // Given the mixture of inputs, there can be different error types here, so just check that it fails but not each message @@ -2216,16 +2432,16 @@ func TestCCIPRouter(t *testing.T) { t.Run("When another user sending a Valid CCIP Message tries to pay with some else's tokens it fails", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: token2022.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } anotherUserNonceEVMPDA, err := getNoncePDA(config.EvmChainSelector, anotherUser.PublicKey()) require.NoError(t, err) - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2240,23 +2456,25 @@ func TestCCIPRouter(t *testing.T) { token2022.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment, []string{ccip_router.InvalidInputs_CcipRouterError.String()}) }) t.Run("When another user sending a Valid CCIP Message Emits CCIPMessageSent", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: token2022.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } anotherUserNonceEVMPDA, err := getNoncePDA(config.EvmChainSelector, anotherUser.PublicKey()) require.NoError(t, err) - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2271,16 +2489,18 @@ func TestCCIPRouter(t *testing.T) { token2022.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment) require.NotNil(t, result) - var chainStateAccount ccip_router.ChainState + var chainStateAccount ccip_router.DestChain err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, destinationChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") // Do not check source chain config, as it may have been updated by other tests in ccip offramp - require.Equal(t, uint64(4), chainStateAccount.DestChain.State.SequenceNumber) + require.Equal(t, uint64(6), chainStateAccount.State.SequenceNumber) var nonceCounterAccount ccip_router.Nonce err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, anotherUserNonceEVMPDA, config.DefaultCommitment, &nonceCounterAccount) @@ -2290,16 +2510,16 @@ func TestCCIPRouter(t *testing.T) { ccipMessageSentEvent := EventCCIPMessageSent{} require.NoError(t, utils.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, config.PrintEvents)) require.Equal(t, uint64(21), ccipMessageSentEvent.DestinationChainSelector) - require.Equal(t, uint64(4), ccipMessageSentEvent.SequenceNumber) + require.Equal(t, uint64(6), ccipMessageSentEvent.SequenceNumber) require.Equal(t, anotherUser.PublicKey(), ccipMessageSentEvent.Message.Sender) - require.Equal(t, []byte{1, 2, 3}, ccipMessageSentEvent.Message.Receiver) + require.Equal(t, validReceiverAddress[:], ccipMessageSentEvent.Message.Receiver) data := [3]uint8{4, 5, 6} require.Equal(t, data[:], ccipMessageSentEvent.Message.Data) require.Equal(t, bin.Uint128{Lo: 5000, Hi: 0}, ccipMessageSentEvent.Message.ExtraArgs.GasLimit) require.Equal(t, false, ccipMessageSentEvent.Message.ExtraArgs.AllowOutOfOrderExecution) require.Equal(t, uint64(15), ccipMessageSentEvent.Message.Header.SourceChainSelector) require.Equal(t, uint64(21), ccipMessageSentEvent.Message.Header.DestChainSelector) - require.Equal(t, uint64(4), ccipMessageSentEvent.Message.Header.SequenceNumber) + require.Equal(t, uint64(6), ccipMessageSentEvent.Message.Header.SequenceNumber) require.Equal(t, uint64(1), ccipMessageSentEvent.Message.Header.Nonce) }) @@ -2310,10 +2530,10 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, err) destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, TokenAmounts: []ccip_router.SolanaTokenAmount{ { @@ -2343,6 +2563,7 @@ func TestCCIPRouter(t *testing.T) { config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, ) + base.GetFeeTokenUserAssociatedAccountAccount().WRITE() tokenMetas, addressTables, err := ParseTokenLookupTable(ctx, solanaGoClient, token0, userTokenAccount) require.NoError(t, err) @@ -2390,10 +2611,10 @@ func TestCCIPRouter(t *testing.T) { t.Parallel() // base transaction destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA message := ccip_router.Solana2AnyMessage{ FeeToken: wsol.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, TokenAmounts: []ccip_router.SolanaTokenAmount{ { @@ -2491,6 +2712,7 @@ func TestCCIPRouter(t *testing.T) { config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, ) + tx.GetFeeTokenUserAssociatedAccountAccount().WRITE() tokenMetas, addressTables, err := ParseTokenLookupTable(ctx, solanaGoClient, token0, userTokenAccount) require.NoError(t, err) @@ -2512,30 +2734,28 @@ func TestCCIPRouter(t *testing.T) { t.Run("When sending a Valid CCIP Message it bills the amount that getFee previously returned", func(t *testing.T) { destinationChainSelector := config.EvmChainSelector - destinationChainStatePDA := config.EvmChainStatePDA + destinationChainStatePDA := config.EvmDestChainStatePDA for _, token := range billingTokens { t.Run("using "+token.name, func(t *testing.T) { message := ccip_router.Solana2AnyMessage{ FeeToken: token.mint, - Receiver: []byte{1, 2, 3}, + Receiver: validReceiverAddress[:], Data: []byte{4, 5, 6}, } - // getFee - billingTokenConfigPDA := getTokenConfigPDA(token.mint) - ix, ferr := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() + ix, ferr := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, token.billingConfigPDA).ValidateAndBuild() require.NoError(t, ferr) feeResult := utils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{ix}, user) require.NotNil(t, feeResult) fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Value.Logs, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) - require.Greater(t, fee, uint64(0)) + require.Equal(t, uint64(1), fee) initialBalance := getBalance(token.billingATA) // ccipSend - instruction, err := ccip_router.NewCcipSendInstruction( + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, config.RouterConfigPDA, @@ -2550,7 +2770,9 @@ func TestCCIPRouter(t *testing.T) { token.billingATA, config.BillingSignerPDA, config.ExternalTokenPoolsSignerPDA, - ).ValidateAndBuild() + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) require.NotNil(t, result) @@ -2563,6 +2785,99 @@ func TestCCIPRouter(t *testing.T) { } }) + t.Run("When sending a Valid CCIP Message but the user does not have enough funds of the fee token, it fails", func(t *testing.T) { + message := ccip_router.Solana2AnyMessage{ + FeeToken: token2022.mint, + Receiver: validReceiverAddress[:], + Data: []byte{4, 5, 6}, + } + + noncePDA, err := getNoncePDA(config.EvmChainSelector, tokenlessUser.PublicKey()) + require.NoError(t, err) + + // ccipSend + raw := ccip_router.NewCcipSendInstruction( + config.EvmChainSelector, + message, + config.RouterConfigPDA, + config.EvmDestChainStatePDA, + noncePDA, + tokenlessUser.PublicKey(), // this user has 0 token2022 balance, though they've approved the transfer + solana.SystemProgramID, + token2022.program, + token2022.mint, + token2022.billingConfigPDA, + token2022.tokenlessUserATA, + token2022.billingATA, + config.BillingSignerPDA, + config.ExternalTokenPoolsSignerPDA, + ) + raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, tokenlessUser, config.DefaultCommitment, []string{"insufficient funds"}) + }) + + t.Run("When sending a valid CCIP message and paying in native SOL, it bills the same amount that getFee previously returned and it's accumulated as Wrapped SOL", func(t *testing.T) { + getLamports := func(account solana.PublicKey) uint64 { + out, err := solanaGoClient.GetBalance(ctx, account, rpc.CommitmentConfirmed) + require.NoError(t, err) + return out.Value + } + + zeroPubkey := solana.PublicKeyFromBytes(make([]byte, 32)) + + message := ccip_router.Solana2AnyMessage{ + FeeToken: zeroPubkey, // will pay with native SOL + Receiver: validReceiverAddress[:], + Data: []byte{4, 5, 6}, + } + + // getFee + ix, ferr := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA).ValidateAndBuild() + require.NoError(t, ferr) + + feeResult := utils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{ix}, user) + require.NotNil(t, feeResult) + fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Value.Logs, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) + require.Greater(t, fee, uint64(0)) + + initialBalance := getBalance(wsol.billingATA) + initialLamports := getLamports(user.PublicKey()) + + // ccipSend + raw := ccip_router.NewCcipSendInstruction( + config.EvmChainSelector, + message, + config.RouterConfigPDA, + config.EvmDestChainStatePDA, + nonceEvmPDA, + user.PublicKey(), + solana.SystemProgramID, + wsol.program, + wsol.mint, + wsol.billingConfigPDA, + zeroPubkey, // no user token account, because paying with native SOL + wsol.billingATA, + config.BillingSignerPDA, + config.ExternalTokenPoolsSignerPDA, + ) + + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) + require.NotNil(t, result) + + finalBalance := getBalance(wsol.billingATA) + finalLamports := getLamports(user.PublicKey()) + + // Check that the billing receiver account balance has increased by the fee amount + require.Equal(t, fee, finalBalance-initialBalance) + + // Check that the user has paid for the tx cost and the ccip fee from their SOL + require.Equal(t, fee+result.Meta.Fee, initialLamports-finalLamports) + }) + //////////////////// // Billing config // //////////////////// @@ -2604,13 +2919,24 @@ func TestCCIPRouter(t *testing.T) { t.Run("Commit", func(t *testing.T) { currentMinSeqNr := uint64(1) + oldReportContext := CreateReportContext(1) // use old sequence number + + type Comparator int + const ( + Less Comparator = iota + Equal + Greater + ) + t.Run("When committing a report with a valid source chain selector, merkle root and interval it succeeds", func(t *testing.T) { priceUpdatesCases := []struct { - Name string - PriceUpdates ccip_router.PriceUpdates - RemainingAccounts []solana.PublicKey - RunEventValidations func(t *testing.T, tx *rpc.GetTransactionResult) - RunStateValidations func(t *testing.T) + Name string + PriceUpdates ccip_router.PriceUpdates + RemainingAccounts []solana.PublicKey + RunEventValidations func(t *testing.T, tx *rpc.GetTransactionResult) + RunStateValidations func(t *testing.T) + ReportContext *[3][32]byte + PriceSequenceComparator Comparator }{ { Name: "No price updates", @@ -2620,23 +2946,24 @@ func TestCCIPRouter(t *testing.T) { require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerTokenUpdated", nil, config.PrintEvents), "event not found") require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerUnitGasUpdated", nil, config.PrintEvents), "event not found") }, - RunStateValidations: func(t *testing.T) {}, + RunStateValidations: func(t *testing.T) {}, + PriceSequenceComparator: Greater, // it is a newer commit but with no price update }, { Name: "Single token price update", PriceUpdates: ccip_router.PriceUpdates{ TokenPriceUpdates: []ccip_router.TokenPriceUpdate{{ SourceToken: wsol.mint, - UsdPerToken: utils.To28BytesLE(1), + UsdPerToken: utils.To28BytesBE(1), }}, }, - RemainingAccounts: []solana.PublicKey{wsol.billingConfigPDA}, + RemainingAccounts: []solana.PublicKey{config.RouterStatePDA, wsol.billingConfigPDA}, RunEventValidations: func(t *testing.T, tx *rpc.GetTransactionResult) { // yes token update var update UsdPerTokenUpdated require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerTokenUpdated", &update, config.PrintEvents)) require.Greater(t, update.Timestamp, int64(0)) // timestamp is set - require.Equal(t, utils.To28BytesLE(1), update.Value) + require.Equal(t, utils.To28BytesBE(1), update.Value) // no gas updates require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerUnitGasUpdated", nil, config.PrintEvents), "event not found") @@ -2644,19 +2971,20 @@ func TestCCIPRouter(t *testing.T) { RunStateValidations: func(t *testing.T) { var tokenConfig ccip_router.BillingTokenConfigWrapper require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, wsol.billingConfigPDA, config.DefaultCommitment, &tokenConfig)) - require.Equal(t, utils.To28BytesLE(1), tokenConfig.Config.UsdPerToken.Value) + require.Equal(t, utils.To28BytesBE(1), tokenConfig.Config.UsdPerToken.Value) require.Greater(t, tokenConfig.Config.UsdPerToken.Timestamp, int64(0)) }, + PriceSequenceComparator: Equal, }, { Name: "Single gas price update on same chain as commit message", PriceUpdates: ccip_router.PriceUpdates{ GasPriceUpdates: []ccip_router.GasPriceUpdate{{ DestChainSelector: config.EvmChainSelector, - UsdPerUnitGas: utils.To28BytesLE(1), + UsdPerUnitGas: utils.To28BytesBE(1), }}, }, - RemainingAccounts: []solana.PublicKey{config.EvmChainStatePDA}, + RemainingAccounts: []solana.PublicKey{config.RouterStatePDA, config.EvmDestChainStatePDA}, RunEventValidations: func(t *testing.T, tx *rpc.GetTransactionResult) { // no token updates require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerTokenUpdated", nil, config.PrintEvents), "event not found") @@ -2665,57 +2993,56 @@ func TestCCIPRouter(t *testing.T) { var update UsdPerUnitGasUpdated require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerUnitGasUpdated", &update, config.PrintEvents)) require.Greater(t, update.Timestamp, int64(0)) // timestamp is set - require.Equal(t, utils.To28BytesLE(1), update.Value) + require.Equal(t, utils.To28BytesBE(1), update.Value) }, RunStateValidations: func(t *testing.T) { - var chainState ccip_router.ChainState - require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &chainState)) - require.Equal(t, utils.To28BytesLE(1), chainState.DestChain.State.UsdPerUnitGas.Value) - require.Greater(t, chainState.DestChain.State.UsdPerUnitGas.Timestamp, int64(0)) + var chainState ccip_router.DestChain + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &chainState)) + require.Equal(t, utils.To28BytesBE(1), chainState.State.UsdPerUnitGas.Value) + require.Greater(t, chainState.State.UsdPerUnitGas.Timestamp, int64(0)) }, + PriceSequenceComparator: Equal, }, { Name: "Single gas price update on different chain (Solana) as commit message (EVM)", PriceUpdates: ccip_router.PriceUpdates{ GasPriceUpdates: []ccip_router.GasPriceUpdate{{ DestChainSelector: config.SolanaChainSelector, - UsdPerUnitGas: utils.To28BytesLE(2), + UsdPerUnitGas: utils.To28BytesBE(2), }}, }, - RemainingAccounts: []solana.PublicKey{config.SolanaChainStatePDA}, + RemainingAccounts: []solana.PublicKey{config.RouterStatePDA, config.SolanaDestChainStatePDA}, RunEventValidations: func(t *testing.T, tx *rpc.GetTransactionResult) { // no token updates require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerTokenUpdated", nil, config.PrintEvents), "event not found") - fmt.Print(tx.Meta.LogMessages) // TODO remove - // yes gas update var update UsdPerUnitGasUpdated require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerUnitGasUpdated", &update, config.PrintEvents)) require.Greater(t, update.Timestamp, int64(0)) // timestamp is set - require.Equal(t, utils.To28BytesLE(2), update.Value) + require.Equal(t, utils.To28BytesBE(2), update.Value) }, RunStateValidations: func(t *testing.T) { - var chainState ccip_router.ChainState - require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaChainStatePDA, config.DefaultCommitment, &chainState)) - fmt.Printf("chainState: %v\n", chainState) // TODO remove - require.Equal(t, utils.To28BytesLE(2), chainState.DestChain.State.UsdPerUnitGas.Value) - require.Greater(t, chainState.DestChain.State.UsdPerUnitGas.Timestamp, int64(0)) + var chainState ccip_router.DestChain + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaDestChainStatePDA, config.DefaultCommitment, &chainState)) + require.Equal(t, utils.To28BytesBE(2), chainState.State.UsdPerUnitGas.Value) + require.Greater(t, chainState.State.UsdPerUnitGas.Timestamp, int64(0)) }, + PriceSequenceComparator: Equal, }, { Name: "Multiple token & gas updates", PriceUpdates: ccip_router.PriceUpdates{ TokenPriceUpdates: []ccip_router.TokenPriceUpdate{ - {SourceToken: wsol.mint, UsdPerToken: utils.To28BytesLE(3)}, - {SourceToken: token2022.mint, UsdPerToken: utils.To28BytesLE(4)}, + {SourceToken: wsol.mint, UsdPerToken: utils.To28BytesBE(3)}, + {SourceToken: token2022.mint, UsdPerToken: utils.To28BytesBE(4)}, }, GasPriceUpdates: []ccip_router.GasPriceUpdate{ - {DestChainSelector: config.EvmChainSelector, UsdPerUnitGas: utils.To28BytesLE(5)}, - {DestChainSelector: config.SolanaChainSelector, UsdPerUnitGas: utils.To28BytesLE(6)}, + {DestChainSelector: config.EvmChainSelector, UsdPerUnitGas: utils.To28BytesBE(5)}, + {DestChainSelector: config.SolanaChainSelector, UsdPerUnitGas: utils.To28BytesBE(6)}, }, }, - RemainingAccounts: []solana.PublicKey{wsol.billingConfigPDA, token2022.billingConfigPDA, config.EvmChainStatePDA, config.SolanaChainStatePDA}, + RemainingAccounts: []solana.PublicKey{config.RouterStatePDA, wsol.billingConfigPDA, token2022.billingConfigPDA, config.EvmDestChainStatePDA, config.SolanaDestChainStatePDA}, RunEventValidations: func(t *testing.T, tx *rpc.GetTransactionResult) { // yes multiple token updates tokenUpdates, err := utils.ParseMultipleEvents[UsdPerTokenUpdated](tx.Meta.LogMessages, "UsdPerTokenUpdated", config.PrintEvents) @@ -2726,10 +3053,10 @@ func TestCCIPRouter(t *testing.T) { switch tokenUpdate.Token { case wsol.mint: eventWsol = true - require.Equal(t, utils.To28BytesLE(3), tokenUpdate.Value) + require.Equal(t, utils.To28BytesBE(3), tokenUpdate.Value) case token2022.mint: eventToken2022 = true - require.Equal(t, utils.To28BytesLE(4), tokenUpdate.Value) + require.Equal(t, utils.To28BytesBE(4), tokenUpdate.Value) default: t.Fatalf("unexpected token update: %v", tokenUpdate) } @@ -2747,10 +3074,10 @@ func TestCCIPRouter(t *testing.T) { switch gasUpdate.DestChain { case config.EvmChainSelector: eventEvm = true - require.Equal(t, utils.To28BytesLE(5), gasUpdate.Value) + require.Equal(t, utils.To28BytesBE(5), gasUpdate.Value) case config.SolanaChainSelector: eventSolana = true - require.Equal(t, utils.To28BytesLE(6), gasUpdate.Value) + require.Equal(t, utils.To28BytesBE(6), gasUpdate.Value) default: t.Fatalf("unexpected gas update: %v", gasUpdate) } @@ -2762,24 +3089,57 @@ func TestCCIPRouter(t *testing.T) { RunStateValidations: func(t *testing.T) { var wsolTokenConfig ccip_router.BillingTokenConfigWrapper require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, wsol.billingConfigPDA, config.DefaultCommitment, &wsolTokenConfig)) - require.Equal(t, utils.To28BytesLE(3), wsolTokenConfig.Config.UsdPerToken.Value) + require.Equal(t, utils.To28BytesBE(3), wsolTokenConfig.Config.UsdPerToken.Value) require.Greater(t, wsolTokenConfig.Config.UsdPerToken.Timestamp, int64(0)) var token2022Config ccip_router.BillingTokenConfigWrapper require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, token2022.billingConfigPDA, config.DefaultCommitment, &token2022Config)) - require.Equal(t, utils.To28BytesLE(4), token2022Config.Config.UsdPerToken.Value) + require.Equal(t, utils.To28BytesBE(4), token2022Config.Config.UsdPerToken.Value) require.Greater(t, token2022Config.Config.UsdPerToken.Timestamp, int64(0)) - var evmChainState ccip_router.ChainState - require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &evmChainState)) - require.Equal(t, utils.To28BytesLE(5), evmChainState.DestChain.State.UsdPerUnitGas.Value) - require.Greater(t, evmChainState.DestChain.State.UsdPerUnitGas.Timestamp, int64(0)) + var evmChainState ccip_router.DestChain + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &evmChainState)) + require.Equal(t, utils.To28BytesBE(5), evmChainState.State.UsdPerUnitGas.Value) + require.Greater(t, evmChainState.State.UsdPerUnitGas.Timestamp, int64(0)) + + var solanaChainState ccip_router.DestChain + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaDestChainStatePDA, config.DefaultCommitment, &solanaChainState)) + require.Equal(t, utils.To28BytesBE(6), solanaChainState.State.UsdPerUnitGas.Value) + require.Greater(t, solanaChainState.State.UsdPerUnitGas.Timestamp, int64(0)) + }, + PriceSequenceComparator: Equal, + }, + { + Name: "Valid price updates but old sequence number, so updates are ignored", + PriceUpdates: ccip_router.PriceUpdates{ + TokenPriceUpdates: []ccip_router.TokenPriceUpdate{ + {SourceToken: wsol.mint, UsdPerToken: utils.To28BytesBE(1)}, + }, + GasPriceUpdates: []ccip_router.GasPriceUpdate{ + {DestChainSelector: config.EvmChainSelector, UsdPerUnitGas: utils.To28BytesBE(1)}, + }, + }, + RemainingAccounts: []solana.PublicKey{config.RouterStatePDA, wsol.billingConfigPDA, config.EvmDestChainStatePDA}, + ReportContext: &oldReportContext, + RunEventValidations: func(t *testing.T, tx *rpc.GetTransactionResult) { + // no events as updates are ignored (but commit is still accepted) + require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerTokenUpdated", nil, config.PrintEvents), "event not found") + require.ErrorContains(t, utils.ParseEvent(tx.Meta.LogMessages, "UsdPerUnitGasUpdated", nil, config.PrintEvents), "event not found") + }, + RunStateValidations: func(t *testing.T) { + var wsolTokenConfig ccip_router.BillingTokenConfigWrapper + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, wsol.billingConfigPDA, config.DefaultCommitment, &wsolTokenConfig)) + // the price is NOT the one sent in this commit + require.NotEqual(t, utils.To28BytesBE(1), wsolTokenConfig.Config.UsdPerToken.Value) + require.Greater(t, wsolTokenConfig.Config.UsdPerToken.Timestamp, int64(0)) - var solanaChainState ccip_router.ChainState - require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.SolanaChainStatePDA, config.DefaultCommitment, &solanaChainState)) - require.Equal(t, utils.To28BytesLE(6), solanaChainState.DestChain.State.UsdPerUnitGas.Value) - require.Greater(t, solanaChainState.DestChain.State.UsdPerUnitGas.Timestamp, int64(0)) + var evmChainState ccip_router.DestChain + require.NoError(t, utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &evmChainState)) + // the price is NOT the one sent in this commit + require.NotEqual(t, utils.To28BytesBE(1), evmChainState.State.UsdPerUnitGas.Value) + require.Greater(t, evmChainState.State.UsdPerUnitGas.Timestamp, int64(0)) }, + PriceSequenceComparator: Less, // it is an older commit, so price update is ignored and state remains ahead of this commit }, } @@ -2806,17 +3166,28 @@ func TestCCIPRouter(t *testing.T) { }, PriceUpdates: testcase.PriceUpdates, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + + var reportContext [3][32]byte + var reportSequence uint64 + if testcase.ReportContext != nil { + reportContext = *testcase.ReportContext + reportSequence = ParseSequenceNumber(reportContext) + } else { + reportContext = NextCommitReportContext() + reportSequence = ReportSequence() + } + + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() raw := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -2842,12 +3213,12 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "Transmitted", &transmittedEvent, config.PrintEvents)) require.Equal(t, config.ConfigDigest, transmittedEvent.ConfigDigest) require.Equal(t, uint8(utils.OcrCommitPlugin), transmittedEvent.OcrPluginType) - require.Equal(t, config.ReportSequence, transmittedEvent.SequenceNumber) + require.Equal(t, reportSequence, transmittedEvent.SequenceNumber) - var chainStateAccount ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &chainStateAccount) + var chainStateAccount ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") - require.Equal(t, currentMinSeqNr, chainStateAccount.SourceChain.State.MinSeqNr) // state now holds the "advanced outer" sequence number, which is the minimum for the next report + require.Equal(t, currentMinSeqNr, chainStateAccount.State.MinSeqNr) // state now holds the "advanced outer" sequence number, which is the minimum for the next report // Do not check dest chain config, as it may have been updated by other tests in ccip onramp var rootAccount ccip_router.CommitReport @@ -2855,6 +3226,19 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, err, "failed to get account info") require.NotEqual(t, bin.Uint128{Lo: 0, Hi: 0}, rootAccount.Timestamp) + var globalState ccip_router.GlobalState + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.RouterStatePDA, config.DefaultCommitment, &globalState) + require.NoError(t, err) + + switch testcase.PriceSequenceComparator { + case Less: + require.Less(t, reportSequence, globalState.LatestPriceSequenceNumber) + case Equal: + require.Equal(t, reportSequence, globalState.LatestPriceSequenceNumber) + case Greater: + require.Greater(t, reportSequence, globalState.LatestPriceSequenceNumber) + } + testcase.RunEventValidations(t, tx) testcase.RunStateValidations(t) }) @@ -2865,7 +3249,7 @@ func TestCCIPRouter(t *testing.T) { t.Run("When committing a report with an invalid source chain selector it fails", func(t *testing.T) { t.Parallel() sourceChainSelector := uint64(34) - sourceChainStatePDA, err := getChainStatePDA(sourceChainSelector) + sourceChainStatePDA, err := GetSourceChainStatePDA(sourceChainSelector) require.NoError(t, err) _, root := MakeEvmToSolanaMessage(t, config.CcipReceiverProgram, sourceChainSelector, config.SolanaChainSelector, []byte{4, 5, 6}) rootPDA, err := GetCommitReportPDA(sourceChainSelector, root) @@ -2883,11 +3267,12 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, @@ -2919,15 +3304,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -2955,15 +3341,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -2991,15 +3378,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3027,15 +3415,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3064,15 +3453,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3083,11 +3473,10 @@ func TestCCIPRouter(t *testing.T) { }) t.Run("Invalid price updates", func(t *testing.T) { - randomToken, err := solana.PublicKeyFromBase58("AGDpGy7auzgKT8zt6qhfHFm1rDwvqQGGTYxuYn7MtydQ") // just some non-existing token - require.NoError(t, err) + randomToken := solana.MustPublicKeyFromBase58("AGDpGy7auzgKT8zt6qhfHFm1rDwvqQGGTYxuYn7MtydQ") // just some non-existing token randomChain := uint64(123456) // just some non-existing chain - randomChainPDA, err := getChainStatePDA(randomChain) + randomChainPDA, err := GetDestChainStatePDA(randomChain) require.NoError(t, err) testcases := []struct { @@ -3120,8 +3509,8 @@ func TestCCIPRouter(t *testing.T) { // in twice, in which case the resulting permissions are the sum of both instances. As only one is manually constructed here, // the other one is always writable (handled by the auto-generated code). Name: "with a non-writable chain state account (different from the message source chain)", - GasChainSelectors: []uint64{config.SolanaChainSelector}, // the message source chain is EVM - AccountMetaSlice: solana.AccountMetaSlice{solana.Meta(config.SolanaChainStatePDA)}, // not writable + GasChainSelectors: []uint64{config.SolanaChainSelector}, // the message source chain is EVM + AccountMetaSlice: solana.AccountMetaSlice{solana.Meta(config.SolanaDestChainStatePDA)}, // not writable ExpectedError: ccip_router.InvalidInputs_CcipRouterError.String(), }, { @@ -3133,7 +3522,7 @@ func TestCCIPRouter(t *testing.T) { { Name: "with the wrong chain state account for a valid gas update", GasChainSelectors: []uint64{config.SolanaChainSelector}, - AccountMetaSlice: solana.AccountMetaSlice{solana.Meta(config.EvmChainStatePDA).WRITE()}, // mismatch chain + AccountMetaSlice: solana.AccountMetaSlice{solana.Meta(config.EvmDestChainStatePDA).WRITE()}, // mismatch chain ExpectedError: ccip_router.InvalidInputs_CcipRouterError.String(), }, { @@ -3145,6 +3534,7 @@ func TestCCIPRouter(t *testing.T) { }, // TODO right now I'm allowing sending too many remaining_accounts, but if we want to be restrictive with that we can add a test here } + _, root := MakeEvmToSolanaMessage(t, config.CcipReceiverProgram, config.EvmChainSelector, config.SolanaChainSelector, []byte{1, 2, 3}) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) @@ -3160,13 +3550,13 @@ func TestCCIPRouter(t *testing.T) { for i, token := range testcase.Tokens { priceUpdates.TokenPriceUpdates[i] = ccip_router.TokenPriceUpdate{ SourceToken: token, - UsdPerToken: utils.To28BytesLE(uint64(i)), + UsdPerToken: utils.To28BytesBE(uint64(i)), } } for i, chainSelector := range testcase.GasChainSelectors { priceUpdates.GasPriceUpdates[i] = ccip_router.GasPriceUpdate{ DestChainSelector: chainSelector, - UsdPerUnitGas: utils.To28BytesLE(uint64(i)), + UsdPerUnitGas: utils.To28BytesBE(uint64(i)), } } @@ -3182,21 +3572,23 @@ func TestCCIPRouter(t *testing.T) { }, PriceUpdates: priceUpdates, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) raw := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, solana.SysVarInstructionsPubkey, ) + raw.AccountMetaSlice.Append(solana.Meta(config.RouterStatePDA).WRITE()) for _, meta := range testcase.AccountMetaSlice { raw.AccountMetaSlice.Append(meta) } @@ -3228,15 +3620,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3258,12 +3651,12 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "Transmitted", &transmittedEvent, config.PrintEvents)) require.Equal(t, config.ConfigDigest, transmittedEvent.ConfigDigest) require.Equal(t, uint8(utils.OcrCommitPlugin), transmittedEvent.OcrPluginType) - require.Equal(t, config.ReportSequence, transmittedEvent.SequenceNumber) + require.Equal(t, ReportSequence(), transmittedEvent.SequenceNumber) - var chainStateAccount ccip_router.ChainState - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmChainStatePDA, config.DefaultCommitment, &chainStateAccount) + var chainStateAccount ccip_router.SourceChain + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmSourceChainStatePDA, config.DefaultCommitment, &chainStateAccount) require.NoError(t, err, "failed to get account info") - require.Equal(t, currentMinSeqNr, chainStateAccount.SourceChain.State.MinSeqNr) + require.Equal(t, currentMinSeqNr, chainStateAccount.State.MinSeqNr) // Do not check dest chain config, as it may have been updated by other tests in ccip onramp var rootAccount ccip_router.CommitReport @@ -3291,7 +3684,8 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) transmitter := getTransmitter() emptyReportContext := [3][32]byte{} @@ -3301,7 +3695,7 @@ func TestCCIPRouter(t *testing.T) { report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3329,15 +3723,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, user.PublicKey(), solana.SystemProgramID, @@ -3365,7 +3760,8 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - hash, err := HashCommitReport(config.ReportContext, report) + reportContext := NextCommitReportContext() + hash, err := HashCommitReport(reportContext, report) require.NoError(t, err) baseSig := ecdsa.SignCompact(secp256k1.PrivKeyFromBytes(signers[0].PrivateKey), hash, false) @@ -3377,11 +3773,11 @@ func TestCCIPRouter(t *testing.T) { transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3413,11 +3809,11 @@ func TestCCIPRouter(t *testing.T) { transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + NextCommitReportContext(), report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3445,10 +3841,11 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) - hash, err := HashCommitReport(config.ReportContext, report) + hash, err := HashCommitReport(reportContext, report) require.NoError(t, err) randomPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) @@ -3460,11 +3857,11 @@ func TestCCIPRouter(t *testing.T) { transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3492,17 +3889,18 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, report, signers) + reportContext := NextCommitReportContext() + sigs, err := SignCommitReport(reportContext, report, signers) require.NoError(t, err) sigs[0] = sigs[1] transmitter := getTransmitter() instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, report, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3520,6 +3918,8 @@ func TestCCIPRouter(t *testing.T) { t.Run("Execute", func(t *testing.T) { var executedSequenceNumber uint64 + reportContext := NextCommitReportContext() // reuse the same commit for all executions + t.Run("When executing a report with merkle tree of size 1, it succeeds", func(t *testing.T) { transmitter := getTransmitter() @@ -3539,17 +3939,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3568,9 +3968,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3624,17 +4024,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3655,9 +4055,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3694,17 +4094,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3715,7 +4115,9 @@ func TestCCIPRouter(t *testing.T) { event := EventCommitReportAccepted{} require.NoError(t, utils.ParseEvent(tx.Meta.LogMessages, "CommitReportAccepted", &event, config.PrintEvents)) - unsupportedChainStatePDA, err := getChainStatePDA(unsupportedChainSelector) + unsupportedSourceChainStatePDA, err := GetSourceChainStatePDA(unsupportedChainSelector) + require.NoError(t, err) + unsupportedDestChainStatePDA, err := GetDestChainStatePDA(unsupportedChainSelector) require.NoError(t, err) message.Header.SourceChainSelector = unsupportedChainSelector message.Header.SequenceNumber = 1 @@ -3724,7 +4126,8 @@ func TestCCIPRouter(t *testing.T) { unsupportedChainSelector, validSourceChainConfig, validDestChainConfig, - unsupportedChainStatePDA, + unsupportedSourceChainStatePDA, + unsupportedDestChainStatePDA, config.RouterConfigPDA, anotherAdmin.PublicKey(), solana.SystemProgramID, @@ -3741,9 +4144,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - unsupportedChainStatePDA, + unsupportedSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3783,17 +4186,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3812,9 +4215,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3850,9 +4253,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3894,9 +4297,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -3943,17 +4346,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -3972,9 +4375,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport1, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4004,9 +4407,9 @@ func TestCCIPRouter(t *testing.T) { } raw = ccip_router.NewExecuteInstruction( executionReport2, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4072,17 +4475,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -4101,9 +4504,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4156,17 +4559,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -4187,9 +4590,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4245,7 +4648,7 @@ func TestCCIPRouter(t *testing.T) { raw := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, user.PublicKey(), @@ -4288,17 +4691,17 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -4321,7 +4724,7 @@ func TestCCIPRouter(t *testing.T) { raw := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, user.PublicKey(), @@ -4355,7 +4758,7 @@ func TestCCIPRouter(t *testing.T) { raw := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4398,7 +4801,7 @@ func TestCCIPRouter(t *testing.T) { raw := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, user.PublicKey(), @@ -4448,7 +4851,7 @@ func TestCCIPRouter(t *testing.T) { raw := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4507,7 +4910,7 @@ func TestCCIPRouter(t *testing.T) { {Pubkey: config.CcipRouterProgram, IsWritable: false}, {Pubkey: config.RouterConfigPDA, IsWritable: false}, {Pubkey: config.ReceiverExternalExecutionConfigPDA, IsWritable: true}, - {Pubkey: config.EvmChainStatePDA, IsWritable: true}, + {Pubkey: config.EvmSourceChainStatePDA, IsWritable: true}, {Pubkey: receiverContractEvmPDA, IsWritable: true}, {Pubkey: solana.SystemProgramID, IsWritable: false}, } @@ -4526,16 +4929,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, _, _ := solana.FindProgramAddress([][]byte{[]byte("commit_report"), config.EvmChainLE, root[:]}, config.CcipRouterProgram) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -4554,9 +4957,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4576,7 +4979,7 @@ func TestCCIPRouter(t *testing.T) { solana.NewAccountMeta(config.CcipRouterProgram, false, false), solana.NewAccountMeta(config.RouterConfigPDA, false, false), solana.NewAccountMeta(config.ReceiverExternalExecutionConfigPDA, true, false), - solana.NewAccountMeta(config.EvmChainStatePDA, true, false), + solana.NewAccountMeta(config.EvmSourceChainStatePDA, true, false), solana.NewAccountMeta(receiverContractEvmPDA, true, false), solana.NewAccountMeta(solana.SystemProgramID, false, false), ) @@ -4618,16 +5021,16 @@ func TestCCIPRouter(t *testing.T) { MerkleRoot: root, }, } - sigs, err := SignCommitReport(config.ReportContext, commitReport, signers) + sigs, err := SignCommitReport(reportContext, commitReport, signers) require.NoError(t, err) rootPDA, err := GetCommitReportPDA(config.EvmChainSelector, root) require.NoError(t, err) instruction, err := ccip_router.NewCommitInstruction( - config.ReportContext, + reportContext, commitReport, sigs, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, transmitter.PublicKey(), solana.SystemProgramID, @@ -4650,9 +5053,9 @@ func TestCCIPRouter(t *testing.T) { } raw := ccip_router.NewExecuteInstruction( executionReport, - config.ReportContext, + reportContext, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, transmitter.PublicKey(), @@ -4680,7 +5083,7 @@ func TestCCIPRouter(t *testing.T) { rawManual := ccip_router.NewManuallyExecuteInstruction( executionReport, config.RouterConfigPDA, - config.EvmChainStatePDA, + config.EvmSourceChainStatePDA, rootPDA, config.ExternalExecutionConfigPDA, admin.PublicKey(), diff --git a/chains/solana/contracts/tests/ccip/ccip_transactions.go b/chains/solana/contracts/tests/ccip/ccip_transactions.go index 534cbd6d..600bd063 100644 --- a/chains/solana/contracts/tests/ccip/ccip_transactions.go +++ b/chains/solana/contracts/tests/ccip/ccip_transactions.go @@ -36,9 +36,15 @@ func SignCommitReport(ctx [3][32]byte, report ccip_router.CommitInput, baseSigne return sigs, nil } -func getChainStatePDA(chainSelector uint64) (solana.PublicKey, error) { +func GetSourceChainStatePDA(chainSelector uint64) (solana.PublicKey, error) { chainSelectorLE := utils.Uint64ToLE(chainSelector) - p, _, err := solana.FindProgramAddress([][]byte{[]byte("chain_state"), chainSelectorLE}, config.CcipRouterProgram) + p, _, err := solana.FindProgramAddress([][]byte{[]byte("source_chain_state"), chainSelectorLE}, config.CcipRouterProgram) + return p, err +} + +func GetDestChainStatePDA(chainSelector uint64) (solana.PublicKey, error) { + chainSelectorLE := utils.Uint64ToLE(chainSelector) + p, _, err := solana.FindProgramAddress([][]byte{[]byte("dest_chain_state"), chainSelectorLE}, config.CcipRouterProgram) return p, err } diff --git a/chains/solana/contracts/tests/config/ccip_config.go b/chains/solana/contracts/tests/config/ccip_config.go index 315ca7b2..e8c564f4 100644 --- a/chains/solana/contracts/tests/config/ccip_config.go +++ b/chains/solana/contracts/tests/config/ccip_config.go @@ -21,6 +21,7 @@ var ( Token2022Program = solana.MustPublicKeyFromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") RouterConfigPDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("config")}, CcipRouterProgram) + RouterStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("state")}, CcipRouterProgram) ExternalExecutionConfigPDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("external_execution_config")}, CcipRouterProgram) ExternalTokenPoolsSignerPDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("external_token_pools_signer")}, CcipRouterProgram) ReceiverTargetAccountPDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("counter")}, CcipReceiverProgram) @@ -32,23 +33,19 @@ var ( SolanaChainSelector uint64 = 15 EvmChainSelector uint64 = 21 + EvmChainLE = utils.Uint64ToLE(EvmChainSelector) - SolanaChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("chain_state"), binary.LittleEndian.AppendUint64([]byte{}, SolanaChainSelector)}, CcipRouterProgram) - EvmChainLE = utils.Uint64ToLE(EvmChainSelector) - EvmChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("chain_state"), binary.LittleEndian.AppendUint64([]byte{}, EvmChainSelector)}, CcipRouterProgram) + SolanaSourceChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("source_chain_state"), binary.LittleEndian.AppendUint64([]byte{}, SolanaChainSelector)}, CcipRouterProgram) + SolanaDestChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("dest_chain_state"), binary.LittleEndian.AppendUint64([]byte{}, SolanaChainSelector)}, CcipRouterProgram) + EvmSourceChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("source_chain_state"), binary.LittleEndian.AppendUint64([]byte{}, EvmChainSelector)}, CcipRouterProgram) + EvmDestChainStatePDA, _, _ = solana.FindProgramAddress([][]byte{[]byte("dest_chain_state"), binary.LittleEndian.AppendUint64([]byte{}, EvmChainSelector)}, CcipRouterProgram) OnRampAddress = []byte{1, 2, 3} EnableExecutionAfter = int64(1800) // 30min - MaxOracles = 16 - OcrF uint8 = 5 - ConfigDigest = utils.MakeRandom32ByteArray() - Empty24Byte = [24]byte{} - ReportSequence = uint64(8) - ReportContext = [3][32]byte{ - ConfigDigest, - [32]byte(binary.BigEndian.AppendUint64(Empty24Byte[:], ReportSequence)), - utils.MakeRandom32ByteArray(), - } - MaxSignersAndTransmitters = 16 + MaxOracles = 16 + OcrF uint8 = 5 + ConfigDigest = utils.MakeRandom32ByteArray() + Empty24Byte = [24]byte{} + MaxSignersAndTransmitters = 16 ) diff --git a/chains/solana/contracts/tests/config/mcm_config.go b/chains/solana/contracts/tests/config/mcm_config.go index 05268e4d..693975bf 100644 --- a/chains/solana/contracts/tests/config/mcm_config.go +++ b/chains/solana/contracts/tests/config/mcm_config.go @@ -26,7 +26,6 @@ var ( MaxNumSigners = 200 MaxAppendSignerBatchSize = 45 MaxAppendSignatureBatchSize = 13 - // root related configs // the following diagram shows the structure of the signers and groups: // ref: https://github.com/smartcontractkit/ccip-owner-contracts/blob/56f1a8d2cd4ba5ef2b99d2185ffded53957dd410/src/ManyChainMultiSig.sol#L65 diff --git a/chains/solana/contracts/tests/config/timelock_config.go b/chains/solana/contracts/tests/config/timelock_config.go index b304db79..b336c931 100644 --- a/chains/solana/contracts/tests/config/timelock_config.go +++ b/chains/solana/contracts/tests/config/timelock_config.go @@ -22,4 +22,6 @@ var ( TimelockEmptyOpID = [32]byte{} TimelockOpDoneTimestamp = uint64(1) + + MaxFunctionSelectorLen = 32 // tested with 128, but for test time consideration, keeping at 32 ) diff --git a/chains/solana/contracts/tests/mcms/mcm.go b/chains/solana/contracts/tests/mcms/mcm.go index 87729e18..d0b8eeec 100644 --- a/chains/solana/contracts/tests/mcms/mcm.go +++ b/chains/solana/contracts/tests/mcms/mcm.go @@ -242,7 +242,7 @@ func IxToMcmTestOpNode(multisig solana.PublicKey, msigSigner solana.PublicKey, i for _, acc := range ix.Accounts() { accCopy := *acc - // NOTE: this bypasses utils.sendTransaction signing part since it's PDA we don't have private key + // NOTE: this bypasses utils.sendTransaction signing part since it's PDA and it doesn't have private key if accCopy.PublicKey == msigSigner { accCopy.IsSigner = false } diff --git a/chains/solana/contracts/tests/mcms/mcm_errors.go b/chains/solana/contracts/tests/mcms/mcm_errors.go index d0211033..f3323553 100644 --- a/chains/solana/contracts/tests/mcms/mcm_errors.go +++ b/chains/solana/contracts/tests/mcms/mcm_errors.go @@ -8,91 +8,13 @@ import ( type McmError agbinary.BorshEnum const ( - WrongMultiSigMcmError McmError = iota - WrongChainIDMcmError - UnauthorizedMcmError - InvalidInputsMcmError - OverflowMcmError - InvalidSignatureMcmError - FailedEcdsaRecoverMcmError - InvalidRootLenMcmError - MismatchedInputSignerVectorsLengthMcmError - OutOfBoundsNumOfSignersMcmError - MismatchedInputGroupArraysLengthMcmError - GroupTreeNotWellFormedMcmError - SignerInDisabledGroupMcmError - OutOfBoundsGroupQuorumMcmError - SignersAddressesMustBeStrictlyIncreasingMcmError - SignedHashAlreadySeenMcmError - InvalidSignerMcmError - MissingConfigMcmError - InsufficientSignersMcmError - ValidUntilHasAlreadyPassedMcmError - ProofCannotBeVerifiedMcmError - PendingOpsMcmError - WrongPreOpCountMcmError - WrongPostOpCountMcmError - PostOpCountReachedMcmError - RootExpiredMcmError - WrongNonceMcmError + UnauthorizedMcmError McmError = iota ) func (value McmError) String() string { switch value { - case WrongMultiSigMcmError: - return "WrongMultiSig" - case WrongChainIDMcmError: - return "WrongChainID" case UnauthorizedMcmError: return "Unauthorized" - case InvalidInputsMcmError: - return "InvalidInputs" - case OverflowMcmError: - return "Overflow" - case InvalidSignatureMcmError: - return "InvalidSignature" - case FailedEcdsaRecoverMcmError: - return "FailedEcdsaRecover" - case InvalidRootLenMcmError: - return "InvalidRootLen" - case MismatchedInputSignerVectorsLengthMcmError: - return "MismatchedInputSignerVectorsLength" - case OutOfBoundsNumOfSignersMcmError: - return "OutOfBoundsNumOfSigners" - case MismatchedInputGroupArraysLengthMcmError: - return "MismatchedInputGroupArraysLength" - case GroupTreeNotWellFormedMcmError: - return "GroupTreeNotWellFormed" - case SignerInDisabledGroupMcmError: - return "SignerInDisabledGroup" - case OutOfBoundsGroupQuorumMcmError: - return "OutOfBoundsGroupQuorum" - case SignersAddressesMustBeStrictlyIncreasingMcmError: - return "SignersAddressesMustBeStrictlyIncreasing" - case SignedHashAlreadySeenMcmError: - return "SignedHashAlreadySeen" - case InvalidSignerMcmError: - return "InvalidSigner" - case MissingConfigMcmError: - return "MissingConfig" - case InsufficientSignersMcmError: - return "InsufficientSigners" - case ValidUntilHasAlreadyPassedMcmError: - return "ValidUntilHasAlreadyPassed" - case ProofCannotBeVerifiedMcmError: - return "ProofCannotBeVerified" - case PendingOpsMcmError: - return "PendingOps" - case WrongPreOpCountMcmError: - return "WrongPreOpCount" - case WrongPostOpCountMcmError: - return "WrongPostOpCount" - case PostOpCountReachedMcmError: - return "PostOpCountReached" - case RootExpiredMcmError: - return "RootExpired" - case WrongNonceMcmError: - return "WrongNonce" default: return "" } diff --git a/chains/solana/contracts/tests/mcms/mcm_events.go b/chains/solana/contracts/tests/mcms/mcm_events.go new file mode 100644 index 00000000..bedb839d --- /dev/null +++ b/chains/solana/contracts/tests/mcms/mcm_events.go @@ -0,0 +1,39 @@ +package contracts + +import ( + "github.com/gagliardetto/solana-go" +) + +// Events - temporary event struct to decode +// anchor-go does not support events +// https://github.com/fragmetric-labs/solana-anchor-go does but requires upgrade to anchor >= v0.30.0 + +// NewRoot represents an event emitted when a new root is set +type NewRoot struct { + Root [32]byte // root + ValidUntil uint32 // valid_until + + // Metadata fields + MetadataChainID uint64 // metadata_chain_id + MetadataMultisig solana.PublicKey // metadata_multisig + MetadataPreOpCount uint64 // metadata_pre_op_count + MetadataPostOpCount uint64 // metadata_post_op_count + MetadataOverridePreviousRoot bool // metadata_override_previous_root +} + +const numGroups = 32 + +// ConfigSet represents an event emitted when a new config is set +type ConfigSet struct { + // Note: Rust comment indicates signers are omitted due to memory overflow + GroupParents [numGroups]byte // group_parents + GroupQuorums [numGroups]byte // group_quorums + IsRootCleared bool // is_root_cleared +} + +// OpExecuted represents an event emitted when an op is successfully executed +type OpExecuted struct { + Nonce uint64 // nonce + To solana.PublicKey // to + Data []byte // data: Vec +} diff --git a/chains/solana/contracts/tests/mcms/mcm_multiple_multisigs_test.go b/chains/solana/contracts/tests/mcms/mcm_multiple_multisigs_test.go index b0555304..8e609417 100644 --- a/chains/solana/contracts/tests/mcms/mcm_multiple_multisigs_test.go +++ b/chains/solana/contracts/tests/mcms/mcm_multiple_multisigs_test.go @@ -30,20 +30,20 @@ func TestMcmMultipleMultisigs(t *testing.T) { solanaGoClient := utils.DeployAllPrograms(t, utils.PathToAnchorConfig, admin) // mcm multisig 1 - TestMsigName1, err := mcmsUtils.PadString32("test_mcm_instance_1") + testMsigName1, err := mcmsUtils.PadString32("test_mcm_instance_1") require.NoError(t, err) - MultisigConfigPDA1 := McmConfigAddress(TestMsigName1) - RootMetadataPDA1 := RootMetadataAddress(TestMsigName1) - ExpiringRootAndOpCountPDA1 := ExpiringRootAndOpCountAddress(TestMsigName1) - ConfigSignersPDA1 := McmConfigSignersAddress(TestMsigName1) + multisigConfigPDA1 := McmConfigAddress(testMsigName1) + rootMetadataPDA1 := RootMetadataAddress(testMsigName1) + expiringRootAndOpCountPDA1 := ExpiringRootAndOpCountAddress(testMsigName1) + configSignersPDA1 := McmConfigSignersAddress(testMsigName1) // mcm multisig 2 - TestMsigName2, err := mcmsUtils.PadString32("test_mcm_instance_2") + testMsigName2, err := mcmsUtils.PadString32("test_mcm_instance_2") require.NoError(t, err) - MultisigConfigPDA2 := McmConfigAddress(TestMsigName2) - RootMetadataPDA2 := RootMetadataAddress(TestMsigName2) - ExpiringRootAndOpCountPDA2 := ExpiringRootAndOpCountAddress(TestMsigName2) - ConfigSignersPDA2 := McmConfigSignersAddress(TestMsigName2) + multisigConfigPDA2 := McmConfigAddress(testMsigName2) + rootMetadataPDA2 := RootMetadataAddress(testMsigName2) + expiringRootAndOpCountPDA2 := ExpiringRootAndOpCountAddress(testMsigName2) + configSignersPDA2 := McmConfigSignersAddress(testMsigName2) t.Run("setup:funding", func(t *testing.T) { utils.FundAccounts(ctx, []solana.PrivateKey{admin}, solanaGoClient, t) @@ -57,7 +57,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { }) require.NoError(t, accErr) - // decode program data + // decode program data∑´ var programData struct { DataType uint32 Address solana.PublicKey @@ -66,21 +66,21 @@ func TestMcmMultipleMultisigs(t *testing.T) { ix, err := mcm.NewInitializeInstruction( config.TestChainID, - TestMsigName1, - MultisigConfigPDA1, + testMsigName1, + multisigConfigPDA1, admin.PublicKey(), solana.SystemProgramID, config.McmProgram, programData.Address, - RootMetadataPDA1, - ExpiringRootAndOpCountPDA1, + rootMetadataPDA1, + expiringRootAndOpCountPDA1, ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA1, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA1, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -102,7 +102,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { groupParents := []uint8{0, 0, 0, 2, 0, 0, 0, 0, 0, 0} mcmConfig, err := mcmsUtils.NewValidMcmConfig( - TestMsigName1, + testMsigName1, signerPrivateKeys, signerGroups, groupQuorums, @@ -120,10 +120,10 @@ func TestMcmMultipleMultisigs(t *testing.T) { require.NoError(t, err) initSignersIx, err := mcm.NewInitSignersInstruction( - TestMsigName1, + testMsigName1, parsedTotalSigners, - MultisigConfigPDA1, - ConfigSignersPDA1, + multisigConfigPDA1, + configSignersPDA1, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -131,14 +131,14 @@ func TestMcmMultipleMultisigs(t *testing.T) { require.NoError(t, err) ixs = append(ixs, initSignersIx) - appendSignersIxs, err := AppendSignersIxs(signerAddresses, TestMsigName1, MultisigConfigPDA1, ConfigSignersPDA1, admin.PublicKey(), config.MaxAppendSignerBatchSize) + appendSignersIxs, err := AppendSignersIxs(signerAddresses, testMsigName1, multisigConfigPDA1, configSignersPDA1, admin.PublicKey(), config.MaxAppendSignerBatchSize) require.NoError(t, err) ixs = append(ixs, appendSignersIxs...) finalizeSignersIx, err := mcm.NewFinalizeSignersInstruction( - TestMsigName1, - MultisigConfigPDA1, - ConfigSignersPDA1, + testMsigName1, + multisigConfigPDA1, + configSignersPDA1, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -149,7 +149,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { } var cfgSignersAccount mcm.ConfigSigners - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, ConfigSignersPDA1, config.DefaultCommitment, &cfgSignersAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA1, config.DefaultCommitment, &cfgSignersAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, true, cfgSignersAccount.IsFinalized) @@ -162,13 +162,13 @@ func TestMcmMultipleMultisigs(t *testing.T) { t.Run("success:set_config", func(t *testing.T) { ix, err := mcm.NewSetConfigInstruction( - TestMsigName1, + testMsigName1, mcmConfig.SignerGroups, mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA1, - ConfigSignersPDA1, + multisigConfigPDA1, + configSignersPDA1, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -180,7 +180,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA1, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA1, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -195,7 +195,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { } // pda closed after set_config - utils.AssertClosedAccount(ctx, t, solanaGoClient, ConfigSignersPDA1, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA1, config.DefaultCommitment) }) }) }) @@ -215,21 +215,21 @@ func TestMcmMultipleMultisigs(t *testing.T) { ix, err := mcm.NewInitializeInstruction( config.TestChainID, - TestMsigName2, - MultisigConfigPDA2, + testMsigName2, + multisigConfigPDA2, admin.PublicKey(), solana.SystemProgramID, config.McmProgram, programData.Address, - RootMetadataPDA2, - ExpiringRootAndOpCountPDA2, + rootMetadataPDA2, + expiringRootAndOpCountPDA2, ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA2, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA2, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -250,7 +250,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { groupParents := []uint8{0, 0, 0, 2, 0, 0, 0, 0, 0, 0} mcmConfig, err := mcmsUtils.NewValidMcmConfig( - TestMsigName2, + testMsigName2, signerPrivateKeys, signerGroups, groupQuorums, @@ -268,10 +268,10 @@ func TestMcmMultipleMultisigs(t *testing.T) { require.NoError(t, err) initSignersIx, err := mcm.NewInitSignersInstruction( - TestMsigName2, + testMsigName2, parsedTotalSigners, - MultisigConfigPDA2, - ConfigSignersPDA2, + multisigConfigPDA2, + configSignersPDA2, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -279,14 +279,14 @@ func TestMcmMultipleMultisigs(t *testing.T) { require.NoError(t, err) ixs = append(ixs, initSignersIx) - appendSignersIxs, err := AppendSignersIxs(signerAddresses, TestMsigName2, MultisigConfigPDA2, ConfigSignersPDA2, admin.PublicKey(), config.MaxAppendSignerBatchSize) + appendSignersIxs, err := AppendSignersIxs(signerAddresses, testMsigName2, multisigConfigPDA2, configSignersPDA2, admin.PublicKey(), config.MaxAppendSignerBatchSize) require.NoError(t, err) ixs = append(ixs, appendSignersIxs...) finalizeSignersIx, err := mcm.NewFinalizeSignersInstruction( - TestMsigName2, - MultisigConfigPDA2, - ConfigSignersPDA2, + testMsigName2, + multisigConfigPDA2, + configSignersPDA2, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -297,7 +297,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { } var cfgSignersAccount mcm.ConfigSigners - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, ConfigSignersPDA2, config.DefaultCommitment, &cfgSignersAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA2, config.DefaultCommitment, &cfgSignersAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, true, cfgSignersAccount.IsFinalized) @@ -310,13 +310,13 @@ func TestMcmMultipleMultisigs(t *testing.T) { t.Run("fail:set_config with invalid seeds", func(t *testing.T) { ix, err := mcm.NewSetConfigInstruction( - TestMsigName1, + testMsigName1, mcmConfig.SignerGroups, mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA2, - ConfigSignersPDA2, + multisigConfigPDA2, + configSignersPDA2, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -329,13 +329,13 @@ func TestMcmMultipleMultisigs(t *testing.T) { t.Run("success:set_config", func(t *testing.T) { ix, err := mcm.NewSetConfigInstruction( - TestMsigName2, + testMsigName2, mcmConfig.SignerGroups, mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA2, - ConfigSignersPDA2, + multisigConfigPDA2, + configSignersPDA2, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -347,7 +347,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA2, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA2, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -362,7 +362,7 @@ func TestMcmMultipleMultisigs(t *testing.T) { } // pda closed after set_config - utils.AssertClosedAccount(ctx, t, solanaGoClient, ConfigSignersPDA2, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA2, config.DefaultCommitment) }) }) }) diff --git a/chains/solana/contracts/tests/mcms/mcm_set_config_test.go b/chains/solana/contracts/tests/mcms/mcm_set_config_test.go index d3706e57..2237f94c 100644 --- a/chains/solana/contracts/tests/mcms/mcm_set_config_test.go +++ b/chains/solana/contracts/tests/mcms/mcm_set_config_test.go @@ -1,7 +1,9 @@ package contracts import ( + "fmt" "reflect" + "slices" "testing" bin "github.com/gagliardetto/binary" @@ -36,13 +38,13 @@ func TestMcmSetConfig(t *testing.T) { solanaGoClient := utils.DeployAllPrograms(t, utils.PathToAnchorConfig, admin) // mcm name - TestMsigName := config.TestMsigNamePaddedBuffer + testMsigName := config.TestMsigNamePaddedBuffer // test mcm pdas - MultisigConfigPDA := McmConfigAddress(TestMsigName) - RootMetadataPDA := RootMetadataAddress(TestMsigName) - ExpiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(TestMsigName) - ConfigSignersPDA := McmConfigSignersAddress(TestMsigName) + multisigConfigPDA := McmConfigAddress(testMsigName) + rootMetadataPDA := RootMetadataAddress(testMsigName) + expiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(testMsigName) + configSignersPDA := McmConfigSignersAddress(testMsigName) t.Run("setup:funding", func(t *testing.T) { utils.FundAccounts(ctx, []solana.PrivateKey{admin, anotherAdmin, user}, solanaGoClient, t) @@ -64,21 +66,21 @@ func TestMcmSetConfig(t *testing.T) { ix, err := mcm.NewInitializeInstruction( config.TestChainID, - TestMsigName, - MultisigConfigPDA, + testMsigName, + multisigConfigPDA, admin.PublicKey(), solana.SystemProgramID, config.McmProgram, programData.Address, - RootMetadataPDA, - ExpiringRootAndOpCountPDA, + rootMetadataPDA, + expiringRootAndOpCountPDA, ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -88,9 +90,9 @@ func TestMcmSetConfig(t *testing.T) { t.Run("mcm:ownership", func(t *testing.T) { // Fail to transfer ownership when not owner instruction, err := mcm.NewTransferOwnershipInstruction( - TestMsigName, + testMsigName, anotherAdmin.PublicKey(), - MultisigConfigPDA, + multisigConfigPDA, user.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -99,9 +101,9 @@ func TestMcmSetConfig(t *testing.T) { // successfully transfer ownership instruction, err = mcm.NewTransferOwnershipInstruction( - TestMsigName, + testMsigName, anotherAdmin.PublicKey(), - MultisigConfigPDA, + multisigConfigPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -110,8 +112,8 @@ func TestMcmSetConfig(t *testing.T) { // Fail to accept ownership when not proposed_owner instruction, err = mcm.NewAcceptOwnershipInstruction( - TestMsigName, - MultisigConfigPDA, + testMsigName, + multisigConfigPDA, user.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -121,8 +123,8 @@ func TestMcmSetConfig(t *testing.T) { // Successfully accept ownership // anotherAdmin becomes owner for remaining tests instruction, err = mcm.NewAcceptOwnershipInstruction( - TestMsigName, - MultisigConfigPDA, + testMsigName, + multisigConfigPDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -131,18 +133,18 @@ func TestMcmSetConfig(t *testing.T) { // Current owner cannot propose self instruction, err = mcm.NewTransferOwnershipInstruction( - TestMsigName, + testMsigName, anotherAdmin.PublicKey(), - MultisigConfigPDA, + multisigConfigPDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) - result = utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment, []string{"Error Code: " + InvalidInputsMcmError.String()}) + result = utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment, []string{"Error Code: " + mcm.InvalidInputs_McmError.String()}) require.NotNil(t, result) // Validate proposed set to 0-address after accepting ownership var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) if err != nil { require.NoError(t, err, "failed to get account info") } @@ -151,9 +153,9 @@ func TestMcmSetConfig(t *testing.T) { // get it back instruction, err = mcm.NewTransferOwnershipInstruction( - TestMsigName, + testMsigName, admin.PublicKey(), - MultisigConfigPDA, + multisigConfigPDA, anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -161,15 +163,15 @@ func TestMcmSetConfig(t *testing.T) { require.NotNil(t, result) instruction, err = mcm.NewAcceptOwnershipInstruction( - TestMsigName, - MultisigConfigPDA, + testMsigName, + multisigConfigPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) require.NotNil(t, result) - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) if err != nil { require.NoError(t, err, "failed to get account info") } @@ -193,7 +195,7 @@ func TestMcmSetConfig(t *testing.T) { groupParents := []uint8{0, 0, 0, 2, 0, 0, 0, 0, 0, 0} mcmConfig, err := mcmsUtils.NewValidMcmConfig( - TestMsigName, + testMsigName, signerPrivateKeys, signerGroups, groupQuorums, @@ -210,10 +212,10 @@ func TestMcmSetConfig(t *testing.T) { require.NoError(t, err) initSignersIx, err := mcm.NewInitSignersInstruction( - TestMsigName, + testMsigName, parsedTotalSigners, - MultisigConfigPDA, - ConfigSignersPDA, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -221,14 +223,14 @@ func TestMcmSetConfig(t *testing.T) { require.NoError(t, err) ixs = append(ixs, initSignersIx) - appendSignersIxs, err := AppendSignersIxs(signerAddresses, TestMsigName, MultisigConfigPDA, ConfigSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) + appendSignersIxs, err := AppendSignersIxs(signerAddresses, testMsigName, multisigConfigPDA, configSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) require.NoError(t, err) ixs = append(ixs, appendSignersIxs...) finalizeSignersIx, err := mcm.NewFinalizeSignersInstruction( - TestMsigName, - MultisigConfigPDA, - ConfigSignersPDA, + testMsigName, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -239,7 +241,7 @@ func TestMcmSetConfig(t *testing.T) { } var cfgSignersAccount mcm.ConfigSigners - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment, &cfgSignersAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA, config.DefaultCommitment, &cfgSignersAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, true, cfgSignersAccount.IsFinalized) @@ -258,8 +260,8 @@ func TestMcmSetConfig(t *testing.T) { mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA, - ConfigSignersPDA, + multisigConfigPDA, + configSignersPDA, user.PublicKey(), // unauthorized user solana.SystemProgramID, ).ValidateAndBuild() @@ -277,20 +279,31 @@ func TestMcmSetConfig(t *testing.T) { mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA, - ConfigSignersPDA, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[ConfigSet]("ConfigSet"), + }, + ) + + event := parsedLogs[0].EventData[0].Data.(*ConfigSet) + require.Equal(t, mcmConfig.GroupParents, event.GroupParents) + require.Equal(t, mcmConfig.GroupQuorums, event.GroupQuorums) + require.Equal(t, mcmConfig.ClearRoot, event.IsRootCleared) // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -305,7 +318,7 @@ func TestMcmSetConfig(t *testing.T) { } // pda closed after set_config - utils.AssertClosedAccount(ctx, t, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA, config.DefaultCommitment) }) }) }) @@ -325,7 +338,7 @@ func TestMcmSetConfig(t *testing.T) { groupParents := []uint8{0, 0, 0, 2, 0, 0, 0, 0, 0, 0} mcmConfig, err := mcmsUtils.NewValidMcmConfig( - TestMsigName, + testMsigName, signerPrivateKeys, signerGroups, groupQuorums, @@ -338,16 +351,16 @@ func TestMcmSetConfig(t *testing.T) { t.Run("mcm:set_config: preload signers on PDA", func(t *testing.T) { // ConfigSignersPDA should be closed before reinitializing - utils.AssertClosedAccount(ctx, t, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA, config.DefaultCommitment) parsedTotalSigners, err := mcmsUtils.SafeToUint8(len(signerAddresses)) require.NoError(t, err) initSignersIx, err := mcm.NewInitSignersInstruction( - TestMsigName, + testMsigName, parsedTotalSigners, - MultisigConfigPDA, - ConfigSignersPDA, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() @@ -355,7 +368,7 @@ func TestMcmSetConfig(t *testing.T) { require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initSignersIx}, admin, config.DefaultCommitment) - appendSignersIxs, err := AppendSignersIxs(signerAddresses, TestMsigName, MultisigConfigPDA, ConfigSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) + appendSignersIxs, err := AppendSignersIxs(signerAddresses, testMsigName, multisigConfigPDA, configSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) require.NoError(t, err) // partially register signers @@ -363,17 +376,29 @@ func TestMcmSetConfig(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - // clear signers + // clear signers(this closes the account) clearIx, err := mcm.NewClearSignersInstruction( - TestMsigName, - MultisigConfigPDA, - ConfigSignersPDA, + testMsigName, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{clearIx}, admin, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA, config.DefaultCommitment) + + reInitSignersIx, err := mcm.NewInitSignersInstruction( + testMsigName, + parsedTotalSigners, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, err) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{reInitSignersIx}, admin, config.DefaultCommitment) // register all signers for _, ix := range appendSignersIxs { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) @@ -381,9 +406,9 @@ func TestMcmSetConfig(t *testing.T) { // finalize registration finalizeSignersIx, err := mcm.NewFinalizeSignersInstruction( - TestMsigName, - MultisigConfigPDA, - ConfigSignersPDA, + testMsigName, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), ).ValidateAndBuild() @@ -391,7 +416,7 @@ func TestMcmSetConfig(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finalizeSignersIx}, admin, config.DefaultCommitment) var cfgSignersAccount mcm.ConfigSigners - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment, &cfgSignersAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA, config.DefaultCommitment, &cfgSignersAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, true, cfgSignersAccount.IsFinalized) @@ -404,25 +429,36 @@ func TestMcmSetConfig(t *testing.T) { t.Run("success:set_config", func(t *testing.T) { ix, err := mcm.NewSetConfigInstruction( - TestMsigName, + testMsigName, mcmConfig.SignerGroups, mcmConfig.GroupQuorums, mcmConfig.GroupParents, mcmConfig.ClearRoot, - MultisigConfigPDA, - ConfigSignersPDA, + multisigConfigPDA, + configSignersPDA, admin.PublicKey(), solana.SystemProgramID, ).ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[ConfigSet]("ConfigSet"), + }, + ) + + event := parsedLogs[0].EventData[0].Data.(*ConfigSet) + require.Equal(t, mcmConfig.GroupParents, event.GroupParents) + require.Equal(t, mcmConfig.GroupQuorums, event.GroupQuorums) + require.Equal(t, mcmConfig.ClearRoot, event.IsRootCleared) // get config and validate var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) require.NoError(t, err, "failed to get account info") require.Equal(t, config.TestChainID, configAccount.ChainId) @@ -437,8 +473,402 @@ func TestMcmSetConfig(t *testing.T) { } // pda closed after set_config - utils.AssertClosedAccount(ctx, t, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, configSignersPDA, config.DefaultCommitment) }) }) - // todo: negative tests + + t.Run("set_config validation", func(t *testing.T) { + tests := []struct { + name string + errorMsg string + modifyConfig func(*mcmsUtils.McmConfigArgs) + skipPreloadSigners bool + skipFinalizeSigners bool + }{ + { + name: "should not be able to call set_config without preloading config_signers", + errorMsg: "Error Code: " + "AccountNotInitialized.", + modifyConfig: func(c *mcmsUtils.McmConfigArgs) {}, + skipPreloadSigners: true, + }, + { + name: "should not be able to call set_config without finalized config_signers", + errorMsg: "Error Code: " + mcm.SignersNotFinalized_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) {}, + skipFinalizeSigners: true, + }, + { + name: "length of signer addresses and signer groups length should be equal", + errorMsg: "Error Code: " + mcm.MismatchedInputSignerVectorsLength_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + c.SignerGroups = append(c.SignerGroups, 1) + }, + }, + { + name: "every group index in signer group should be less than NUM_GROUPS", + errorMsg: "Error Code: " + mcm.MismatchedInputGroupArraysLength_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + (c.SignerGroups)[0] = 32 + }, + }, + { + name: "the parent of root has to be 0", + errorMsg: "Error Code: " + mcm.GroupTreeNotWellFormed_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + (c.GroupParents)[0] = 1 + }, + }, + { + name: "the parent group should be at a higher index than the child group", + errorMsg: "Error Code: " + mcm.GroupTreeNotWellFormed_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + (c.GroupParents)[1] = 2 + }, + }, + { + name: "disabled group(with 0 quorum) should not have a signer", + errorMsg: "Error Code: " + mcm.SignerInDisabledGroup_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + (c.GroupQuorums)[3] = 0 // set quorum of group 3 to 0, but we still have signers in group 3 + }, + }, + { + name: "the group quorum should be able to be met(i.e. have more signers than the quorum)", + errorMsg: "Error Code: " + mcm.OutOfBoundsGroupQuorum_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + (c.GroupQuorums)[3] = 3 // set quorum of group 3 to 3, but we have two signers in group 3 + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("set_config validation:%s", tt.name), func(t *testing.T) { + t.Parallel() + + // use different msig accounts per test + failTestMsigName, err := mcmsUtils.PadString32(fmt.Sprintf("fail_test_%d", i)) + require.NoError(t, err) + + // test scope mcm pdas + failMultisigConfigPDA := McmConfigAddress(failTestMsigName) + failRootMetadataPDA := RootMetadataAddress(failTestMsigName) + failExpiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(failTestMsigName) + failConfigSignersPDA := McmConfigSignersAddress(failTestMsigName) + + t.Run(fmt.Sprintf("msig initialization:%s", tt.name), func(t *testing.T) { + // get program data account + data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ + Commitment: config.DefaultCommitment, + }) + require.NoError(t, accErr) + + // decode program data + var programData struct { + DataType uint32 + Address solana.PublicKey + } + require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) + + // initialize msig + ix, initIxErr := mcm.NewInitializeInstruction( + config.TestChainID, + failTestMsigName, + failMultisigConfigPDA, + admin.PublicKey(), + solana.SystemProgramID, + config.McmProgram, + programData.Address, + failRootMetadataPDA, + failExpiringRootAndOpCountPDA, + ).ValidateAndBuild() + require.NoError(t, initIxErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + }) + + cfg, err := mcmsUtils.NewValidMcmConfig( + failTestMsigName, + config.SignerPrivateKeys, + config.SignerGroups, + config.GroupQuorums, + config.GroupParents, + config.ClearRoot, + ) + require.NoError(t, err) + + t.Run("preload signers for validation tests", func(t *testing.T) { + if tt.skipPreloadSigners { + return + } + parsedTotalSigners, parsingErr := mcmsUtils.SafeToUint8(len(cfg.SignerAddresses)) + require.NoError(t, parsingErr) + + initSignersIx, initSignersErr := mcm.NewInitSignersInstruction( + failTestMsigName, + parsedTotalSigners, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, initSignersErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initSignersIx}, admin, config.DefaultCommitment) + + appendSignersIxs, appendSignersIxsErr := AppendSignersIxs(cfg.SignerAddresses, failTestMsigName, failMultisigConfigPDA, failConfigSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) + require.NoError(t, appendSignersIxsErr) + for _, ix := range appendSignersIxs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } + + if !tt.skipFinalizeSigners { + finalizeSignersIx, finSignersIxErr := mcm.NewFinalizeSignersInstruction( + failTestMsigName, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, finSignersIxErr) + + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finalizeSignersIx}, admin, config.DefaultCommitment) + + var cfgSignersAccount mcm.ConfigSigners + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, failConfigSignersPDA, config.DefaultCommitment, &cfgSignersAccount) + require.NoError(t, err, "failed to get account info") + + require.Equal(t, true, cfgSignersAccount.IsFinalized) + + // check if the addresses are registered correctly + for i, signer := range cfgSignersAccount.SignerAddresses { + require.Equal(t, cfg.SignerAddresses[i], signer) + } + } + }) + + // corrupt the config + tt.modifyConfig(cfg) + + ix, err := mcm.NewSetConfigInstruction( + cfg.MultisigName, + cfg.SignerGroups, + cfg.GroupQuorums, + cfg.GroupParents, + cfg.ClearRoot, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, err) + + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, rpc.CommitmentConfirmed, []string{tt.errorMsg}) + require.NotNil(t, result) + }) + } + }) + + t.Run("pre-uploading config_signers validations", func(t *testing.T) { + type TestStage int + + const ( + InitStage TestStage = iota + AppendStage + FinalizeStage + ) + + type TxWithStage struct { + Instructions []solana.Instruction + Stage TestStage + } + + tests := []struct { + name string + errorMsg string + modifyConfig func(*mcmsUtils.McmConfigArgs) + failureStage TestStage + skipInitSigners bool + totalSignersOffset int + }{ + { + name: "should not be able to initialize config_signers with empty", + errorMsg: "Error Code: " + mcm.OutOfBoundsNumOfSigners_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + // empty cfg.SignerAddresses + c.SignerAddresses = make([][20]byte, 0) + }, + failureStage: InitStage, + }, + { + name: "should not be able to initialize config_signers with more than MAX_NUM_SIGNERS", + errorMsg: "Error Code: " + mcm.OutOfBoundsNumOfSigners_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + // replace cfg.SignerAddresses with more than MAX_NUM_SIGNERS(200) + privateKeys, err := eth.GenerateEthPrivateKeys(201) + require.NoError(t, err) + signers, err := eth.GetEvmSigners(privateKeys) + require.NoError(t, err) + signerAddresses := make([][20]byte, 0) + for _, signer := range signers { + signerAddresses = append(signerAddresses, signer.Address) + } + c.SignerAddresses = signerAddresses + }, + failureStage: InitStage, + }, + { + name: "should not be able to append signers without initializing", + errorMsg: "Error Code: " + "AccountNotInitialized.", + modifyConfig: func(c *mcmsUtils.McmConfigArgs) {}, + failureStage: AppendStage, + skipInitSigners: true, + }, + { + name: "should not be able to append unsorted signer", + errorMsg: "Error Code: " + mcm.SignersAddressesMustBeStrictlyIncreasing_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) { + slices.Reverse(c.SignerAddresses) + }, + failureStage: AppendStage, + }, + { + name: "should not be able to append more signers than specified in total_signers", + errorMsg: "Error Code: " + mcm.OutOfBoundsNumOfSigners_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) {}, + failureStage: AppendStage, + totalSignersOffset: -2, + }, + { + name: "should not be able to finalize unmatched total signers", + errorMsg: "Error Code: " + mcm.OutOfBoundsNumOfSigners_McmError.String(), + modifyConfig: func(c *mcmsUtils.McmConfigArgs) {}, + failureStage: FinalizeStage, + totalSignersOffset: 2, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("set_config validation:%s", tt.name), func(t *testing.T) { + t.Parallel() + + // use different msig accounts per test + failTestMsigName, err := mcmsUtils.PadString32(fmt.Sprintf("fail_preupload_signer_test_%d", i)) + require.NoError(t, err) + + // test scope mcm pdas + failMultisigConfigPDA := McmConfigAddress(failTestMsigName) + failRootMetadataPDA := RootMetadataAddress(failTestMsigName) + failExpiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(failTestMsigName) + failConfigSignersPDA := McmConfigSignersAddress(failTestMsigName) + + t.Run(fmt.Sprintf("msig initialization:%s", tt.name), func(t *testing.T) { + // get program data account + data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ + Commitment: config.DefaultCommitment, + }) + require.NoError(t, accErr) + + // decode program data + var programData struct { + DataType uint32 + Address solana.PublicKey + } + require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) + + // initialize msig + ix, initIxErr := mcm.NewInitializeInstruction( + config.TestChainID, + failTestMsigName, + failMultisigConfigPDA, + admin.PublicKey(), + solana.SystemProgramID, + config.McmProgram, + programData.Address, + failRootMetadataPDA, + failExpiringRootAndOpCountPDA, + ).ValidateAndBuild() + require.NoError(t, initIxErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + }) + + cfg, err := mcmsUtils.NewValidMcmConfig( + failTestMsigName, + config.SignerPrivateKeys, + config.SignerGroups, + config.GroupQuorums, + config.GroupParents, + config.ClearRoot, + ) + require.NoError(t, err) + + // corrupt the config if needed + tt.modifyConfig(cfg) + + var txs []TxWithStage + + if !tt.skipInitSigners { + actualLength := len(cfg.SignerAddresses) + totalSigners, _ := mcmsUtils.SafeToUint8(actualLength + tt.totalSignersOffset) // offset for the test + + initSignersIx, _ := mcm.NewInitSignersInstruction( + failTestMsigName, + totalSigners, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + txs = append(txs, TxWithStage{ + Instructions: []solana.Instruction{initSignersIx}, + Stage: InitStage, + }) + } + + appendIxs, _ := AppendSignersIxs( + cfg.SignerAddresses, + failTestMsigName, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + config.MaxAppendSignerBatchSize, + ) + for _, ix := range appendIxs { + txs = append(txs, TxWithStage{ + Instructions: []solana.Instruction{ix}, + Stage: AppendStage, + }) + } + + finalizeIx, _ := mcm.NewFinalizeSignersInstruction( + failTestMsigName, + failMultisigConfigPDA, + failConfigSignersPDA, + admin.PublicKey(), + ).ValidateAndBuild() + txs = append(txs, TxWithStage{ + Instructions: []solana.Instruction{finalizeIx}, + Stage: FinalizeStage, + }) + + for _, tx := range txs { + if tx.Stage == tt.failureStage { + // this stage should fail + result := utils.SendAndFailWith(ctx, t, solanaGoClient, + tx.Instructions, + admin, + rpc.CommitmentConfirmed, + []string{tt.errorMsg}, + ) + require.NotNil(t, result) + break + } + + // all other instructions should succeed + utils.SendAndConfirm(ctx, t, solanaGoClient, + tx.Instructions, + admin, + config.DefaultCommitment, + ) + } + }) + } + }) } diff --git a/chains/solana/contracts/tests/mcms/mcm_set_root_execute_test.go b/chains/solana/contracts/tests/mcms/mcm_set_root_execute_test.go index aa083962..2a187352 100644 --- a/chains/solana/contracts/tests/mcms/mcm_set_root_execute_test.go +++ b/chains/solana/contracts/tests/mcms/mcm_set_root_execute_test.go @@ -1,9 +1,11 @@ package contracts import ( + "bytes" "crypto/sha256" "fmt" "reflect" + "slices" "strings" "testing" @@ -47,496 +49,963 @@ func TestMcmSetRootAndExecute(t *testing.T) { solanaGoClient := utils.DeployAllPrograms(t, utils.PathToAnchorConfig, admin) - // mcm name - TestMsigName := config.TestMsigNamePaddedBuffer - - // test mcm pdas - MultisigConfigPDA := McmConfigAddress(TestMsigName) - RootMetadataPDA := RootMetadataAddress(TestMsigName) - ExpiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(TestMsigName) - ConfigSignersPDA := McmConfigSignersAddress(TestMsigName) - MsigSignerPDA := McmSignerAddress(TestMsigName) - - // helper to inject anchor discriminator into instruction data - // NOTE: if ix is built with anchor-go we don't need it - getAnchorInstructionData := func(method string, data []byte) []byte { - discriminator := sha256.Sum256([]byte("global:" + method)) - return append(discriminator[:8], data...) - } - - // NOTE: this list of operations is methods for testing program, - // contracts/programs/external_program_cpi_stub - // the other way to construct mcmTestOp is using IxToMcmTestOpNode - var stupProgramTestMcmOps = []TestMcmOperation{ - { - ExpectedMethod: "Initialize", - Data: getAnchorInstructionData("initialize", nil), - ExpectedLogSubstr: "Called `initialize`", - RemainingAccounts: []*solana.AccountMeta{ - { - PublicKey: config.StubAccountPDA, - IsSigner: false, - IsWritable: true, - }, - { - PublicKey: McmSignerAddress(config.TestMsigNamePaddedBuffer), - IsSigner: false, - IsWritable: true, - }, - { - PublicKey: solana.SystemProgramID, - IsSigner: false, - IsWritable: false, - }, - }, - }, - { - ExpectedMethod: "Empty", - Data: getAnchorInstructionData("empty", nil), - ExpectedLogSubstr: "Called `empty`", - }, - { - ExpectedMethod: "U8InstructionData", - Data: getAnchorInstructionData("u8_instruction_data", []byte{123}), - ExpectedLogSubstr: "Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", - }, - { - ExpectedMethod: "StructInstructionData", - Data: getAnchorInstructionData("struct_instruction_data", []byte{234}), - ExpectedLogSubstr: "Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", - }, - { - ExpectedMethod: "AccountRead", - Data: getAnchorInstructionData("account_read", nil), - RemainingAccounts: []*solana.AccountMeta{ - { - PublicKey: config.StubAccountPDA, - IsSigner: false, - IsWritable: false, - }, - }, - ExpectedLogSubstr: "Called `account_read`", - CheckExpectations: func(instruction *utils.AnchorInstruction) error { - if !strings.Contains(instruction.Logs[0], "value: 1") { - return fmt.Errorf("expected log to contain 'value: 1', got: %s", instruction.Logs[0]) - } - return nil - }, - }, - { - ExpectedMethod: "AccountMut", - Data: getAnchorInstructionData("account_mut", nil), - RemainingAccounts: []*solana.AccountMeta{ - { - PublicKey: config.StubAccountPDA, - IsSigner: false, - IsWritable: true, - }, - { - PublicKey: McmSignerAddress(config.TestMsigNamePaddedBuffer), - IsSigner: false, - IsWritable: true, - }, - { - PublicKey: solana.SystemProgramID, - IsSigner: false, - IsWritable: false, - }, - }, - ExpectedLogSubstr: "Called `account_mut`", - CheckExpectations: func(instruction *utils.AnchorInstruction) error { - if !strings.Contains(instruction.Logs[0], "is_writable: true") { - return fmt.Errorf("expected log to contain 'is_writable: true', got: %s", instruction.Logs[0]) - } - return nil - }, - }, - } - t.Run("setup:funding", func(t *testing.T) { utils.FundAccounts(ctx, []solana.PrivateKey{admin, user}, solanaGoClient, t) + }) + + t.Run("mcm:general test cases", func(t *testing.T) { + // mcm name + testMsigName := config.TestMsigNamePaddedBuffer + + // test mcm pdas + multisigConfigPDA := McmConfigAddress(testMsigName) + rootMetadataPDA := RootMetadataAddress(testMsigName) + expiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(testMsigName) + configSignersPDA := McmConfigSignersAddress(testMsigName) + msigSignerPDA := McmSignerAddress(testMsigName) + // fund the signer pda - fundPDAIx := system.NewTransferInstruction(1*solana.LAMPORTS_PER_SOL, admin.PublicKey(), MsigSignerPDA).Build() + fundPDAIx := system.NewTransferInstruction(1*solana.LAMPORTS_PER_SOL, admin.PublicKey(), msigSignerPDA).Build() result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{fundPDAIx}, admin, config.DefaultCommitment) require.NotNil(t, result) - }) - - t.Run("fail: NOT able to init program from non-deployer user", func(t *testing.T) { - // get program data account - data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ - Commitment: config.DefaultCommitment, - }) - require.NoError(t, accErr) - // decode program data - var programData struct { - DataType uint32 - Address solana.PublicKey + // helper to inject anchor discriminator into instruction data + // NOTE: if ix is built with anchor-go we don't need it + getAnchorInstructionData := func(method string, data []byte) []byte { + discriminator := sha256.Sum256([]byte("global:" + method)) + return append(discriminator[:8], data...) } - require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - - ix, initErr := mcm.NewInitializeInstruction( - config.TestChainID, - TestMsigName, - MultisigConfigPDA, - user.PublicKey(), - solana.SystemProgramID, - config.McmProgram, - programData.Address, - RootMetadataPDA, - ExpiringRootAndOpCountPDA, - ).ValidateAndBuild() - require.NoError(t, initErr) - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, user, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedMcmError.String()}) - require.NotNil(t, result) - }) - - t.Run("setup:mcm", func(t *testing.T) { - // get program data account - data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ - Commitment: config.DefaultCommitment, - }) - require.NoError(t, accErr) - // decode program data - var programData struct { - DataType uint32 - Address solana.PublicKey - } - require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - - ix, initErr := mcm.NewInitializeInstruction( - config.TestChainID, - TestMsigName, - MultisigConfigPDA, - admin.PublicKey(), - solana.SystemProgramID, - config.McmProgram, - programData.Address, - RootMetadataPDA, - ExpiringRootAndOpCountPDA, - ).ValidateAndBuild() - require.NoError(t, initErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - - // get config and validate - var configAccount mcm.MultisigConfig - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) - require.NoError(t, err, "failed to get account info") - - require.Equal(t, config.TestChainID, configAccount.ChainId) - require.Equal(t, admin.PublicKey(), configAccount.Owner) - }) - - numSigners := 50 - signerPrivateKeys, err := eth.GenerateEthPrivateKeys(numSigners) - - t.Run("mcm:set_config:success", func(t *testing.T) { - require.NoError(t, err) - - signerGroups := make([]byte, numSigners) - for i := 0; i < len(signerGroups); i++ { - signerGroups[i] = byte(i % 5) + // NOTE: this list of operations is methods for testing program, + // contracts/programs/external_program_cpi_stub + // the other way to construct mcmTestOp is using IxToMcmTestOpNode + var stupProgramTestMcmOps = []TestMcmOperation{ + { + ExpectedMethod: "Initialize", + Data: getAnchorInstructionData("initialize", nil), + ExpectedLogSubstr: "Called `initialize`", + RemainingAccounts: []*solana.AccountMeta{ + { + PublicKey: config.StubAccountPDA, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: McmSignerAddress(config.TestMsigNamePaddedBuffer), + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: solana.SystemProgramID, + IsSigner: false, + IsWritable: false, + }, + }, + }, + { + ExpectedMethod: "Empty", + Data: getAnchorInstructionData("empty", nil), + ExpectedLogSubstr: "Called `empty`", + }, + { + ExpectedMethod: "U8InstructionData", + Data: getAnchorInstructionData("u8_instruction_data", []byte{123}), + ExpectedLogSubstr: "Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", + }, + { + ExpectedMethod: "StructInstructionData", + Data: getAnchorInstructionData("struct_instruction_data", []byte{234}), + ExpectedLogSubstr: "Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", + }, + { + ExpectedMethod: "AccountRead", + Data: getAnchorInstructionData("account_read", nil), + RemainingAccounts: []*solana.AccountMeta{ + { + PublicKey: config.StubAccountPDA, + IsSigner: false, + IsWritable: false, + }, + }, + ExpectedLogSubstr: "Called `account_read`", + CheckExpectations: func(instruction *utils.AnchorInstruction) error { + if !strings.Contains(instruction.Logs[0], "value: 1") { + return fmt.Errorf("expected log to contain 'value: 1', got: %s", instruction.Logs[0]) + } + return nil + }, + }, + { + ExpectedMethod: "AccountMut", + Data: getAnchorInstructionData("account_mut", nil), + RemainingAccounts: []*solana.AccountMeta{ + { + PublicKey: config.StubAccountPDA, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: McmSignerAddress(config.TestMsigNamePaddedBuffer), + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: solana.SystemProgramID, + IsSigner: false, + IsWritable: false, + }, + }, + ExpectedLogSubstr: "Called `account_mut`", + CheckExpectations: func(instruction *utils.AnchorInstruction) error { + if !strings.Contains(instruction.Logs[0], "is_writable: true") { + return fmt.Errorf("expected log to contain 'is_writable: true', got: %s", instruction.Logs[0]) + } + return nil + }, + }, } - // just use simple config for now - groupQuorums := []uint8{1, 1, 1, 1, 1} - groupParents := []uint8{0, 0, 0, 2, 0} - - mcmConfig, configErr := mcmsUtils.NewValidMcmConfig( - TestMsigName, - signerPrivateKeys, - signerGroups, - groupQuorums, - groupParents, - config.ClearRoot, - ) - require.NoError(t, configErr) + t.Run("should not be able to init program from non-deployer", func(t *testing.T) { + // get program data account + data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ + Commitment: config.DefaultCommitment, + }) + require.NoError(t, accErr) + + // decode program data + var programData struct { + DataType uint32 + Address solana.PublicKey + } + require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - signerAddresses := mcmConfig.SignerAddresses + ix, initErr := mcm.NewInitializeInstruction( + config.TestChainID, + testMsigName, + multisigConfigPDA, + user.PublicKey(), + solana.SystemProgramID, + config.McmProgram, + programData.Address, + rootMetadataPDA, + expiringRootAndOpCountPDA, + ).ValidateAndBuild() + require.NoError(t, initErr) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, user, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedMcmError.String()}) + require.NotNil(t, result) + }) - t.Run("mcm:preload signers", func(t *testing.T) { - ixs := make([]solana.Instruction, 0) + t.Run("setup:mcm", func(t *testing.T) { + // get program data account + data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ + Commitment: config.DefaultCommitment, + }) + require.NoError(t, accErr) + + // decode program data + var programData struct { + DataType uint32 + Address solana.PublicKey + } + require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - parsedTotalSigners, pErr := mcmsUtils.SafeToUint8(len(signerAddresses)) - require.NoError(t, pErr) - initSignersIx, isErr := mcm.NewInitSignersInstruction( - TestMsigName, - parsedTotalSigners, - MultisigConfigPDA, - ConfigSignersPDA, + ix, initErr := mcm.NewInitializeInstruction( + config.TestChainID, + testMsigName, + multisigConfigPDA, admin.PublicKey(), solana.SystemProgramID, + config.McmProgram, + programData.Address, + rootMetadataPDA, + expiringRootAndOpCountPDA, ).ValidateAndBuild() + require.NoError(t, initErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + + // get config and validate + var configAccount mcm.MultisigConfig + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) + require.NoError(t, err, "failed to get account info") - require.NoError(t, isErr) - ixs = append(ixs, initSignersIx) + require.Equal(t, config.TestChainID, configAccount.ChainId) + require.Equal(t, admin.PublicKey(), configAccount.Owner) + }) - appendSignersIxs, asErr := AppendSignersIxs(signerAddresses, TestMsigName, MultisigConfigPDA, ConfigSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) - require.NoError(t, asErr) - ixs = append(ixs, appendSignersIxs...) + numSigners := 50 + signerPrivateKeys, err := eth.GenerateEthPrivateKeys(numSigners) - finalizeSignersIx, fsErr := mcm.NewFinalizeSignersInstruction( - TestMsigName, - MultisigConfigPDA, - ConfigSignersPDA, - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, fsErr) - ixs = append(ixs, finalizeSignersIx) + t.Run("mcm:set_config:success", func(t *testing.T) { + require.NoError(t, err) - for _, ix := range ixs { - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + signerGroups := make([]byte, numSigners) + for i := 0; i < len(signerGroups); i++ { + signerGroups[i] = byte(i % 5) } - var cfgSignersAccount mcm.ConfigSigners - queryErr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, ConfigSignersPDA, config.DefaultCommitment, &cfgSignersAccount) - require.NoError(t, queryErr, "failed to get account info") + // just use simple config for now + groupQuorums := []uint8{1, 1, 1, 1, 1} + groupParents := []uint8{0, 0, 0, 2, 0} + + mcmConfig, configErr := mcmsUtils.NewValidMcmConfig( + testMsigName, + signerPrivateKeys, + signerGroups, + groupQuorums, + groupParents, + config.ClearRoot, + ) + require.NoError(t, configErr) + + signerAddresses := mcmConfig.SignerAddresses + + t.Run("mcm:preload signers", func(t *testing.T) { + ixs := make([]solana.Instruction, 0) + + parsedTotalSigners, pErr := mcmsUtils.SafeToUint8(len(signerAddresses)) + require.NoError(t, pErr) + initSignersIx, isErr := mcm.NewInitSignersInstruction( + testMsigName, + parsedTotalSigners, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, isErr) + ixs = append(ixs, initSignersIx) + + appendSignersIxs, asErr := AppendSignersIxs(signerAddresses, testMsigName, multisigConfigPDA, configSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) + require.NoError(t, asErr) + ixs = append(ixs, appendSignersIxs...) + + finalizeSignersIx, fsErr := mcm.NewFinalizeSignersInstruction( + testMsigName, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, fsErr) + ixs = append(ixs, finalizeSignersIx) + + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } - require.Equal(t, true, cfgSignersAccount.IsFinalized) + var cfgSignersAccount mcm.ConfigSigners + queryErr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA, config.DefaultCommitment, &cfgSignersAccount) + require.NoError(t, queryErr, "failed to get account info") - // check if the addresses are registered correctly - for i, signer := range cfgSignersAccount.SignerAddresses { - require.Equal(t, signerAddresses[i], signer) - } - }) + require.Equal(t, true, cfgSignersAccount.IsFinalized) - // set config - ix, configErr := mcm.NewSetConfigInstruction( - mcmConfig.MultisigName, - mcmConfig.SignerGroups, - mcmConfig.GroupQuorums, - mcmConfig.GroupParents, - mcmConfig.ClearRoot, - MultisigConfigPDA, - ConfigSignersPDA, - admin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - - require.NoError(t, configErr) - - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - require.NotNil(t, result) + // check if the addresses are registered correctly + for i, signer := range cfgSignersAccount.SignerAddresses { + require.Equal(t, signerAddresses[i], signer) + } + }) + + // set config + ix, configErr := mcm.NewSetConfigInstruction( + mcmConfig.MultisigName, + mcmConfig.SignerGroups, + mcmConfig.GroupQuorums, + mcmConfig.GroupParents, + mcmConfig.ClearRoot, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() - // get config and validate - var configAccount mcm.MultisigConfig - configErr = utils.GetAccountDataBorshInto(ctx, solanaGoClient, MultisigConfigPDA, config.DefaultCommitment, &configAccount) - require.NoError(t, configErr, "failed to get account info") + require.NoError(t, configErr) - require.Equal(t, config.TestChainID, configAccount.ChainId) - require.Equal(t, reflect.DeepEqual(configAccount.GroupParents, mcmConfig.GroupParents), true) - require.Equal(t, reflect.DeepEqual(configAccount.GroupQuorums, mcmConfig.GroupQuorums), true) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + require.NotNil(t, result) - // check if the McmSigner struct is correct - for i, signer := range configAccount.Signers { - require.Equal(t, signer.EvmAddress, mcmConfig.SignerAddresses[i]) - require.Equal(t, signer.Index, uint8(i)) - require.Equal(t, signer.Group, mcmConfig.SignerGroups[i]) - } - }) + // get config and validate + var configAccount mcm.MultisigConfig + configErr = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) + require.NoError(t, configErr, "failed to get account info") - var opNodes []mcmsUtils.McmOpNode + require.Equal(t, config.TestChainID, configAccount.ChainId) + require.Equal(t, reflect.DeepEqual(configAccount.GroupParents, mcmConfig.GroupParents), true) + require.Equal(t, reflect.DeepEqual(configAccount.GroupQuorums, mcmConfig.GroupQuorums), true) - t.Run("mcm:set_root:success", func(t *testing.T) { - for i, op := range stupProgramTestMcmOps { - node := mcmsUtils.McmOpNode{ - Nonce: uint64(i), - Multisig: MultisigConfigPDA, - To: config.ExternalCpiStubProgram, - Data: op.Data, - RemainingAccounts: op.RemainingAccounts, + // check if the McmSigner struct is correct + for i, signer := range configAccount.Signers { + require.Equal(t, signer.EvmAddress, mcmConfig.SignerAddresses[i]) + require.Equal(t, signer.Index, uint8(i)) + require.Equal(t, signer.Group, mcmConfig.SignerGroups[i]) } - opNodes = append(opNodes, node) - } - validUntil := uint32(0xffffffff) - - rootValidationData, rvErr := CreateMcmRootData( - McmRootInput{ - Multisig: MultisigConfigPDA, - Operations: opNodes, - PreOpCount: 0, - PostOpCount: uint64(len(opNodes)), - ValidUntil: validUntil, - OverridePreviousRoot: false, - }, - ) - require.NoError(t, rvErr) - signaturesPDA := RootSignaturesAddress(TestMsigName, rootValidationData.Root, validUntil) - - t.Run("mcm:preload signatures", func(t *testing.T) { - signers, getSignerErr := eth.GetEvmSigners(signerPrivateKeys) - require.NoError(t, getSignerErr, "Failed to get signers") - - signatures, sigsErr := BulkSignOnMsgHash(signers, rootValidationData.EthMsgHash) - require.NoError(t, sigsErr) + }) - parsedTotalSigs, pErr := mcmsUtils.SafeToUint8(len(signatures)) - require.NoError(t, pErr) + var opNodes []mcmsUtils.McmOpNode - initSigsIx, isErr := mcm.NewInitSignaturesInstruction( - TestMsigName, - rootValidationData.Root, - validUntil, - parsedTotalSigs, - signaturesPDA, - admin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() + t.Run("mcm set_root happy path", func(t *testing.T) { + for i, op := range stupProgramTestMcmOps { + node := mcmsUtils.McmOpNode{ + Nonce: uint64(i), + Multisig: multisigConfigPDA, + To: config.ExternalCpiStubProgram, + Data: op.Data, + RemainingAccounts: op.RemainingAccounts, + } + opNodes = append(opNodes, node) + } + validUntil := uint32(0xffffffff) + + rootValidationData, rvErr := CreateMcmRootData( + McmRootInput{ + Multisig: multisigConfigPDA, + Operations: opNodes, + PreOpCount: 0, + PostOpCount: uint64(len(opNodes)), + ValidUntil: validUntil, + OverridePreviousRoot: false, + }, + ) + require.NoError(t, rvErr) + signaturesPDA := RootSignaturesAddress(testMsigName, rootValidationData.Root, validUntil) + + t.Run("preload signatures", func(t *testing.T) { + signers, getSignerErr := eth.GetEvmSigners(signerPrivateKeys) + require.NoError(t, getSignerErr, "Failed to get signers") + + signatures, sigsErr := BulkSignOnMsgHash(signers, rootValidationData.EthMsgHash) + require.NoError(t, sigsErr) + + parsedTotalSigs, pErr := mcmsUtils.SafeToUint8(len(signatures)) + require.NoError(t, pErr) + + initSigsIx, isErr := mcm.NewInitSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + parsedTotalSigs, + signaturesPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, isErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initSigsIx}, admin, config.DefaultCommitment) + + appendSigsIxs, asErr := AppendSignaturesIxs(signatures, testMsigName, rootValidationData.Root, validUntil, signaturesPDA, admin.PublicKey(), config.MaxAppendSignatureBatchSize) + require.NoError(t, asErr) + + // partially register signatures + for _, ix := range appendSigsIxs[:3] { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } - require.NoError(t, isErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initSigsIx}, admin, config.DefaultCommitment) + // clear uploaded signatures(this closes the account) + clearIx, cErr := mcm.NewClearSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + signaturesPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, cErr) + + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{clearIx}, admin, config.DefaultCommitment) + utils.AssertClosedAccount(ctx, t, solanaGoClient, signaturesPDA, config.DefaultCommitment) + + reInitSigsIx, rIsErr := mcm.NewInitSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + parsedTotalSigs, + signaturesPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, rIsErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{reInitSigsIx}, admin, config.DefaultCommitment) + + // register all signatures again + for _, ix := range appendSigsIxs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } - appendSigsIxs, asErr := AppendSignaturesIxs(signatures, TestMsigName, rootValidationData.Root, validUntil, signaturesPDA, admin.PublicKey(), config.MaxAppendSignatureBatchSize) - require.NoError(t, asErr) + finalizeSigsIx, fsErr := mcm.NewFinalizeSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + signaturesPDA, + admin.PublicKey(), + ).ValidateAndBuild() - // partially register signatures - for _, ix := range appendSigsIxs[:3] { - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - } + require.NoError(t, fsErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finalizeSigsIx}, admin, config.DefaultCommitment) - // clear uploaded signatures - clearIx, cErr := mcm.NewClearSignaturesInstruction( - TestMsigName, - rootValidationData.Root, - validUntil, - signaturesPDA, - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, cErr) + var sigAccount mcm.RootSignatures + queryErr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, signaturesPDA, config.DefaultCommitment, &sigAccount) + require.NoError(t, queryErr, "failed to get account info") - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{clearIx}, admin, config.DefaultCommitment) + require.Equal(t, true, sigAccount.IsFinalized) + require.Equal(t, true, sigAccount.TotalSignatures == uint8(len(signatures))) - // register all signatures again - for _, ix := range appendSigsIxs { - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - } + // check if the sigs are registered correctly + for i, sig := range sigAccount.Signatures { + require.Equal(t, signatures[i], sig) + } + }) - finalizeSigsIx, fsErr := mcm.NewFinalizeSignaturesInstruction( - TestMsigName, + newIx, setRootIxErr := mcm.NewSetRootInstruction( + testMsigName, rootValidationData.Root, validUntil, + rootValidationData.Metadata, + rootValidationData.MetadataProof, signaturesPDA, + rootMetadataPDA, + SeenSignedHashesAddress(testMsigName, rootValidationData.Root, validUntil), + expiringRootAndOpCountPDA, + multisigConfigPDA, admin.PublicKey(), + solana.SystemProgramID, ).ValidateAndBuild() + require.NoError(t, setRootIxErr) - require.NoError(t, fsErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finalizeSigsIx}, admin, config.DefaultCommitment) - - var sigAccount mcm.RootSignatures - queryErr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, signaturesPDA, config.DefaultCommitment, &sigAccount) - require.NoError(t, queryErr, "failed to get account info") - - require.Equal(t, true, sigAccount.IsFinalized) - require.Equal(t, true, sigAccount.TotalSignatures == uint8(len(signatures))) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{newIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000)) + require.NotNil(t, tx) - // check if the sigs are registered correctly - for i, sig := range sigAccount.Signatures { - require.Equal(t, signatures[i], sig) - } + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[NewRoot]("NewRoot"), + }, + ) + event := parsedLogs[0].EventData[0].Data.(*NewRoot) + require.Equal(t, rootValidationData.Root, event.Root) + require.Equal(t, validUntil, event.ValidUntil) + require.Equal(t, rootValidationData.Metadata.ChainId, event.MetadataChainID) + require.Equal(t, multisigConfigPDA, event.MetadataMultisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, event.MetadataPreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, event.MetadataPostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, event.MetadataOverridePreviousRoot) + + var newRootAndOpCount mcm.ExpiringRootAndOpCount + + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, expiringRootAndOpCountPDA, config.DefaultCommitment, &newRootAndOpCount) + require.NoError(t, err, "failed to get account info") + + require.Equal(t, rootValidationData.Root, newRootAndOpCount.Root) + require.Equal(t, validUntil, newRootAndOpCount.ValidUntil) + require.Equal(t, rootValidationData.Metadata.PreOpCount, newRootAndOpCount.OpCount) + + // get config and validate + var newRootMetadata mcm.RootMetadata + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, rootMetadataPDA, config.DefaultCommitment, &newRootMetadata) + require.NoError(t, err, "failed to get account info") + + require.Equal(t, rootValidationData.Metadata.ChainId, newRootMetadata.ChainId) + require.Equal(t, rootValidationData.Metadata.Multisig, newRootMetadata.Multisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, newRootMetadata.PreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, newRootMetadata.PostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, newRootMetadata.OverridePreviousRoot) }) - newIx, setRootIxErr := mcm.NewSetRootInstruction( - TestMsigName, - rootValidationData.Root, - validUntil, - rootValidationData.Metadata, - rootValidationData.MetadataProof, - signaturesPDA, - RootMetadataPDA, - SeenSignedHashesAddress(TestMsigName, rootValidationData.Root, validUntil), - ExpiringRootAndOpCountPDA, - MultisigConfigPDA, - admin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, setRootIxErr) - cu := uint32(numSigners * 28_000) //estimated cu per signer - require.True(t, cu <= 1_400_000, "maximum CU limit exceeded") - cuIx, cuErr := computebudget.NewSetComputeUnitLimitInstruction(cu).ValidateAndBuild() - require.NoError(t, cuErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{cuIx, newIx}, admin, config.DefaultCommitment) - require.NotNil(t, result) - - var newRootAndOpCount mcm.ExpiringRootAndOpCount - - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, ExpiringRootAndOpCountPDA, config.DefaultCommitment, &newRootAndOpCount) - require.NoError(t, err, "failed to get account info") - - require.Equal(t, rootValidationData.Root, newRootAndOpCount.Root) - require.Equal(t, validUntil, newRootAndOpCount.ValidUntil) - require.Equal(t, rootValidationData.Metadata.PreOpCount, newRootAndOpCount.OpCount) + t.Run("mcm execute happy path", func(t *testing.T) { + for i, op := range opNodes { + proofs, proofsErr := op.Proofs() + require.NoError(t, proofsErr, "Failed to getting op proof") + + ix := mcm.NewExecuteInstruction( + testMsigName, + config.TestChainID, + op.Nonce, + op.Data, + proofs, + + multisigConfigPDA, + rootMetadataPDA, + expiringRootAndOpCountPDA, + config.ExternalCpiStubProgram, + McmSignerAddress(testMsigName), + admin.PublicKey(), + ) + // append remaining accounts + ix.AccountMetaSlice = append(ix.AccountMetaSlice, op.RemainingAccounts...) + + vIx, vIxErr := ix.ValidateAndBuild() + require.NoError(t, vIxErr) + + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment) + require.NotNil(t, tx.Meta) + require.Nil(t, tx.Meta.Err, fmt.Sprintf("tx failed with: %+v", tx.Meta)) + parsedInstructions := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[OpExecuted]("OpExecuted"), + }, + ) + + require.Len(t, parsedInstructions, 1, "Expected 1 top-level instruction") + + topLevelInstruction := parsedInstructions[0] + require.Equal(t, "Execute", topLevelInstruction.Name, "Top-level instruction should be Execute") + require.Equal(t, config.McmProgram.String(), topLevelInstruction.ProgramID, "Top-level instruction should be executed by MCM program") + + require.Len(t, topLevelInstruction.InnerCalls, 1, "Expected 1 inner call") + innerCall := topLevelInstruction.InnerCalls[0] + + require.Equal(t, stupProgramTestMcmOps[i].ExpectedMethod, innerCall.Name, "Inner call name should match the expected method") + require.Equal(t, config.ExternalCpiStubProgram.String(), innerCall.ProgramID, "Inner call should be executed by external CPI stub program") + + require.NotEmpty(t, innerCall.Logs, "Inner call should have logs") + require.Contains(t, innerCall.Logs[0], stupProgramTestMcmOps[i].ExpectedLogSubstr, "Inner call log should contain expected substring") + + if stupProgramTestMcmOps[i].CheckExpectations != nil { + vIxErr = stupProgramTestMcmOps[i].CheckExpectations(innerCall) + require.NoError(t, vIxErr, "Custom expectations check failed") + } + } - // get config and validate - var newRootMetadata mcm.RootMetadata - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, RootMetadataPDA, config.DefaultCommitment, &newRootMetadata) - require.NoError(t, err, "failed to get account info") + var stubAccountValue external_program_cpi_stub.Value + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.StubAccountPDA, config.DefaultCommitment, &stubAccountValue) + require.NoError(t, err, "failed to get account info") - require.Equal(t, rootValidationData.Metadata.ChainId, newRootMetadata.ChainId) - require.Equal(t, rootValidationData.Metadata.Multisig, newRootMetadata.Multisig) - require.Equal(t, rootValidationData.Metadata.PreOpCount, newRootMetadata.PreOpCount) - require.Equal(t, rootValidationData.Metadata.PostOpCount, newRootMetadata.PostOpCount) - require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, newRootMetadata.OverridePreviousRoot) + require.Equal(t, uint8(2), stubAccountValue.Value) + }) }) - t.Run("mcm:execute:success", func(t *testing.T) { - for i, op := range opNodes { - proofs, proofsErr := op.Proofs() - require.NoError(t, proofsErr, "Failed to getting op proof") + t.Run("mcm set_root validations", func(t *testing.T) { + type TestStage int - ix := mcm.NewExecuteInstruction( - TestMsigName, - config.TestChainID, - op.Nonce, - op.Data, - proofs, - - MultisigConfigPDA, - RootMetadataPDA, - ExpiringRootAndOpCountPDA, - config.ExternalCpiStubProgram, - McmSignerAddress(TestMsigName), - admin.PublicKey(), - ) - // append remaining accounts - ix.AccountMetaSlice = append(ix.AccountMetaSlice, op.RemainingAccounts...) - - vIx, vIxErr := ix.ValidateAndBuild() - require.NoError(t, vIxErr) - - tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment) - require.NotNil(t, tx.Meta) - require.Nil(t, tx.Meta.Err, fmt.Sprintf("tx failed with: %+v", tx.Meta)) - - parsedInstructions := utils.ParseLogMessages(tx.Meta.LogMessages) + const ( + InitSignatures TestStage = iota + AppendSignatures + FinalizeSignatures + SetRoot + Execute + ) - require.Len(t, parsedInstructions, 1, "Expected 1 top-level instruction") + type TxWithStage struct { + Instructions []solana.Instruction + Stage TestStage + } - topLevelInstruction := parsedInstructions[0] - require.Equal(t, "Execute", topLevelInstruction.Name, "Top-level instruction should be Execute") - require.Equal(t, config.McmProgram.String(), topLevelInstruction.ProgramID, "Top-level instruction should be executed by MCM program") + tests := []struct { + name string + errorMsg string + failureStage TestStage + modifyTxs func(*[]TxWithStage) + modifySigs func(*[]mcm.Signature, *McmRootData) + }{ + { + name: "should not be able to initialize signatures more than one time ", + errorMsg: "already in use Program 11111111111111111111111111111111 failed", // account creation failure from system program + failureStage: InitSignatures, + modifyTxs: func(txs *[]TxWithStage) { + // find the index of the first initSignatures instruction + var initSigIndex int + for i, tx := range *txs { + if tx.Stage == InitSignatures { + initSigIndex = i + break + } + } + (*txs)[initSigIndex] = TxWithStage{ + Instructions: []solana.Instruction{(*txs)[initSigIndex].Instructions[0], (*txs)[initSigIndex].Instructions[0]}, + Stage: InitSignatures, + } + }, + }, + { + name: "should not be able to finalize signatures before having all signatures", + errorMsg: "Error Code: " + mcm.SignatureCountMismatch_McmError.String(), + failureStage: FinalizeSignatures, + modifyTxs: func(txs *[]TxWithStage) { + finalizeSigsIdx := -1 + appendSigsIdx := -1 + for i, tx := range *txs { + if tx.Stage == FinalizeSignatures { + finalizeSigsIdx = i + } + if tx.Stage == AppendSignatures { + appendSigsIdx = i + } + } + // move finalize before append in place + if finalizeSigsIdx > appendSigsIdx { + (*txs)[finalizeSigsIdx], (*txs)[appendSigsIdx] = (*txs)[appendSigsIdx], (*txs)[finalizeSigsIdx] + } + }, + }, + { + name: "attempt to append more signatures than defined in total", + errorMsg: "Error Code: " + mcm.TooManySignatures_McmError.String(), + failureStage: AppendSignatures, + modifyTxs: func(txs *[]TxWithStage) { + appendSigsIdx := -1 + for i, tx := range *txs { + if tx.Stage == AppendSignatures { + appendSigsIdx = i + } + } + (*txs)[appendSigsIdx] = TxWithStage{ + Instructions: []solana.Instruction{(*txs)[appendSigsIdx].Instructions[0], (*txs)[appendSigsIdx].Instructions[0]}, + Stage: AppendSignatures, + } + }, + }, + { + name: "signatures not in ascending order", + errorMsg: "Error Code: " + mcm.SignersAddressesMustBeStrictlyIncreasing_McmError.String(), + failureStage: SetRoot, + modifySigs: func(sigs *[]mcm.Signature, _ *McmRootData) { + slices.Reverse(*sigs) + }, + }, + { + name: "should fail set_root when signatures don't meet group quorum", + errorMsg: "Error Code: " + mcm.InsufficientSigners_McmError.String(), + failureStage: SetRoot, + modifySigs: func(sigs *[]mcm.Signature, _ *McmRootData) { + *sigs = (*sigs)[:1] // only keep first signature + }, + }, + { + name: "when message hash is different from the one used to sign", + errorMsg: "Error Code: " + mcm.InvalidSigner_McmError.String(), + failureStage: SetRoot, + modifySigs: func(sigs *[]mcm.Signature, _ *McmRootData) { + // same signers + signers, signerErr := eth.GetEvmSigners(config.SignerPrivateKeys) + require.NoError(t, signerErr) + // but different signatures(wrong eth hash) + // secp256k1_recover_from recovers a valid but different address --> invalidSigner + signatures, sigErr := BulkSignOnMsgHash(signers, bytes.Repeat([]byte{1}, 32)) + require.NoError(t, sigErr) + *sigs = signatures + }, + }, + { + name: "invalid signature should fail ECDSA recovery", + errorMsg: "Error Code: " + mcm.FailedEcdsaRecover_McmError.String(), + failureStage: SetRoot, + modifySigs: func(sigs *[]mcm.Signature, _ *McmRootData) { + invalidSig := (*sigs)[0] + // corrupt V + invalidSig.V = 26 + newSigs := make([]mcm.Signature, len(*sigs)) + for i := range newSigs { + newSigs[i] = invalidSig + } + *sigs = newSigs + }, + }, + { + name: "signatures from unauthorized signers should fail", + errorMsg: "Error Code: " + mcm.InvalidSigner_McmError.String(), + failureStage: SetRoot, + modifySigs: func(sigs *[]mcm.Signature, rootData *McmRootData) { + wrongPrivateKeys, err := eth.GenerateEthPrivateKeys(len(*sigs)) + require.NoError(t, err) + wrongSigners, err := eth.GetEvmSigners(wrongPrivateKeys) + require.NoError(t, err) + signatures, err := BulkSignOnMsgHash(wrongSigners, rootData.EthMsgHash) + require.NoError(t, err) + *sigs = signatures + }, + }, + } - require.Len(t, topLevelInstruction.InnerCalls, 1, "Expected 1 inner call") - innerCall := topLevelInstruction.InnerCalls[0] + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // use different msig accounts per test + testMsigName, err := mcmsUtils.PadString32(fmt.Sprintf("fail_sig_validation_test_%d", i)) + require.NoError(t, err) + + // test scoped mcm pdas + multisigConfigPDA := McmConfigAddress(testMsigName) + multisigSignerPDA := McmSignerAddress(testMsigName) + rootMetadataPDA := RootMetadataAddress(testMsigName) + expiringRootAndOpCountPDA := ExpiringRootAndOpCountAddress(testMsigName) + configSignersPDA := McmConfigSignersAddress(testMsigName) + + // fund the signer pda + fundPDAIx, err := system.NewTransferInstruction(1*solana.LAMPORTS_PER_SOL, admin.PublicKey(), multisigSignerPDA).ValidateAndBuild() + require.NoError(t, err) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, + []solana.Instruction{fundPDAIx}, + admin, config.DefaultCommitment) + require.NotNil(t, result) + + t.Run("setup:initialize mcm", func(t *testing.T) { + // get program data account + data, accErr := solanaGoClient.GetAccountInfoWithOpts(ctx, config.McmProgram, &rpc.GetAccountInfoOpts{ + Commitment: config.DefaultCommitment, + }) + require.NoError(t, accErr) + + // decode program data + var programData struct { + DataType uint32 + Address solana.PublicKey + } + require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) + + ix, initErr := mcm.NewInitializeInstruction( + config.TestChainID, + testMsigName, + multisigConfigPDA, + admin.PublicKey(), + solana.SystemProgramID, + config.McmProgram, + programData.Address, + rootMetadataPDA, + expiringRootAndOpCountPDA, + ).ValidateAndBuild() + require.NoError(t, initErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + + // get config and validate + var configAccount mcm.MultisigConfig + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) + require.NoError(t, err, "failed to get account info") + + require.Equal(t, config.TestChainID, configAccount.ChainId) + require.Equal(t, admin.PublicKey(), configAccount.Owner) + }) + + mcmConfig, configErr := mcmsUtils.NewValidMcmConfig( + testMsigName, + config.SignerPrivateKeys, + config.SignerGroups, + config.GroupQuorums, + config.GroupParents, + config.ClearRoot, + ) + require.NoError(t, configErr) + + t.Run("setup: load signers and set_config", func(t *testing.T) { + ixs := make([]solana.Instruction, 0) + + parsedTotalSigners, pErr := mcmsUtils.SafeToUint8(len(mcmConfig.SignerAddresses)) + require.NoError(t, pErr) + initSignersIx, isErr := mcm.NewInitSignersInstruction( + testMsigName, + parsedTotalSigners, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, isErr) + ixs = append(ixs, initSignersIx) + + appendSignersIxs, asErr := AppendSignersIxs(mcmConfig.SignerAddresses, testMsigName, multisigConfigPDA, configSignersPDA, admin.PublicKey(), config.MaxAppendSignerBatchSize) + require.NoError(t, asErr) + ixs = append(ixs, appendSignersIxs...) + + finalizeSignersIx, fsErr := mcm.NewFinalizeSignersInstruction( + testMsigName, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, fsErr) + ixs = append(ixs, finalizeSignersIx) + + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } + + var cfgSignersAccount mcm.ConfigSigners + queryErr := utils.GetAccountDataBorshInto(ctx, solanaGoClient, configSignersPDA, config.DefaultCommitment, &cfgSignersAccount) + require.NoError(t, queryErr, "failed to get account info") + + require.Equal(t, true, cfgSignersAccount.IsFinalized) + + // check if the addresses are registered correctly + for i, signer := range cfgSignersAccount.SignerAddresses { + require.Equal(t, mcmConfig.SignerAddresses[i], signer) + } + + // set config + ix, configErr := mcm.NewSetConfigInstruction( + mcmConfig.MultisigName, + mcmConfig.SignerGroups, + mcmConfig.GroupQuorums, + mcmConfig.GroupParents, + mcmConfig.ClearRoot, + multisigConfigPDA, + configSignersPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + + require.NoError(t, configErr) + + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + + // get config and validate + var configAccount mcm.MultisigConfig + configErr = utils.GetAccountDataBorshInto(ctx, solanaGoClient, multisigConfigPDA, config.DefaultCommitment, &configAccount) + require.NoError(t, configErr, "failed to get account info") + + require.Equal(t, config.TestChainID, configAccount.ChainId) + require.Equal(t, reflect.DeepEqual(configAccount.GroupParents, mcmConfig.GroupParents), true) + require.Equal(t, reflect.DeepEqual(configAccount.GroupQuorums, mcmConfig.GroupQuorums), true) + + // check if the McmSigner struct is correct + for i, signer := range configAccount.Signers { + require.Equal(t, signer.EvmAddress, mcmConfig.SignerAddresses[i]) + require.Equal(t, signer.Index, uint8(i)) + require.Equal(t, signer.Group, mcmConfig.SignerGroups[i]) + } + }) + + var txs []TxWithStage + + // use simple program for testing + stubProgramIx, err := external_program_cpi_stub.NewEmptyInstruction().ValidateAndBuild() + node, err := IxToMcmTestOpNode(multisigConfigPDA, multisigSignerPDA, stubProgramIx, 0) + require.NoError(t, err) + + validUntil := uint32(0xffffffff) + + // this will be used to generate proof on mcm::execute + ops := []mcmsUtils.McmOpNode{ + node, + } - require.Equal(t, stupProgramTestMcmOps[i].ExpectedMethod, innerCall.Name, "Inner call name should match the expected method") - require.Equal(t, config.ExternalCpiStubProgram.String(), innerCall.ProgramID, "Inner call should be executed by external CPI stub program") + rootValidationData, rvErr := CreateMcmRootData( + McmRootInput{ + Multisig: multisigConfigPDA, + Operations: ops, + PreOpCount: 0, + PostOpCount: 1, + ValidUntil: validUntil, + OverridePreviousRoot: false, + }, + ) + require.NoError(t, rvErr) + signaturesPDA := RootSignaturesAddress(testMsigName, rootValidationData.Root, validUntil) + + signers, getSignerErr := eth.GetEvmSigners(config.SignerPrivateKeys) + require.NoError(t, getSignerErr) + signatures, err := BulkSignOnMsgHash(signers, rootValidationData.EthMsgHash) + require.NoError(t, err) + + if tt.modifySigs != nil { + tt.modifySigs(&signatures, &rootValidationData) + } - require.NotEmpty(t, innerCall.Logs, "Inner call should have logs") - require.Contains(t, innerCall.Logs[0], stupProgramTestMcmOps[i].ExpectedLogSubstr, "Inner call log should contain expected substring") + parsedTotalSigs, err := mcmsUtils.SafeToUint8(len(signatures)) + require.NoError(t, err) + + initSigsIx, err := mcm.NewInitSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + parsedTotalSigs, + signaturesPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, err) + txs = append(txs, TxWithStage{Instructions: []solana.Instruction{initSigsIx}, Stage: InitSignatures}) + + appendSigsIxs, asErr := AppendSignaturesIxs(signatures, testMsigName, rootValidationData.Root, validUntil, signaturesPDA, admin.PublicKey(), config.MaxAppendSignatureBatchSize) + require.NoError(t, asErr) + + // one tx is enough since we only have 5 signers + txs = append(txs, TxWithStage{Instructions: appendSigsIxs, Stage: AppendSignatures}) + + finalizeSigsIx, err := mcm.NewFinalizeSignaturesInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + signaturesPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, err) + + txs = append(txs, TxWithStage{Instructions: []solana.Instruction{finalizeSigsIx}, Stage: FinalizeSignatures}) + + setRootIx, err := mcm.NewSetRootInstruction( + testMsigName, + rootValidationData.Root, + validUntil, + rootValidationData.Metadata, + rootValidationData.MetadataProof, + signaturesPDA, + rootMetadataPDA, + SeenSignedHashesAddress(testMsigName, rootValidationData.Root, validUntil), + expiringRootAndOpCountPDA, + multisigConfigPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, err) + + // set compute budget for signature verification + cuIx, err := computebudget.NewSetComputeUnitLimitInstruction(uint32(1_400_000)).ValidateAndBuild() + require.NoError(t, err) + + txs = append(txs, TxWithStage{Instructions: []solana.Instruction{cuIx, setRootIx}, Stage: SetRoot}) + + // here only one op exists + opNode := ops[0] + proofs, proofsErr := opNode.Proofs() + require.NoError(t, proofsErr, "Failed to getting op proof") + + executeIx := mcm.NewExecuteInstruction( + testMsigName, + config.TestChainID, + opNode.Nonce, + opNode.Data, + proofs, + + multisigConfigPDA, + rootMetadataPDA, + expiringRootAndOpCountPDA, + config.ExternalCpiStubProgram, + multisigSignerPDA, + admin.PublicKey(), + ) + // append remaining accounts + executeIx.AccountMetaSlice = append(executeIx.AccountMetaSlice, opNode.RemainingAccounts...) + + vIx, vIxErr := executeIx.ValidateAndBuild() + require.NoError(t, vIxErr) + + txs = append(txs, TxWithStage{Instructions: []solana.Instruction{vIx}, Stage: Execute}) + + if tt.modifyTxs != nil { + tt.modifyTxs(&txs) + } - if stupProgramTestMcmOps[i].CheckExpectations != nil { - vIxErr = stupProgramTestMcmOps[i].CheckExpectations(innerCall) - require.NoError(t, vIxErr, "Custom expectations check failed") - } + for _, tx := range txs { + if tx.Stage == tt.failureStage { + // this stage should fail + result := utils.SendAndFailWith(ctx, t, solanaGoClient, + tx.Instructions, + admin, + rpc.CommitmentConfirmed, + []string{tt.errorMsg}, + ) + require.NotNil(t, result) + break + } + + // all other instructions should succeed + utils.SendAndConfirm(ctx, t, solanaGoClient, + tx.Instructions, + admin, + config.DefaultCommitment, + ) + } + }) } - - var stubAccountValue external_program_cpi_stub.Value - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.StubAccountPDA, config.DefaultCommitment, &stubAccountValue) - require.NoError(t, err, "failed to get account info") - - require.Equal(t, uint8(2), stubAccountValue.Value) }) } diff --git a/chains/solana/contracts/tests/mcms/mcm_timelock_test.go b/chains/solana/contracts/tests/mcms/mcm_timelock_test.go index 4c8686ff..9043caea 100644 --- a/chains/solana/contracts/tests/mcms/mcm_timelock_test.go +++ b/chains/solana/contracts/tests/mcms/mcm_timelock_test.go @@ -8,12 +8,12 @@ import ( bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" - computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/accesscontroller" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" mcmsUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/mcms" @@ -180,8 +180,19 @@ func TestMcmWithTimelock(t *testing.T) { ).ValidateAndBuild() require.NoError(t, setConfigErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[ConfigSet]("ConfigSet"), + }, + ) + + event := parsedLogs[0].EventData[0].Data.(*ConfigSet) + require.Equal(t, msig.RawConfig.GroupParents, event.GroupParents) + require.Equal(t, msig.RawConfig.GroupQuorums, event.GroupQuorums) + require.Equal(t, msig.RawConfig.ClearRoot, event.IsRootCleared) // get config and validate var configAccount mcm.MultisigConfig @@ -283,24 +294,10 @@ func TestMcmWithTimelock(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - var ac access_controller.AccessController - err = utils.GetAccountDataBorshInto( - ctx, - solanaGoClient, - roleMsigs.AccessController.PublicKey(), - config.DefaultCommitment, - &ac, - ) - require.NoError(t, err) - - require.Equal(t, uint64(len(roleMsigs.Multisigs)), ac.AccessList.Len, - "AccessList length mismatch for %s", role) - for _, msig := range roleMsigs.Multisigs { - targetPubKey := msig.SignerPDA - _, found := mcmsUtils.FindInSortedList(ac.AccessList.Xs[:ac.AccessList.Len], targetPubKey) - require.True(t, found, "Account %s not found in %s AccessList", - targetPubKey, role) + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, roleMsigs.AccessController.PublicKey(), msig.SignerPDA, config.DefaultCommitment) + require.NoError(t, ferr) + require.True(t, found, "Account %s not found in %s AccessList", msig.SignerPDA, role) } }) } @@ -350,53 +347,35 @@ func TestMcmWithTimelock(t *testing.T) { id := acceptOwnershipOp.OperationID() operationPDA := acceptOwnershipOp.OperationPDA() - ixs := make([]solana.Instruction, 0) - initOpIx, initOpErr := timelock.NewInitializeOperationInstruction( - acceptOwnershipOp.OperationID(), - acceptOwnershipOp.Predecessor, - acceptOwnershipOp.Salt, - acceptOwnershipOp.IxsCountU32(), - config.TimelockConfigPDA, - operationPDA, - admin.PublicKey(), - admin.PublicKey(), // proposer - direct schedule batch here - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, initOpErr) - ixs = append(ixs, initOpIx) - - appendIxIx, apErr := timelock.NewAppendInstructionsInstruction( - acceptOwnershipOp.OperationID(), - acceptOwnershipOp.ToInstructionData(), - operationPDA, - admin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, apErr) - - ixs = append(ixs, appendIxIx) - - finIxIx, finErr := timelock.NewFinalizeOperationInstruction( - acceptOwnershipOp.OperationID(), - operationPDA, - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, finErr) - ixs = append(ixs, finIxIx) - - utils.SendAndConfirm(ctx, t, solanaGoClient, ixs, admin, config.DefaultCommitment) + ixs, ierr := TimelockPreloadOperationIxs(ctx, acceptOwnershipOp, admin.PublicKey(), solanaGoClient) + require.NoError(t, ierr) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) + } scheduleBatchIx, scErr := timelock.NewScheduleBatchInstruction( acceptOwnershipOp.OperationID(), acceptOwnershipOp.Delay, - config.TimelockConfigPDA, operationPDA, + config.TimelockConfigPDA, roleMsigs.AccessController.PublicKey(), admin.PublicKey(), ).ValidateAndBuild() require.NoError(t, scErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{scheduleBatchIx}, admin, config.DefaultCommitment) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{scheduleBatchIx}, admin, config.DefaultCommitment) + parsed := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallScheduled]("CallScheduled"), + }, + ) + + for _, ixx := range acceptOwnershipOp.ToInstructionData() { + event := parsed[0].EventData[0].Data.(*CallScheduled) + require.Equal(t, acceptOwnershipOp.OperationID(), event.ID) + require.Equal(t, acceptOwnershipOp.Salt, event.Salt) + require.Equal(t, ixx.Data, event.Data) + } var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA, config.DefaultCommitment, &opAccount) @@ -405,7 +384,7 @@ func TestMcmWithTimelock(t *testing.T) { } require.Equal(t, - result.BlockTime.Time().Add(time.Duration(acceptOwnershipOp.Delay)*time.Second).Unix(), + tx.BlockTime.Time().Add(time.Duration(acceptOwnershipOp.Delay)*time.Second).Unix(), int64(opAccount.Timestamp), "Scheduled Times don't match", ) @@ -418,9 +397,9 @@ func TestMcmWithTimelock(t *testing.T) { bypassExeIx := timelock.NewBypasserExecuteBatchInstruction( acceptOwnershipOp.OperationID(), + acceptOwnershipOp.OperationPDA(), config.TimelockConfigPDA, config.TimelockSignerPDA, - acceptOwnershipOp.OperationPDA(), roleMsigs.AccessController.PublicKey(), admin.PublicKey(), // bypass execute with admin previledges ) @@ -429,7 +408,20 @@ func TestMcmWithTimelock(t *testing.T) { vIx, vIxErr := bypassExeIx.ValidateAndBuild() require.NoError(t, vIxErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment) + acceptTx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment) + + parsedLogs := utils.ParseLogMessages(acceptTx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[BypasserCallExecuted]("BypasserCallExecuted"), + }, + ) + + for i, ixx := range acceptOwnershipOp.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*BypasserCallExecuted) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, msig.ConfigPDA, config.DefaultCommitment, &configAccount) if err != nil { @@ -523,47 +515,18 @@ func TestMcmWithTimelock(t *testing.T) { opToSchedule.AddInstruction(ix, []solana.PublicKey{v.tokenProgram}) } - initOpIx, ioErr := timelock.NewInitializeOperationInstruction( - opToSchedule.OperationID(), - opToSchedule.Predecessor, - opToSchedule.Salt, - uint32(len(opToSchedule.instructions)), - config.TimelockConfigPDA, - opToSchedule.OperationPDA(), - admin.PublicKey(), - proposerMsig.SignerPDA, - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, ioErr) - - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initOpIx}, admin, config.DefaultCommitment) - - for _, instruction := range opToSchedule.ToInstructionData() { - appendIxsIx, apErr := timelock.NewAppendInstructionsInstruction( - opToSchedule.OperationID(), - []timelock.InstructionData{instruction}, // this should be a slice of instruction within 1232 bytes - opToSchedule.OperationPDA(), - admin.PublicKey(), - solana.SystemProgramID, // for reallocation - ).ValidateAndBuild() - require.NoError(t, apErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{appendIxsIx}, admin, config.DefaultCommitment) + ixs, ierr := TimelockPreloadOperationIxs(ctx, opToSchedule, admin.PublicKey(), solanaGoClient) + require.NoError(t, ierr) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - finOpIx, foErr := timelock.NewFinalizeOperationInstruction( - opToSchedule.OperationID(), - opToSchedule.OperationPDA(), - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, foErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finOpIx}, admin, config.DefaultCommitment) - // Schedule the operation scheduleIx, scErr := timelock.NewScheduleBatchInstruction( opToSchedule.OperationID(), opToSchedule.Delay, - config.TimelockConfigPDA, opToSchedule.OperationPDA(), + config.TimelockConfigPDA, msigs[timelock.Proposer_Role].AccessController.PublicKey(), proposerMsig.SignerPDA, // msig signer since we're going to run this ix with mcm::execute ).ValidateAndBuild() @@ -657,10 +620,22 @@ func TestMcmWithTimelock(t *testing.T) { ).ValidateAndBuild() require.NoError(t, setRootIxErr) - cuIx, cuErr := computebudget.NewSetComputeUnitLimitInstruction(1_400_000).ValidateAndBuild() - require.NoError(t, cuErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{cuIx, newIx}, admin, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{newIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000)) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[NewRoot]("NewRoot"), + }, + ) + event := parsedLogs[0].EventData[0].Data.(*NewRoot) + require.Equal(t, rootValidationData.Root, event.Root) + require.Equal(t, validUntil, event.ValidUntil) + require.Equal(t, rootValidationData.Metadata.ChainId, event.MetadataChainID) + require.Equal(t, proposerMsig.ConfigPDA, event.MetadataMultisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, event.MetadataPreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, event.MetadataPostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, event.MetadataOverridePreviousRoot) var newRootAndOpCount mcm.ExpiringRootAndOpCount @@ -720,6 +695,34 @@ func TestMcmWithTimelock(t *testing.T) { require.NotNil(t, tx.Meta) require.Nil(t, tx.Meta.Err, fmt.Sprintf("tx failed with: %+v", tx.Meta)) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[OpExecuted]("OpExecuted"), + utils.EventMappingFor[CallScheduled]("CallScheduled"), + }, + ) + + // check opExecuted event + event := parsedLogs[0].EventData[0].Data.(*OpExecuted) + require.Equal(t, op.Nonce, event.Nonce) + require.Equal(t, op.To, event.To) + require.Equal(t, op.Data, utils.NormalizeData(event.Data)) + + // check inner CallScheduled events + opIxData := opToSchedule.ToInstructionData() + require.Equal(t, len(opIxData), len(parsedLogs[0].InnerCalls[0].EventData), "Number of actual CallScheduled events does not match expected for operation") + + for j, ix := range opIxData { + timelockEvent := parsedLogs[0].InnerCalls[0].EventData[j].Data.(*CallScheduled) + require.Equal(t, opToSchedule.OperationID(), timelockEvent.ID, "ID does not match") + require.Equal(t, uint64(j), timelockEvent.Index, "Index does not match") + require.Equal(t, ix.ProgramId, timelockEvent.Target, "Target does not match") + require.Equal(t, opToSchedule.Predecessor, timelockEvent.Predecessor, "Predecessor does not match") + require.Equal(t, opToSchedule.Salt, timelockEvent.Salt, "Salt does not match") + require.Equal(t, opToSchedule.Delay, timelockEvent.Delay, "Delay does not match") + require.Equal(t, ix.Data, utils.NormalizeData(timelockEvent.Data), "Data does not match") + } + var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, opToSchedule.OperationPDA(), config.DefaultCommitment, &opAccount) if err != nil { @@ -748,10 +751,10 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("timelock worker -> timelock::execute_batch", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( opToSchedule.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, opToSchedule.OperationPDA(), config.TimelockEmptyOpID, + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), // timelock worker authority ) @@ -761,8 +764,22 @@ func TestMcmWithTimelock(t *testing.T) { vIx, vErr := ix.ValidateAndBuild() require.NoError(t, vErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000)) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000)) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + + for i, ixx := range opToSchedule.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.Equal(t, opToSchedule.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, opToSchedule.OperationPDA(), config.DefaultCommitment, &opAccount) @@ -912,57 +929,21 @@ func TestMcmWithTimelock(t *testing.T) { // Pre-create Timelock Operation PDAs // //////////////////////////////////////// opNodes := []mcmsUtils.McmOpNode{} + timelockOps := []TimelockOperation{op1, op2, op3} - for i, op := range []TimelockOperation{op1, op2, op3} { + for i, op := range timelockOps { t.Run(fmt.Sprintf("prepare mcm op node %d with timelock::schedule_batch ix", i), func(t *testing.T) { - prepareOpIxs := make([]solana.Instruction, 0) - - initOpIx, initOpIxErr := timelock.NewInitializeOperationInstruction( - op.OperationID(), - op.Predecessor, - op.Salt, - op.IxsCountU32(), - config.TimelockConfigPDA, - op.OperationPDA(), - admin.PublicKey(), - proposerMsig.SignerPDA, - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, initOpIxErr) - prepareOpIxs = append(prepareOpIxs, initOpIx) - - for _, ixData := range op.ToInstructionData() { - appendIxsIx, apErr := timelock.NewAppendInstructionsInstruction( - op.OperationID(), - []timelock.InstructionData{ixData}, // this should be a slice of instruction within 1232 bytes - op.OperationPDA(), - admin.PublicKey(), - solana.SystemProgramID, // for reallocation - ).ValidateAndBuild() - require.NoError(t, apErr) - prepareOpIxs = append(prepareOpIxs, appendIxsIx) - } - - finOpIx, finOpErr := timelock.NewFinalizeOperationInstruction( - op.OperationID(), - op.OperationPDA(), - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, finOpErr) - prepareOpIxs = append(prepareOpIxs, finOpIx) - - for _, ix := range prepareOpIxs { + ixs, ierr := TimelockPreloadOperationIxs(ctx, op, admin.PublicKey(), solanaGoClient) + require.NoError(t, ierr) + for _, ix := range ixs { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - /////////////////////////////////////////// - // Construct schedule_batch instruction // - /////////////////////////////////////////// scheduleOpIx, scErr := timelock.NewScheduleBatchInstruction( op.OperationID(), op.Delay, - config.TimelockConfigPDA, op.OperationPDA(), + config.TimelockConfigPDA, msigs[timelock.Proposer_Role].AccessController.PublicKey(), proposerMsig.SignerPDA, ).ValidateAndBuild() @@ -1067,10 +1048,22 @@ func TestMcmWithTimelock(t *testing.T) { ).ValidateAndBuild() require.NoError(t, setRootIxErr) - cuIx, cuErr := computebudget.NewSetComputeUnitLimitInstruction(1_400_000).ValidateAndBuild() - require.NoError(t, cuErr) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{cuIx, newIx}, admin, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{newIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000)) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[NewRoot]("NewRoot"), + }, + ) + event := parsedLogs[0].EventData[0].Data.(*NewRoot) + require.Equal(t, rootValidationData.Root, event.Root) + require.Equal(t, validUntil, event.ValidUntil) + require.Equal(t, rootValidationData.Metadata.ChainId, event.MetadataChainID) + require.Equal(t, proposerMsig.ConfigPDA, event.MetadataMultisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, event.MetadataPreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, event.MetadataPostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, event.MetadataOverridePreviousRoot) var newRootAndOpCount mcm.ExpiringRootAndOpCount @@ -1094,7 +1087,7 @@ func TestMcmWithTimelock(t *testing.T) { }) t.Run("mcm::execute to schedule timelock operations", func(t *testing.T) { - for _, op := range opNodes { + for i, op := range opNodes { proofs, proofsErr := op.Proofs() require.NoError(t, proofsErr) @@ -1117,7 +1110,37 @@ func TestMcmWithTimelock(t *testing.T) { vIx, vIxErr := ix.ValidateAndBuild() require.NoError(t, vIxErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[OpExecuted]("OpExecuted"), + utils.EventMappingFor[CallScheduled]("CallScheduled"), + }, + ) + + // check opExecuted event + event := parsedLogs[0].EventData[0].Data.(*OpExecuted) + require.Equal(t, op.Nonce, event.Nonce) + require.Equal(t, op.To, event.To) + require.Equal(t, op.Data, utils.NormalizeData(event.Data)) + + // check inner CallScheduled events + currentOp := timelockOps[i] // match the TimelockOperation with the current opNode + opIxData := currentOp.ToInstructionData() + + require.Equal(t, len(opIxData), len(parsedLogs[0].InnerCalls[0].EventData), "Number of actual CallScheduled events does not match expected for operation %d", i) + + for j, ix := range opIxData { + timelockEvent := parsedLogs[0].InnerCalls[0].EventData[j].Data.(*CallScheduled) + require.Equal(t, currentOp.OperationID(), timelockEvent.ID, "ID does not match") + require.Equal(t, uint64(j), timelockEvent.Index, "Index does not match") + require.Equal(t, ix.ProgramId, timelockEvent.Target, "Target does not match") + require.Equal(t, currentOp.Predecessor, timelockEvent.Predecessor, "Predecessor does not match") + require.Equal(t, currentOp.Salt, timelockEvent.Salt, "Salt does not match") + require.Equal(t, currentOp.Delay, timelockEvent.Delay, "Delay does not match") + require.Equal(t, ix.Data, utils.NormalizeData(timelockEvent.Data), "Data does not match") + } } }) @@ -1129,8 +1152,8 @@ func TestMcmWithTimelock(t *testing.T) { cancelIx, err := timelock.NewCancelInstruction( op3.OperationID(), - config.TimelockConfigPDA, op3.OperationPDA(), + config.TimelockConfigPDA, msigs[timelock.Canceller_Role].AccessController.PublicKey(), canceller.SignerPDA, ).ValidateAndBuild() @@ -1215,7 +1238,22 @@ func TestMcmWithTimelock(t *testing.T) { ).ValidateAndBuild() require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{setRootIx}, admin, config.DefaultCommitment) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{setRootIx}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[NewRoot]("NewRoot"), + }, + ) + event := parsedLogs[0].EventData[0].Data.(*NewRoot) + require.Equal(t, rootValidationData.Root, event.Root) + require.Equal(t, validUntil, event.ValidUntil) + require.Equal(t, rootValidationData.Metadata.ChainId, event.MetadataChainID) + require.Equal(t, canceller.ConfigPDA, event.MetadataMultisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, event.MetadataPreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, event.MetadataPostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, event.MetadataOverridePreviousRoot) // execute mcm operation to cancel the timelock operation proofs, err := cancleOpNodes[0].Proofs() @@ -1239,8 +1277,27 @@ func TestMcmWithTimelock(t *testing.T) { vIx, err := executeIx.ValidateAndBuild() require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + exeTx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + require.NotNil(t, exeTx) + parsedExeLogs := utils.ParseLogMessages(exeTx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[OpExecuted]("OpExecuted"), + utils.EventMappingFor[Cancelled]("Cancelled"), + }, + ) + + // check opExecuted event + exeEvent := parsedExeLogs[0].EventData[0].Data.(*OpExecuted) + require.Equal(t, node.Nonce, exeEvent.Nonce) + require.Equal(t, node.To, exeEvent.To) + require.Equal(t, node.Data, utils.NormalizeData(exeEvent.Data)) + + // check inner Cancelled event + timelockEvent := parsedExeLogs[0].InnerCalls[0].EventData[0].Data.(*Cancelled) + require.Equal(t, op3.OperationID(), timelockEvent.ID, "ID does not match") + + // check if operation pda is closed utils.AssertClosedAccount(ctx, t, solanaGoClient, op3.OperationPDA(), config.DefaultCommitment) }) @@ -1266,59 +1323,27 @@ func TestMcmWithTimelock(t *testing.T) { newOp3.AddInstruction(ix2, []solana.PublicKey{tokenProgram}) newOp3.AddInstruction(ix3, []solana.PublicKey{tokenProgram}) - // Initialize operation account - initOpIx, err := timelock.NewInitializeOperationInstruction( - newOp3.OperationID(), - newOp3.Predecessor, - newOp3.Salt, - newOp3.IxsCountU32(), - config.TimelockConfigPDA, - newOp3.OperationPDA(), - admin.PublicKey(), - proposerMsig.SignerPDA, - solana.SystemProgramID, - ).ValidateAndBuild() + ixs, err := TimelockPreloadOperationIxs(ctx, newOp3, admin.PublicKey(), solanaGoClient) require.NoError(t, err) - - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initOpIx}, admin, config.DefaultCommitment) - - // Append instructions - for _, ixData := range newOp3.ToInstructionData() { - appendIx, appendIxErr := timelock.NewAppendInstructionsInstruction( - newOp3.OperationID(), - []timelock.InstructionData{ixData}, - newOp3.OperationPDA(), - admin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, appendIxErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{appendIx}, admin, config.DefaultCommitment) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - // Finalize operation - finalizeIx, err := timelock.NewFinalizeOperationInstruction( - newOp3.OperationID(), - newOp3.OperationPDA(), - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finalizeIx}, admin, config.DefaultCommitment) - // Create mcm operation node for scheduling scheduleIx, err := timelock.NewScheduleBatchInstruction( newOp3.OperationID(), newOp3.Delay, - config.TimelockConfigPDA, newOp3.OperationPDA(), + config.TimelockConfigPDA, msigs[timelock.Proposer_Role].AccessController.PublicKey(), proposerMsig.SignerPDA, ).ValidateAndBuild() require.NoError(t, err) - node, err := IxToMcmTestOpNode(proposerMsig.ConfigPDA, proposerMsig.SignerPDA, scheduleIx, uint64(currentOpCount)) + opNode, err := IxToMcmTestOpNode(proposerMsig.ConfigPDA, proposerMsig.SignerPDA, scheduleIx, uint64(currentOpCount)) require.NoError(t, err) - newOpNodes := []mcmsUtils.McmOpNode{node} + newOpNodes := []mcmsUtils.McmOpNode{opNode} // Create and validate root data validUntil := uint32(0xffffffff) @@ -1399,7 +1424,22 @@ func TestMcmWithTimelock(t *testing.T) { ).ValidateAndBuild() require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{setRootIx}, admin, config.DefaultCommitment) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{setRootIx}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[NewRoot]("NewRoot"), + }, + ) + event := parsedLogs[0].EventData[0].Data.(*NewRoot) + require.Equal(t, rootValidationData.Root, event.Root) + require.Equal(t, validUntil, event.ValidUntil) + require.Equal(t, rootValidationData.Metadata.ChainId, event.MetadataChainID) + require.Equal(t, proposerMsig.ConfigPDA, event.MetadataMultisig) + require.Equal(t, rootValidationData.Metadata.PreOpCount, event.MetadataPreOpCount) + require.Equal(t, rootValidationData.Metadata.PostOpCount, event.MetadataPostOpCount) + require.Equal(t, rootValidationData.Metadata.OverridePreviousRoot, event.MetadataOverridePreviousRoot) // Execute mcm operation to schedule the timelock operation proofs, err := newOpNodes[0].Proofs() @@ -1408,22 +1448,50 @@ func TestMcmWithTimelock(t *testing.T) { executeIx := mcm.NewExecuteInstruction( proposerMsig.PaddedName, config.TestChainID, - node.Nonce, - node.Data, + opNode.Nonce, + opNode.Data, proofs, proposerMsig.ConfigPDA, proposerMsig.RootMetadataPDA, proposerMsig.ExpiringRootAndOpCountPDA, - node.To, + opNode.To, proposerMsig.SignerPDA, anyone.PublicKey(), ) - executeIx.AccountMetaSlice = append(executeIx.AccountMetaSlice, node.RemainingAccounts...) + executeIx.AccountMetaSlice = append(executeIx.AccountMetaSlice, opNode.RemainingAccounts...) vIx, err := executeIx.ValidateAndBuild() require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + exeTx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, anyone, config.DefaultCommitment) + require.NotNil(t, exeTx) + + parsedExeLogs := utils.ParseLogMessages(exeTx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[OpExecuted]("OpExecuted"), + utils.EventMappingFor[CallScheduled]("CallScheduled"), + }, + ) + exeEvent := parsedExeLogs[0].EventData[0].Data.(*OpExecuted) + require.Equal(t, opNode.Nonce, exeEvent.Nonce) + require.Equal(t, opNode.To, exeEvent.To) + require.Equal(t, opNode.Data, utils.NormalizeData(exeEvent.Data)) + + // check inner CallScheduled events + opIxData := newOp3.ToInstructionData() + + require.Equal(t, len(opIxData), len(parsedExeLogs[0].InnerCalls[0].EventData), "Number of actual CallScheduled events does not match expected for operation") + + for j, ix := range opIxData { + timelockEvent := parsedExeLogs[0].InnerCalls[0].EventData[j].Data.(*CallScheduled) + require.Equal(t, newOp3.OperationID(), timelockEvent.ID, "ID does not match") + require.Equal(t, uint64(j), timelockEvent.Index, "Index does not match") + require.Equal(t, ix.ProgramId, timelockEvent.Target, "Target does not match") + require.Equal(t, newOp3.Predecessor, timelockEvent.Predecessor, "Predecessor does not match") + require.Equal(t, newOp3.Salt, timelockEvent.Salt, "Salt does not match") + require.Equal(t, newOp3.Delay, timelockEvent.Delay, "Delay does not match") + require.Equal(t, ix.Data, utils.NormalizeData(timelockEvent.Data), "Data does not match") + } }) }) @@ -1438,10 +1506,10 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("op2: cannot be executed before op1", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op1.OperationPDA(), // provide op1 PDA as predecessor + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), ) @@ -1461,10 +1529,10 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("op1: initial mint to treasury", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op1.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op1.OperationPDA(), config.TimelockEmptyOpID, + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), ) @@ -1473,13 +1541,25 @@ func TestMcmWithTimelock(t *testing.T) { vIx, err := ix.ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000), ) - require.NotNil(t, result) + require.NotNil(t, tx) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + for i, ix := range op1.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.Equal(t, op1.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ix.ProgramId, event.Target) + require.Equal(t, ix.Data, utils.NormalizeData(event.Data)) + } // Verify operation status var opAccount timelock.Operation @@ -1523,10 +1603,10 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("op2: should provide the correct predecessor pda address", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op1.OperationID(), // provide op1 ID as predecessor + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), ) @@ -1546,10 +1626,10 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("op2: team ata creation", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op1.OperationPDA(), // provide op1 PDA as predecessor + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), ) @@ -1558,13 +1638,25 @@ func TestMcmWithTimelock(t *testing.T) { vIx, err := ix.ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, admin, config.DefaultCommitment, utils.AddComputeUnitLimit(1_400_000), ) - require.NotNil(t, result) + require.NotNil(t, tx) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + for i, ix := range op2.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.Equal(t, op2.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ix.ProgramId, event.Target) + require.Equal(t, ix.Data, utils.NormalizeData(event.Data)) + } // verify operation status var opAccount timelock.Operation @@ -1576,14 +1668,15 @@ func TestMcmWithTimelock(t *testing.T) { t.Run("op3: team token distribution", func(t *testing.T) { // Wait for delay and execute the timelock operation - time.Sleep(time.Duration(newOp3.Delay) * time.Second) + werr := WaitForOperationToBeReady(ctx, solanaGoClient, newOp3.OperationPDA(), config.DefaultCommitment) + require.NoError(t, werr) executeTimelockIx := timelock.NewExecuteBatchInstruction( newOp3.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, newOp3.OperationPDA(), op2.OperationPDA(), + config.TimelockConfigPDA, + config.TimelockSignerPDA, msigs[timelock.Executor_Role].AccessController.PublicKey(), admin.PublicKey(), ) @@ -1592,8 +1685,21 @@ func TestMcmWithTimelock(t *testing.T) { vTimelockIx, err := executeTimelockIx.ValidateAndBuild() require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vTimelockIx}, admin, config.DefaultCommitment) - + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vTimelockIx}, admin, config.DefaultCommitment) + require.NotNil(t, tx) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + for i, ix := range newOp3.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.NotEqual(t, op3.OperationID(), event.ID) + require.Equal(t, newOp3.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ix.ProgramId, event.Target) + require.Equal(t, ix.Data, utils.NormalizeData(event.Data)) + } // Verify final balances _, treasuryBalance, err := utils.TokenBalance(ctx, solanaGoClient, treasuryATA, config.DefaultCommitment) require.NoError(t, err) diff --git a/chains/solana/contracts/tests/mcms/timelock.go b/chains/solana/contracts/tests/mcms/timelock.go index cb9cfb21..764e722b 100644 --- a/chains/solana/contracts/tests/mcms/timelock.go +++ b/chains/solana/contracts/tests/mcms/timelock.go @@ -78,11 +78,7 @@ func TimelockBatchAddAccessIxs(ctx context.Context, roleAcAccount solana.PublicK authority.PublicKey(), ) for _, address := range chunk { - ix.Append(&solana.AccountMeta{ - PublicKey: address, - IsSigner: false, - IsWritable: false, - }) + ix.Append(solana.Meta(address)) } vIx, err := ix.ValidateAndBuild() if err != nil { @@ -93,6 +89,53 @@ func TimelockBatchAddAccessIxs(ctx context.Context, roleAcAccount solana.PublicK return ixs, nil } +// instructions builder for preloading instructions to timelock operation +func TimelockPreloadOperationIxs(ctx context.Context, op TimelockOperation, authority solana.PublicKey, client *rpc.Client) ([]solana.Instruction, error) { + ixs := []solana.Instruction{} + initOpIx, ioErr := timelock.NewInitializeOperationInstruction( + op.OperationID(), + op.Predecessor, + op.Salt, + op.IxsCountU32(), + op.OperationPDA(), + config.TimelockConfigPDA, + authority, + solana.SystemProgramID, + ).ValidateAndBuild() + if ioErr != nil { + return nil, fmt.Errorf("failed to build initialize operation instruction: %w", ioErr) + } + ixs = append(ixs, initOpIx) + + for _, instruction := range op.ToInstructionData() { + appendIxsIx, apErr := timelock.NewAppendInstructionsInstruction( + op.OperationID(), + []timelock.InstructionData{instruction}, // this should be a slice of instruction within 1232 bytes + op.OperationPDA(), + config.TimelockConfigPDA, + authority, + solana.SystemProgramID, // for reallocation + ).ValidateAndBuild() + if apErr != nil { + return nil, fmt.Errorf("failed to build append instructions instruction: %w", apErr) + } + ixs = append(ixs, appendIxsIx) + } + + finOpIx, foErr := timelock.NewFinalizeOperationInstruction( + op.OperationID(), + op.OperationPDA(), + config.TimelockConfigPDA, + authority, + ).ValidateAndBuild() + if foErr != nil { + return nil, fmt.Errorf("failed to build finalize operation instruction: %w", foErr) + } + ixs = append(ixs, finOpIx) + + return ixs, nil +} + // mcm + timelock test helpers type RoleMultisigs struct { Multisigs []mcmsUtils.Multisig @@ -103,8 +146,8 @@ func (r RoleMultisigs) GetAnyMultisig() mcmsUtils.Multisig { if len(r.Multisigs) == 0 { panic("no multisigs to pick from") } - maxN := big.NewInt(int64(len(r.Multisigs))) - n, err := crypto_rand.Int(crypto_rand.Reader, maxN) + maxCount := big.NewInt(int64(len(r.Multisigs))) + n, err := crypto_rand.Int(crypto_rand.Reader, maxCount) if err != nil { panic(err) } @@ -195,3 +238,29 @@ func WaitForOperationToBeReady(ctx context.Context, client *rpc.Client, opPDA so return fmt.Errorf("operation not ready after %d attempts (scheduled for: %v, with buffer: %v)", maxAttempts, scheduledTime.UTC(), scheduledTimeWithBuffer.UTC()) } + +func GetBlockedFunctionSelectors( + ctx context.Context, + client *rpc.Client, + configPubKey solana.PublicKey, + commitment rpc.CommitmentType, +) ([][]byte, error) { + var config timelock.Config + err := utils.GetAccountDataBorshInto(ctx, client, configPubKey, commitment, &config) + if err != nil { + return nil, fmt.Errorf("failed to fetch config account data: %w", err) + } + + blockedCount := config.BlockedSelectors.Len + if blockedCount == 0 { + return nil, nil + } + + // convert to [][]byte for easier comparison + selectors := make([][]byte, blockedCount) + for i := uint64(0); i < blockedCount; i++ { + selectors[i] = config.BlockedSelectors.Xs[i][:] // Convert [8]byte to []byte + } + + return selectors, nil +} diff --git a/chains/solana/contracts/tests/mcms/timelock_bypasser_execute_test.go b/chains/solana/contracts/tests/mcms/timelock_bypasser_execute_test.go index 550ffb1c..328088af 100644 --- a/chains/solana/contracts/tests/mcms/timelock_bypasser_execute_test.go +++ b/chains/solana/contracts/tests/mcms/timelock_bypasser_execute_test.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/accesscontroller" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" mcmsUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/mcms" @@ -136,24 +137,10 @@ func TestTimelockBypasserExecute(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - var ac access_controller.AccessController - acAccountErr := utils.GetAccountDataBorshInto( - ctx, - solanaGoClient, - data.AccessController.PublicKey(), - config.DefaultCommitment, - &ac, - ) - require.NoError(t, acAccountErr) - - require.Equal(t, uint64(len(data.Accounts)), ac.AccessList.Len, - "AccessList length mismatch for %s", data.Role) - for _, account := range data.Accounts { - targetPubKey := account.PublicKey() - _, found := mcmsUtils.FindInSortedList(ac.AccessList.Xs[:ac.AccessList.Len], targetPubKey) - require.True(t, found, "Account %s not found in %s AccessList", - targetPubKey, data.Role) + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, data.AccessController.PublicKey(), account.PublicKey(), config.DefaultCommitment) + require.NoError(t, ferr) + require.True(t, found, "Account %s not found in %s AccessList", account.PublicKey(), data.Role) } } }) @@ -252,44 +239,14 @@ func TestTimelockBypasserExecute(t *testing.T) { id := op.OperationID() operationPDA := op.OperationPDA() - signer := roleMap[timelock.Proposer_Role].RandomPick() - initOpIx, err := timelock.NewInitializeOperationInstruction( - op.OperationID(), - op.Predecessor, - op.Salt, - op.IxsCountU32(), - config.TimelockConfigPDA, - op.OperationPDA(), - signer.PublicKey(), - signer.PublicKey(), // proposer - who calls the schedule_batch - solana.SystemProgramID, - ).ValidateAndBuild() + ixs, err := TimelockPreloadOperationIxs(ctx, op, signer.PublicKey(), solanaGoClient) require.NoError(t, err) - - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initOpIx}, signer, config.DefaultCommitment) - - for _, ixData := range op.ToInstructionData() { - appendIxIx, aErr := timelock.NewAppendInstructionsInstruction( - op.OperationID(), - []timelock.InstructionData{ixData}, - op.OperationPDA(), - signer.PublicKey(), - solana.SystemProgramID, // for reallocation - ).ValidateAndBuild() - require.NoError(t, aErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{appendIxIx}, signer, config.DefaultCommitment) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) } - finIxIx, err := timelock.NewFinalizeOperationInstruction( - op.OperationID(), - op.OperationPDA(), - signer.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finIxIx}, signer, config.DefaultCommitment) - var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA, config.DefaultCommitment, &opAccount) if err != nil { @@ -308,9 +265,9 @@ func TestTimelockBypasserExecute(t *testing.T) { ix := timelock.NewBypasserExecuteBatchInstruction( id, + operationPDA, config.TimelockConfigPDA, config.TimelockSignerPDA, - operationPDA, ac.PublicKey(), signer.PublicKey(), ) @@ -319,8 +276,21 @@ func TestTimelockBypasserExecute(t *testing.T) { vIx, err := ix.ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, signer, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, signer, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[BypasserCallExecuted]("BypasserCallExecuted"), + }, + ) + + for i, ixx := range op.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*BypasserCallExecuted) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA, config.DefaultCommitment, &opAccount) diff --git a/chains/solana/contracts/tests/mcms/timelock_errors.go b/chains/solana/contracts/tests/mcms/timelock_errors.go new file mode 100644 index 00000000..f7e58142 --- /dev/null +++ b/chains/solana/contracts/tests/mcms/timelock_errors.go @@ -0,0 +1,21 @@ +package contracts + +import ( + agbinary "github.com/gagliardetto/binary" +) + +// This Errors should be automatically generated by Anchor-Go but they only support one error per program +type TimelockError agbinary.BorshEnum + +const ( + UnauthorizedTimelockError TimelockError = iota +) + +func (value TimelockError) String() string { + switch value { + case UnauthorizedTimelockError: + return "Unauthorized" + default: + return "" + } +} diff --git a/chains/solana/contracts/tests/mcms/timelock_events.go b/chains/solana/contracts/tests/mcms/timelock_events.go new file mode 100644 index 00000000..52ae930e --- /dev/null +++ b/chains/solana/contracts/tests/mcms/timelock_events.go @@ -0,0 +1,56 @@ +package contracts + +import ( + "github.com/gagliardetto/solana-go" +) + +// Events - temporary event struct to decode +// anchor-go does not support events +// https://github.com/fragmetric-labs/solana-anchor-go does but requires upgrade to anchor >= v0.30.0 + +// CallScheduled represents an event emitted when a call is scheduled +type CallScheduled struct { + ID [32]byte // id + Index uint64 // index + Target solana.PublicKey // target + Predecessor [32]byte // predecessor + Salt [32]byte // salt + Delay uint64 // delay + Data []byte // data: Vec +} + +// CallExecuted represents an event emitted when a call is performed +type CallExecuted struct { + ID [32]byte // id + Index uint64 // index + Target solana.PublicKey // target + Data []byte // data: Vec +} + +// BypasserCallExecuted represents an event emitted when a call is performed via bypasser +type BypasserCallExecuted struct { + Index uint64 // index + Target solana.PublicKey // target + Data []byte // data: Vec +} + +// Cancelled represents an event emitted when an operation is cancelled +type Cancelled struct { + ID [32]byte // id +} + +// MinDelayChange represents an event emitted when the minimum delay is modified +type MinDelayChange struct { + OldDuration uint64 // old_duration + NewDuration uint64 // new_duration +} + +// FunctionSelectorBlocked represents an event emitted when a function selector is blocked +type FunctionSelectorBlocked struct { + Selector [8]byte // selector +} + +// FunctionSelectorUnblocked represents an event emitted when a function selector is unblocked +type FunctionSelectorUnblocked struct { + Selector [8]byte // selector +} diff --git a/chains/solana/contracts/tests/mcms/timelock_operations.go b/chains/solana/contracts/tests/mcms/timelock_operations.go index 6a8e6111..ed34f4c0 100644 --- a/chains/solana/contracts/tests/mcms/timelock_operations.go +++ b/chains/solana/contracts/tests/mcms/timelock_operations.go @@ -8,7 +8,6 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" - "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/eth" mcmsUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/mcms" ) @@ -58,8 +57,8 @@ func (op *TimelockOperation) IxsCountU32() uint32 { // convert operation to timelock instruction data slice func (op *TimelockOperation) ToInstructionData() []timelock.InstructionData { ixs := make([]timelock.InstructionData, len(op.instructions)) - for i, instr := range op.instructions { - ixData, err := convertToInstructionData(instr.Ix) + for i, ix := range op.instructions { + ixData, err := convertToInstructionData(ix.Ix) if err != nil { panic(err) } @@ -151,7 +150,7 @@ func hashOperation(instructions []timelock.InstructionData, predecessor [32]byte encodedData.Write(predecessor[:]) encodedData.Write(salt[:]) - result := eth.Keccak256(encodedData.Bytes()) + result := mcmsUtils.Keccak256(encodedData.Bytes()) var hash [32]byte copy(hash[:], result) diff --git a/chains/solana/contracts/tests/mcms/timelock_rbac_test.go b/chains/solana/contracts/tests/mcms/timelock_rbac_test.go index 146a1899..7cd7a743 100644 --- a/chains/solana/contracts/tests/mcms/timelock_rbac_test.go +++ b/chains/solana/contracts/tests/mcms/timelock_rbac_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/accesscontroller" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" mcmsUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/mcms" @@ -49,8 +50,8 @@ func TestTimelockRBAC(t *testing.T) { t.Run("setup:init access controllers", func(t *testing.T) { for _, data := range roleMap { - initAccIxs, err := InitAccessControllersIxs(ctx, data.AccessController.PublicKey(), admin, solanaGoClient) - require.NoError(t, err) + initAccIxs, ierr := InitAccessControllersIxs(ctx, data.AccessController.PublicKey(), admin, solanaGoClient) + require.NoError(t, ierr) utils.SendAndConfirm(ctx, t, solanaGoClient, initAccIxs, admin, config.DefaultCommitment, utils.AddSigners(data.AccessController)) @@ -76,7 +77,7 @@ func TestTimelockRBAC(t *testing.T) { } require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - initTimelockIx, err := timelock.NewInitializeInstruction( + initTimelockIx, ierr := timelock.NewInitializeInstruction( config.MinDelay, config.TimelockConfigPDA, anotherAdmin.PublicKey(), @@ -89,9 +90,9 @@ func TestTimelockRBAC(t *testing.T) { roleMap[timelock.Canceller_Role].AccessController.PublicKey(), roleMap[timelock.Bypasser_Role].AccessController.PublicKey(), ).ValidateAndBuild() - require.NoError(t, err) + require.NoError(t, ierr) - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{initTimelockIx}, anotherAdmin, config.DefaultCommitment, []string{"Error Code: " + timelock.Unauthorized_TimelockError.String()}) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{initTimelockIx}, anotherAdmin, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) require.NotNil(t, result) }) @@ -109,7 +110,7 @@ func TestTimelockRBAC(t *testing.T) { } require.NoError(t, bin.UnmarshalBorsh(&programData, data.Bytes())) - initTimelockIx, err := timelock.NewInitializeInstruction( + initTimelockIx, ierr := timelock.NewInitializeInstruction( config.MinDelay, config.TimelockConfigPDA, admin.PublicKey(), @@ -122,7 +123,7 @@ func TestTimelockRBAC(t *testing.T) { roleMap[timelock.Canceller_Role].AccessController.PublicKey(), roleMap[timelock.Bypasser_Role].AccessController.PublicKey(), ).ValidateAndBuild() - require.NoError(t, err) + require.NoError(t, ierr) utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initTimelockIx}, admin, config.DefaultCommitment) @@ -141,89 +142,96 @@ func TestTimelockRBAC(t *testing.T) { }) t.Run("timelock:ownership", func(t *testing.T) { - // Fail to transfer ownership when not owner - instruction, err := timelock.NewTransferOwnershipInstruction( - anotherAdmin.PublicKey(), - config.TimelockConfigPDA, - user.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: " + timelock.Unauthorized_TimelockError.String()}) - require.NotNil(t, result) + t.Run("fail to transfer ownership when not owner", func(t *testing.T) { + instruction, ierr := timelock.NewTransferOwnershipInstruction( + anotherAdmin.PublicKey(), + config.TimelockConfigPDA, + user.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) + require.NotNil(t, result) + }) - // successfully transfer ownership - instruction, err = timelock.NewTransferOwnershipInstruction( - anotherAdmin.PublicKey(), - config.TimelockConfigPDA, - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) - require.NotNil(t, result) + t.Run("Current owner cannot propose self", func(t *testing.T) { + instruction, ierr := timelock.NewTransferOwnershipInstruction( + admin.PublicKey(), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment, []string{"Error Code: " + timelock.InvalidInput_TimelockError.String()}) + require.NotNil(t, result) + }) - // Fail to accept ownership when not proposed_owner - instruction, err = timelock.NewAcceptOwnershipInstruction( - config.TimelockConfigPDA, - user.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: " + timelock.Unauthorized_TimelockError.String()}) - require.NotNil(t, result) + t.Run("successfully transfer ownership", func(t *testing.T) { + instruction, ierr := timelock.NewTransferOwnershipInstruction( + anotherAdmin.PublicKey(), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) + require.NotNil(t, result) + }) - // Successfully accept ownership - // anotherAdmin becomes owner for remaining tests - instruction, err = timelock.NewAcceptOwnershipInstruction( - config.TimelockConfigPDA, - anotherAdmin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment) - require.NotNil(t, result) + t.Run("Fail to accept ownership when not proposed_owner", func(t *testing.T) { + instruction, ierr := timelock.NewAcceptOwnershipInstruction( + config.TimelockConfigPDA, + user.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) + require.NotNil(t, result) + }) - // Current owner cannot propose self - instruction, err = timelock.NewTransferOwnershipInstruction( - anotherAdmin.PublicKey(), - config.TimelockConfigPDA, - anotherAdmin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment, []string{"Error Code: " + timelock.InvalidInput_TimelockError.String()}) - require.NotNil(t, result) + t.Run("anotherAdmin becomes owner", func(t *testing.T) { + instruction, ierr := timelock.NewAcceptOwnershipInstruction( + config.TimelockConfigPDA, + anotherAdmin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment) + require.NotNil(t, result) - // Validate proposed set to 0-address after accepting ownership - var configAccount timelock.Config - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &configAccount) - if err != nil { - require.NoError(t, err, "failed to get account info") - } - require.Equal(t, anotherAdmin.PublicKey(), configAccount.Owner) - require.Equal(t, solana.PublicKey{}, configAccount.ProposedOwner) + // Validate proposed set to 0-address after accepting ownership + var configAccount timelock.Config + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &configAccount) + if err != nil { + require.NoError(t, err, "failed to get account info") + } + require.Equal(t, anotherAdmin.PublicKey(), configAccount.Owner) + require.Equal(t, solana.PublicKey{}, configAccount.ProposedOwner) + }) // get it back - instruction, err = timelock.NewTransferOwnershipInstruction( - admin.PublicKey(), - config.TimelockConfigPDA, - anotherAdmin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment) - require.NotNil(t, result) + t.Run("retrieve back ownership to admin", func(t *testing.T) { + tix, ierr := timelock.NewTransferOwnershipInstruction( + admin.PublicKey(), + config.TimelockConfigPDA, + anotherAdmin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, ierr) + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{tix}, anotherAdmin, config.DefaultCommitment) + require.NotNil(t, result) - instruction, err = timelock.NewAcceptOwnershipInstruction( - config.TimelockConfigPDA, - admin.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, admin, config.DefaultCommitment) - require.NotNil(t, result) + aix, aerr := timelock.NewAcceptOwnershipInstruction( + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, aerr) + result = utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{aix}, admin, config.DefaultCommitment) + require.NotNil(t, result) - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &configAccount) - if err != nil { - require.NoError(t, err, "failed to get account info") - } + var configAccount timelock.Config + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &configAccount) + if err != nil { + require.NoError(t, err, "failed to get account info") + } - require.Equal(t, admin.PublicKey(), configAccount.Owner) - require.Equal(t, solana.PublicKey{}, configAccount.ProposedOwner) + require.Equal(t, admin.PublicKey(), configAccount.Owner) + require.Equal(t, solana.PublicKey{}, configAccount.ProposedOwner) + }) }) t.Run("setup:register access list & verify", func(t *testing.T) { @@ -232,38 +240,24 @@ func TestTimelockRBAC(t *testing.T) { for _, account := range data.Accounts { addresses = append(addresses, account.PublicKey()) } - batchAddAccessIxs, err := TimelockBatchAddAccessIxs(ctx, data.AccessController.PublicKey(), role, addresses, admin, config.BatchAddAccessChunkSize, solanaGoClient) - require.NoError(t, err) + batchAddAccessIxs, baerr := TimelockBatchAddAccessIxs(ctx, data.AccessController.PublicKey(), role, addresses, admin, config.BatchAddAccessChunkSize, solanaGoClient) + require.NoError(t, baerr) for _, ix := range batchAddAccessIxs { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - var ac access_controller.AccessController - err = utils.GetAccountDataBorshInto( - ctx, - solanaGoClient, - data.AccessController.PublicKey(), - config.DefaultCommitment, - &ac, - ) - require.NoError(t, err) - - require.Equal(t, uint64(len(data.Accounts)), ac.AccessList.Len, - "AccessList length mismatch for %s", data.Role) - for _, account := range data.Accounts { - targetPubKey := account.PublicKey() - _, found := mcmsUtils.FindInSortedList(ac.AccessList.Xs[:ac.AccessList.Len], targetPubKey) - require.True(t, found, "Account %s not found in %s AccessList", - targetPubKey, data.Role) + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, data.AccessController.PublicKey(), account.PublicKey(), config.DefaultCommitment) + require.NoError(t, ferr) + require.True(t, found, "Account %s not found in %s AccessList", account.PublicKey(), data.Role) } } }) t.Run("rbac: schedule and cancel a timelock operation", func(t *testing.T) { - salt, err := mcmsUtils.SimpleSalt() - require.NoError(t, err) + salt, serr := mcmsUtils.SimpleSalt() + require.NoError(t, serr) nonExecutableOp := TimelockOperation{ Predecessor: config.TimelockEmptyOpID, Salt: salt, @@ -273,136 +267,197 @@ func TestTimelockRBAC(t *testing.T) { ix := system.NewTransferInstruction(1*solana.LAMPORTS_PER_SOL, admin.PublicKey(), config.TimelockSignerPDA).Build() nonExecutableOp.AddInstruction(ix, []solana.PublicKey{}) - id := nonExecutableOp.OperationID() - operationPDA := nonExecutableOp.OperationPDA() - - t.Run("rbac: Should able to schedule tx with proposer role", func(t *testing.T) { - signer := roleMap[timelock.Proposer_Role].RandomPick() + t.Run("rbac: when try to schedule from non proposer role, it fails", func(t *testing.T) { + nonProposer := roleMap[timelock.Executor_Role].RandomPick() ac := roleMap[timelock.Proposer_Role].AccessController - ixs := make([]solana.Instruction, 0) - initOpIx, err := timelock.NewInitializeOperationInstruction( + ixs, prierr := TimelockPreloadOperationIxs(ctx, nonExecutableOp, nonProposer.PublicKey(), solanaGoClient) + require.NoError(t, prierr) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, nonProposer, config.DefaultCommitment) + } + + ix, scerr := timelock.NewScheduleBatchInstruction( nonExecutableOp.OperationID(), - nonExecutableOp.Predecessor, - nonExecutableOp.Salt, - uint32(len(nonExecutableOp.instructions)), + nonExecutableOp.Delay, + nonExecutableOp.OperationPDA(), config.TimelockConfigPDA, - operationPDA, - signer.PublicKey(), - signer.PublicKey(), // proposer - direct schedule batch here - solana.SystemProgramID, + ac.PublicKey(), + nonProposer.PublicKey(), ).ValidateAndBuild() - require.NoError(t, err) - ixs = append(ixs, initOpIx) + require.NoError(t, scerr) + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, nonProposer, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) + }) - appendIxIx, err := timelock.NewAppendInstructionsInstruction( - nonExecutableOp.OperationID(), - nonExecutableOp.ToInstructionData(), - operationPDA, - signer.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, err) - ixs = append(ixs, appendIxIx) + t.Run("rbac: Should able to schedule tx with proposer role", func(t *testing.T) { + proposer := roleMap[timelock.Proposer_Role].RandomPick() + ac := roleMap[timelock.Proposer_Role].AccessController - finIxIx, err := timelock.NewFinalizeOperationInstruction( - nonExecutableOp.OperationID(), - operationPDA, - signer.PublicKey(), + t.Run("rbac: when proposer's access is removed, it should not be able to schedule", func(t *testing.T) { + raIx, raerr := access_controller.NewRemoveAccessInstruction( + ac.PublicKey(), + admin.PublicKey(), + proposer.PublicKey(), // remove access of proposer + ).ValidateAndBuild() + require.NoError(t, raerr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{raIx}, admin, config.DefaultCommitment) + + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, ac.PublicKey(), proposer.PublicKey(), config.DefaultCommitment) + require.NoError(t, ferr) + require.False(t, found, "Account %s should not be in the AccessList", proposer.PublicKey()) + + ix, scerr := timelock.NewScheduleBatchInstruction( + nonExecutableOp.OperationID(), + nonExecutableOp.Delay, + nonExecutableOp.OperationPDA(), + config.TimelockConfigPDA, + ac.PublicKey(), + proposer.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, scerr) + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, proposer, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) + }) + + raIx, raerr := access_controller.NewAddAccessInstruction( + ac.PublicKey(), + admin.PublicKey(), + proposer.PublicKey(), // add access of proposer again ).ValidateAndBuild() - require.NoError(t, err) - ixs = append(ixs, finIxIx) + require.NoError(t, raerr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{raIx}, admin, config.DefaultCommitment) + + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, ac.PublicKey(), proposer.PublicKey(), config.DefaultCommitment) + require.NoError(t, ferr) + require.True(t, found, "Account %s should be in the AccessList", proposer.PublicKey()) + + salt, serr := mcmsUtils.SimpleSalt() + require.NoError(t, serr) + nonExecutableOp2 := TimelockOperation{ + Predecessor: config.TimelockEmptyOpID, + Salt: salt, + Delay: uint64(1), + } + ix := system.NewTransferInstruction(1*solana.LAMPORTS_PER_SOL, admin.PublicKey(), config.TimelockSignerPDA).Build() + nonExecutableOp2.AddInstruction(ix, []solana.PublicKey{}) - utils.SendAndConfirm(ctx, t, solanaGoClient, ixs, signer, config.DefaultCommitment) + ixs, prerr := TimelockPreloadOperationIxs(ctx, nonExecutableOp2, proposer.PublicKey(), solanaGoClient) + require.NoError(t, prerr) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, proposer, config.DefaultCommitment) + } - ix, err := timelock.NewScheduleBatchInstruction( - nonExecutableOp.OperationID(), - nonExecutableOp.Delay, + sbix, sberr := timelock.NewScheduleBatchInstruction( + nonExecutableOp2.OperationID(), + nonExecutableOp2.Delay, + nonExecutableOp2.OperationPDA(), // formerly uploaded config.TimelockConfigPDA, - operationPDA, ac.PublicKey(), - signer.PublicKey(), + proposer.PublicKey(), ).ValidateAndBuild() + require.NoError(t, sberr) - require.NoError(t, err) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{sbix}, proposer, config.DefaultCommitment) + require.NotNil(t, tx) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) - require.NotNil(t, result) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallScheduled]("CallScheduled"), + }, + ) + + for i, ixx := range nonExecutableOp2.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallScheduled) + require.Equal(t, nonExecutableOp2.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, nonExecutableOp2.Predecessor, event.Predecessor) + require.Equal(t, nonExecutableOp2.Salt, event.Salt) + require.Equal(t, nonExecutableOp2.Delay, event.Delay) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } var opAccount timelock.Operation - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA, config.DefaultCommitment, &opAccount) + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, nonExecutableOp2.OperationPDA(), config.DefaultCommitment, &opAccount) if err != nil { require.NoError(t, err, "failed to get account info") } require.Equal(t, - result.BlockTime.Time().Add(time.Duration(nonExecutableOp.Delay)*time.Second).Unix(), + tx.BlockTime.Time().Add(time.Duration(nonExecutableOp2.Delay)*time.Second).Unix(), int64(opAccount.Timestamp), "Scheduled Times don't match", ) require.Equal(t, - id, + nonExecutableOp2.OperationID(), opAccount.Id, "Ids don't match", ) - }) - - t.Run("rbac: cancel scheduled tx", func(t *testing.T) { - t.Run("fail: should feed the right role access controller", func(t *testing.T) { - signer := roleMap[timelock.Canceller_Role].RandomPick() - ac := roleMap[timelock.Proposer_Role].AccessController - - ix, err := timelock.NewCancelInstruction( - id, - config.TimelockConfigPDA, - operationPDA, - ac.PublicKey(), - signer.PublicKey(), - ).ValidateAndBuild() - - require.NoError(t, err) - - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + "InvalidAccessController."}) - require.NotNil(t, result) - }) - - t.Run("fail: unauthorized on cancel attempt from non-canceller(proposer)", func(t *testing.T) { - signer := roleMap[timelock.Proposer_Role].RandomPick() - ac := roleMap[timelock.Canceller_Role].AccessController - - ix, err := timelock.NewCancelInstruction( - id, - config.TimelockConfigPDA, - operationPDA, - ac.PublicKey(), - signer.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, err) - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + timelock.Unauthorized_TimelockError.String()}) - require.NotNil(t, result) - }) - - t.Run("success: Should able to cancel scheduled tx: PDA closed", func(t *testing.T) { - signer := roleMap[timelock.Canceller_Role].RandomPick() - ac := roleMap[timelock.Canceller_Role].AccessController - - ix, err := timelock.NewCancelInstruction( - id, - - config.TimelockConfigPDA, - operationPDA, - - ac.PublicKey(), - signer.PublicKey(), - ).ValidateAndBuild() - - require.NoError(t, err) - - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) - require.NotNil(t, result) - - utils.AssertClosedAccount(ctx, t, solanaGoClient, operationPDA, config.DefaultCommitment) + t.Run("rbac: cancel scheduled tx", func(t *testing.T) { + t.Run("fail: should feed the right role access controller", func(t *testing.T) { + signer := roleMap[timelock.Canceller_Role].RandomPick() + ac := roleMap[timelock.Proposer_Role].AccessController + + ix, cerr := timelock.NewCancelInstruction( + nonExecutableOp2.OperationID(), + nonExecutableOp2.OperationPDA(), + config.TimelockConfigPDA, + ac.PublicKey(), + signer.PublicKey(), + ).ValidateAndBuild() + + require.NoError(t, cerr) + + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + "InvalidAccessController."}) + require.NotNil(t, result) + }) + + t.Run("fail: unauthorized on cancel attempt from non-canceller(proposer)", func(t *testing.T) { + signer := roleMap[timelock.Proposer_Role].RandomPick() + ac := roleMap[timelock.Canceller_Role].AccessController + + ix, cerr := timelock.NewCancelInstruction( + nonExecutableOp2.OperationID(), + nonExecutableOp2.OperationPDA(), + config.TimelockConfigPDA, + ac.PublicKey(), + signer.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, cerr) + + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) + require.NotNil(t, result) + }) + + t.Run("success: Should able to cancel scheduled tx: PDA closed", func(t *testing.T) { + signer := roleMap[timelock.Canceller_Role].RandomPick() + ac := roleMap[timelock.Canceller_Role].AccessController + + ix, cerr := timelock.NewCancelInstruction( + nonExecutableOp2.OperationID(), + nonExecutableOp2.OperationPDA(), + config.TimelockConfigPDA, + ac.PublicKey(), + signer.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, cerr) + + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[Cancelled]("Cancelled"), + }, + ) + + for i := range nonExecutableOp2.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*Cancelled) + require.Equal(t, nonExecutableOp2.OperationID(), event.ID) + } + + utils.AssertClosedAccount(ctx, t, solanaGoClient, nonExecutableOp2.OperationPDA(), config.DefaultCommitment) + }) }) }) }) @@ -413,20 +468,26 @@ func TestTimelockRBAC(t *testing.T) { t.Run("fail: only admin can call functions with only_admin macro", func(t *testing.T) { signer := roleMap[timelock.Proposer_Role].RandomPick() - ix, err := timelock.NewUpdateDelayInstruction( + ix, ierr := timelock.NewUpdateDelayInstruction( newMinDelay, config.TimelockConfigPDA, signer.PublicKey(), ).ValidateAndBuild() - require.NoError(t, err) + require.NoError(t, ierr) - result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + timelock.Unauthorized_TimelockError.String()}) + result := utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment, []string{"Error Code: " + UnauthorizedTimelockError.String()}) require.NotNil(t, result) }) t.Run("success: only admin can call functions with only_admin macro", func(t *testing.T) { signer := admin + var oldConfigAccount timelock.Config + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &oldConfigAccount) + if err != nil { + require.NoError(t, err, "failed to get account info") + } + ix, err := timelock.NewUpdateDelayInstruction( newMinDelay, config.TimelockConfigPDA, @@ -434,15 +495,25 @@ func TestTimelockRBAC(t *testing.T) { ).ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, signer, config.DefaultCommitment) + require.NotNil(t, tx) - var configAccount timelock.Config - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &configAccount) + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[MinDelayChange]("MinDelayChange"), + }, + ) + + event := parsedLogs[0].EventData[0].Data.(*MinDelayChange) + require.Equal(t, oldConfigAccount.MinDelay, event.OldDuration) + require.Equal(t, newMinDelay, event.NewDuration) + + var newConfigAccount timelock.Config + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment, &newConfigAccount) if err != nil { require.NoError(t, err, "failed to get account info") } - require.Equal(t, newMinDelay, configAccount.MinDelay, "MinDelay is not updated") + require.Equal(t, newMinDelay, newConfigAccount.MinDelay, "MinDelay is not updated") }) }) } diff --git a/chains/solana/contracts/tests/mcms/timelock_schedule_execute_test.go b/chains/solana/contracts/tests/mcms/timelock_schedule_execute_test.go index f8b04923..0a2f47aa 100644 --- a/chains/solana/contracts/tests/mcms/timelock_schedule_execute_test.go +++ b/chains/solana/contracts/tests/mcms/timelock_schedule_execute_test.go @@ -13,10 +13,12 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/accesscontroller" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" mcmsUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/mcms" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/access_controller" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/external_program_cpi_stub" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock" ) @@ -135,24 +137,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment) } - var ac access_controller.AccessController - acAccErr := utils.GetAccountDataBorshInto( - ctx, - solanaGoClient, - data.AccessController.PublicKey(), - config.DefaultCommitment, - &ac, - ) - require.NoError(t, acAccErr) - - require.Equal(t, uint64(len(data.Accounts)), ac.AccessList.Len, - "AccessList length mismatch for %s", data.Role) - for _, account := range data.Accounts { - targetPubKey := account.PublicKey() - _, found := mcmsUtils.FindInSortedList(ac.AccessList.Xs[:ac.AccessList.Len], targetPubKey) - require.True(t, found, "Account %s not found in %s AccessList", - targetPubKey, data.Role) + found, ferr := accesscontroller.HasAccess(ctx, solanaGoClient, data.AccessController.PublicKey(), account.PublicKey(), config.DefaultCommitment) + require.NoError(t, ferr) + require.True(t, found, "Account %s not found in %s AccessList", account.PublicKey(), data.Role) } } }) @@ -276,7 +264,7 @@ func TestTimelockScheduleAndExecute(t *testing.T) { op3 := TimelockOperation{ Predecessor: op1.OperationID(), Salt: salt3, - Delay: 60, + Delay: 300, // enough delay to assert OperationNotReady error } anotherTransferIx, atErr := utils.TokenTransferChecked( @@ -301,76 +289,39 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("success: schedule all operations", func(t *testing.T) { for _, op := range []TimelockOperation{op1, op2, op3} { - id := op.OperationID() - operationPDA1 := op.OperationPDA() - - initOpIx, iErr := timelock.NewInitializeOperationInstruction( - op.OperationID(), - op.Predecessor, - op.Salt, - op.IxsCountU32(), - config.TimelockConfigPDA, - op.OperationPDA(), - proposer.PublicKey(), - proposer.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, iErr) - - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initOpIx}, proposer, config.DefaultCommitment) + invalidIxs, ierr := TimelockPreloadOperationIxs(ctx, op, proposer.PublicKey(), solanaGoClient) + require.NoError(t, ierr) + for _, ix := range invalidIxs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, proposer, config.DefaultCommitment) + } - for _, ixData := range op.ToInstructionData() { - appendIxsIx, aErr := timelock.NewAppendInstructionsInstruction( + t.Run("clear operation", func(t *testing.T) { + // clear instructions so that we can reinitialize the operation + clearIx, ciErr := timelock.NewClearOperationInstruction( op.OperationID(), - []timelock.InstructionData{ixData}, op.OperationPDA(), + config.TimelockConfigPDA, proposer.PublicKey(), - solana.SystemProgramID, // for reallocation ).ValidateAndBuild() - require.NoError(t, aErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{appendIxsIx}, proposer, config.DefaultCommitment) - } - - t.Run("clear & reappend op instructions", func(t *testing.T) { - // clear instructions so that we can reinitialize the operation - clearIx, ciErr := timelock.NewClearOperationInstruction(op.OperationID(), op.OperationPDA(), proposer.PublicKey()).ValidateAndBuild() require.NoError(t, ciErr) // send clear and check if it's closed utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{clearIx}, proposer, config.DefaultCommitment) utils.AssertClosedAccount(ctx, t, solanaGoClient, op.OperationPDA(), config.DefaultCommitment) - - // reinitialize operation - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{initOpIx}, proposer, config.DefaultCommitment) - - // reappend instructions - for _, ixData := range op.ToInstructionData() { - appendIxsIx, aErr := timelock.NewAppendInstructionsInstruction( - op.OperationID(), - []timelock.InstructionData{ixData}, - op.OperationPDA(), - proposer.PublicKey(), - solana.SystemProgramID, // for reallocation - ).ValidateAndBuild() - require.NoError(t, aErr) - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{appendIxsIx}, proposer, config.DefaultCommitment) - } }) - finIxIx, fErr := timelock.NewFinalizeOperationInstruction( - op.OperationID(), - op.OperationPDA(), - proposer.PublicKey(), - ).ValidateAndBuild() - require.NoError(t, fErr) - - utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{finIxIx}, proposer, config.DefaultCommitment) + // re-preload instructions + ixs, err := TimelockPreloadOperationIxs(ctx, op, proposer.PublicKey(), solanaGoClient) + require.NoError(t, err) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, proposer, config.DefaultCommitment) + } ix, ixVErr := timelock.NewScheduleBatchInstruction( - id, + op.OperationID(), op.Delay, + op.OperationPDA(), config.TimelockConfigPDA, - operationPDA1, proposerAccessController, proposer.PublicKey(), ).ValidateAndBuild() @@ -380,10 +331,8 @@ func TestTimelockScheduleAndExecute(t *testing.T) { require.NotNil(t, result) var opAccount timelock.Operation - err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA1, config.DefaultCommitment, &opAccount) - if err != nil { - require.NoError(t, err, "failed to get account info") - } + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, op.OperationPDA(), config.DefaultCommitment, &opAccount) + require.NoError(t, err, "failed to get account info") require.Equal(t, result.BlockTime.Time().Add(time.Duration(op.Delay)*time.Second).Unix(), @@ -392,7 +341,7 @@ func TestTimelockScheduleAndExecute(t *testing.T) { ) require.Equal(t, - id, + op.OperationID(), opAccount.Id, "Ids don't match", ) @@ -409,8 +358,8 @@ func TestTimelockScheduleAndExecute(t *testing.T) { ix := timelock.NewScheduleBatchInstruction( op1.OperationID(), op1.Delay, - config.TimelockConfigPDA, op1.OperationPDA(), + config.TimelockConfigPDA, proposerAccessController, proposer.PublicKey(), ).Build() @@ -427,10 +376,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("fail: should provide the right dependency pda", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op2.OperationPDA(), // wrong dependency + config.TimelockConfigPDA, + config.TimelockSignerPDA, executorAccessController, executor.PublicKey(), ) @@ -445,10 +394,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("fail: not able to execute op2 before dependency(op1) execution", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op1.OperationPDA(), // not executed yet + config.TimelockConfigPDA, + config.TimelockSignerPDA, executorAccessController, executor.PublicKey(), ) @@ -464,10 +413,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("success: op1 executed", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op1.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op1.OperationPDA(), config.TimelockEmptyOpID, + config.TimelockConfigPDA, + config.TimelockSignerPDA, executorAccessController, executor.PublicKey(), ) @@ -477,8 +426,22 @@ func TestTimelockScheduleAndExecute(t *testing.T) { vIx, err := ix.ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, executor, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, executor, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + + for i, ixx := range op1.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.Equal(t, op1.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, op1.OperationPDA(), config.DefaultCommitment, &opAccount) @@ -496,10 +459,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("success: op2 executed", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op2.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op2.OperationPDA(), op1.OperationPDA(), + config.TimelockConfigPDA, + config.TimelockSignerPDA, executorAccessController, executor.PublicKey(), ) @@ -509,8 +472,22 @@ func TestTimelockScheduleAndExecute(t *testing.T) { vIx, err := ix.ValidateAndBuild() require.NoError(t, err) - result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, executor, config.DefaultCommitment) - require.NotNil(t, result) + tx := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{vIx}, executor, config.DefaultCommitment) + require.NotNil(t, tx) + + parsedLogs := utils.ParseLogMessages(tx.Meta.LogMessages, + []utils.EventMapping{ + utils.EventMappingFor[CallExecuted]("CallExecuted"), + }, + ) + + for i, ixx := range op2.ToInstructionData() { + event := parsedLogs[0].EventData[i].Data.(*CallExecuted) + require.Equal(t, op2.OperationID(), event.ID) + require.Equal(t, uint64(i), event.Index) + require.Equal(t, ixx.ProgramId, event.Target) + require.Equal(t, ixx.Data, utils.NormalizeData(event.Data)) + } var opAccount timelock.Operation err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, op1.OperationPDA(), config.DefaultCommitment, &opAccount) @@ -540,10 +517,10 @@ func TestTimelockScheduleAndExecute(t *testing.T) { t.Run("failure on execution try: op3 is not ready", func(t *testing.T) { ix := timelock.NewExecuteBatchInstruction( op3.OperationID(), - config.TimelockConfigPDA, - config.TimelockSignerPDA, op3.OperationPDA(), config.TimelockEmptyOpID, + config.TimelockConfigPDA, + config.TimelockSignerPDA, executorAccessController, executor.PublicKey(), ) @@ -557,4 +534,157 @@ func TestTimelockScheduleAndExecute(t *testing.T) { }) }) }) + + t.Run("function blockers", func(t *testing.T) { + proposer := roleMap[timelock.Proposer_Role].RandomPick() + proposerAccessController := roleMap[timelock.Proposer_Role].AccessController.PublicKey() + + salt, err := mcmsUtils.SimpleSalt() + require.NoError(t, err) + + op := TimelockOperation{ + Predecessor: config.TimelockEmptyOpID, + Salt: salt, + Delay: 1, + } + + ix, err := external_program_cpi_stub.NewInitializeInstruction( + config.StubAccountPDA, + admin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, err) + + op.AddInstruction(ix, []solana.PublicKey{solana.TokenProgramID, solana.SPLAssociatedTokenAccountProgramID}) + + t.Run("blocks initialize function", func(t *testing.T) { + bIx, bIxErr := timelock.NewBlockFunctionSelectorInstruction( + [8]uint8(external_program_cpi_stub.Instruction_Initialize.Bytes()), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, bIxErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{bIx}, admin, config.DefaultCommitment) + + blockedSelectors, bserr := GetBlockedFunctionSelectors(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment) + require.NoError(t, bserr) + require.Contains(t, blockedSelectors, external_program_cpi_stub.Instruction_Initialize.Bytes()) + }) + + t.Run("not able to block function that is already blocked", func(t *testing.T) { + bbIx, bbIxErr := timelock.NewBlockFunctionSelectorInstruction( + [8]uint8(external_program_cpi_stub.Instruction_Initialize.Bytes()), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, bbIxErr) + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{bbIx}, admin, config.DefaultCommitment, []string{"Error Code: " + timelock.AlreadyBlocked_TimelockError.String()}) + }) + + id := op.OperationID() + operationPDA := op.OperationPDA() + + ixs, err := TimelockPreloadOperationIxs(ctx, op, proposer.PublicKey(), solanaGoClient) + require.NoError(t, err) + for _, ix := range ixs { + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, proposer, config.DefaultCommitment) + } + + scIx, scIxVErr := timelock.NewScheduleBatchInstruction( + id, + op.Delay, + operationPDA, + config.TimelockConfigPDA, + proposerAccessController, + proposer.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, scIxVErr) + + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{scIx}, proposer, config.DefaultCommitment, []string{"Error Code: " + timelock.BlockedSelector_TimelockError.String()}) + + t.Run("unblocks initialize function", func(t *testing.T) { + bIx, bIxErr := timelock.NewUnblockFunctionSelectorInstruction( + [8]uint8(external_program_cpi_stub.Instruction_Initialize.Bytes()), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, bIxErr) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{bIx}, admin, config.DefaultCommitment) + + blockedSelectors, bserr := GetBlockedFunctionSelectors(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment) + require.NoError(t, bserr) + require.NotContains(t, blockedSelectors, external_program_cpi_stub.Instruction_Initialize.Bytes()) + }) + + t.Run("not able to unblock function that is not blocked", func(t *testing.T) { + bbIx, bbIxErr := timelock.NewUnblockFunctionSelectorInstruction( + [8]uint8(external_program_cpi_stub.Instruction_Initialize.Bytes()), + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, bbIxErr) + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{bbIx}, admin, config.DefaultCommitment, []string{"Error Code: " + timelock.SelectorNotFound_TimelockError.String()}) + }) + + t.Run("when unblocked, able to schedule operation", func(t *testing.T) { + result := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{scIx}, proposer, config.DefaultCommitment) + require.NotNil(t, result) + + var opAccount timelock.Operation + err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, operationPDA, config.DefaultCommitment, &opAccount) + require.NoError(t, err, "failed to get account info") + + require.Equal(t, op.OperationID(), opAccount.Id, "Ids don't match") + require.Equal(t, + result.BlockTime.Time().Add(time.Duration(op.Delay)*time.Second).Unix(), + int64(opAccount.Timestamp), + "Scheduled Times don't match", + ) + }) + + t.Run("can't register more than MAX_SELECTOR", func(t *testing.T) { + // check if it's empty + oldBlockedSelectors, gberr := GetBlockedFunctionSelectors(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment) + require.NoError(t, gberr) + require.Empty(t, oldBlockedSelectors) + + ixs := []solana.Instruction{} + for i := 0; i < config.MaxFunctionSelectorLen; i++ { + ix, nberr := timelock.NewBlockFunctionSelectorInstruction( + [8]uint8{byte(i)}, + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, nberr) + + ixs = append(ixs, ix) + } + + // max selectors at 32, two transactions happen here + chunkSize := 16 + for i := 0; i < len(ixs); i += chunkSize { + end := i + chunkSize + if end > len(ixs) { + end = len(ixs) + } + chunk := ixs[i:end] + utils.SendAndConfirm(ctx, t, solanaGoClient, chunk, admin, config.DefaultCommitment) + } + + // check if it's full + blockedSelectors, bserr := GetBlockedFunctionSelectors(ctx, solanaGoClient, config.TimelockConfigPDA, config.DefaultCommitment) + require.NoError(t, bserr) + require.Equal(t, config.MaxFunctionSelectorLen, len(blockedSelectors)) + + // try one more + ix, nberr := timelock.NewBlockFunctionSelectorInstruction( + [8]uint8{255}, + config.TimelockConfigPDA, + admin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, nberr) + + utils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{ix}, admin, config.DefaultCommitment, []string{"Error Code: " + timelock.MaxCapacityReached_TimelockError.String()}) + }) + }) } diff --git a/chains/solana/contracts/tests/txsizing_test.go b/chains/solana/contracts/tests/txsizing_test.go new file mode 100644 index 00000000..164f74a0 --- /dev/null +++ b/chains/solana/contracts/tests/txsizing_test.go @@ -0,0 +1,330 @@ +package contracts + +import ( + "fmt" + "strings" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" +) + +func mustRandomPubkey() solana.PublicKey { + k, err := solana.NewRandomPrivateKey() + if err != nil { + panic(err) + } + return k.PublicKey() +} + +// NOTE: this test does not execute or validate transaction inputs, it simply builds transactions to calculate the size of each transaction with signers +func TestTransactionSizing(t *testing.T) { + ccip_router.SetProgramID(config.CcipRouterProgram) + + auth, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + // mocked router lookup table for constant accounts + // chain specific configs are not constant but are a small set relative to the number of users + routerTable := map[string]solana.PublicKey{ + "routerConfig": mustRandomPubkey(), + "destChainConfig": mustRandomPubkey(), + "systemProgram": solana.SystemProgramID, + "billingTokenProgram": solana.TokenProgramID, + "billingTokenMint": mustRandomPubkey(), + "billingTokenConfig": mustRandomPubkey(), + "routerBillingTokenATA": mustRandomPubkey(), + "routerBillingSigner": mustRandomPubkey(), + "routerTokenPoolSigner": mustRandomPubkey(), + "sysVarInstruction": solana.SysVarInstructionsPubkey, + "originChainConfig": mustRandomPubkey(), + "arbMessagingSigner": mustRandomPubkey(), + } + + tokenTable := map[string]solana.PublicKey{ + "tokenAdminRegistryPDA": mustRandomPubkey(), + "poolLookupTable": mustRandomPubkey(), + "poolProgram": config.CcipTokenPoolProgram, + "poolConfig": mustRandomPubkey(), + "poolTokenAccount": mustRandomPubkey(), + "poolSigner": mustRandomPubkey(), + "tokenProgram": config.Token2022Program, + "mint": mustRandomPubkey(), + } + + run := func(name string, ix solana.Instruction, tables map[solana.PublicKey]solana.PublicKeySlice, opts ...utils.TxModifier) string { + tx, err := solana.NewTransaction([]solana.Instruction{ix}, solana.Hash{1}, solana.TransactionAddressTables(tables)) + require.NoError(t, err) + + for _, o := range opts { + require.NoError(t, o(tx, nil)) + } + + _, err = tx.Sign(func(_ solana.PublicKey) *solana.PrivateKey { + return &auth + }) + require.NoError(t, err) + + bz, err := tx.MarshalBinary() + require.NoError(t, err) + l := len(bz) + require.LessOrEqual(t, l, 1232) + return fmt.Sprintf("%-55s: %-4d - remaining: %d", name, l, 1232-l) + } + + // ccipSend test messages + instruction --------------------------------- + sendNoTokens := ccip_router.Solana2AnyMessage{ + Receiver: make([]byte, 20), // EVM address + Data: []byte{}, + TokenAmounts: []ccip_router.SolanaTokenAmount{}, // no tokens + FeeToken: [32]byte{}, // solana fee token + ExtraArgs: ccip_router.ExtraArgsInput{}, // default options + TokenIndexes: []byte{}, // no tokens + } + sendSingleMinimalToken := ccip_router.Solana2AnyMessage{ + Receiver: make([]byte, 20), + Data: []byte{}, + TokenAmounts: []ccip_router.SolanaTokenAmount{ccip_router.SolanaTokenAmount{ + Token: [32]byte{}, + Amount: 0, + }}, // one token + FeeToken: [32]byte{}, + ExtraArgs: ccip_router.ExtraArgsInput{}, // default options + TokenIndexes: []byte{0}, // one token + } + ixCcipSend := func(msg ccip_router.Solana2AnyMessage, addAccounts solana.PublicKeySlice) solana.Instruction { + base := ccip_router.NewCcipSendInstruction( + 1, + msg, + routerTable["routerConfig"], + routerTable["destChainConfig"], + mustRandomPubkey(), // user nonce PDA + auth.PublicKey(), // sender/authority + routerTable["systemProgram"], + routerTable["billingTokenProgram"], + routerTable["billingTokenMint"], + routerTable["billingTokenConfig"], + mustRandomPubkey(), // user billing token ATA + routerTable["routerBillingTokenATA"], + routerTable["routerBillingSigner"], + routerTable["routerTokenPoolSigner"], + ) + + for _, v := range addAccounts { + base.AccountMetaSlice = append(base.AccountMetaSlice, solana.Meta(v)) + } + ix, err := base.ValidateAndBuild() + require.NoError(t, err) + return ix + } + + // ccip commit test messages + instruction ------------------------ + commitNoPrices := ccip_router.CommitInput{ + MerkleRoot: ccip_router.MerkleRoot{ + SourceChainSelector: 0, + OnRampAddress: make([]byte, 20), // EVM onramp + MinSeqNr: 0, + MaxSeqNr: 0, + MerkleRoot: [32]uint8{}, + }, + } + commitWithPrices := ccip_router.CommitInput{ + PriceUpdates: ccip_router.PriceUpdates{ + TokenPriceUpdates: make([]ccip_router.TokenPriceUpdate, 1), + GasPriceUpdates: make([]ccip_router.GasPriceUpdate, 1), + }, + MerkleRoot: ccip_router.MerkleRoot{ + SourceChainSelector: 0, + OnRampAddress: make([]byte, 20), + MinSeqNr: 0, + MaxSeqNr: 0, + MerkleRoot: [32]uint8{}, + }, + } + ixCommit := func(input ccip_router.CommitInput, addAccounts solana.PublicKeySlice) solana.Instruction { + base := ccip_router.NewCommitInstruction( + [3][32]byte{}, // report context + input, + make([][65]byte, 6), // f = 5, estimating f+1 signatures + routerTable["routerConfig"], + routerTable["originChainConfig"], + mustRandomPubkey(), // commit report PDA + auth.PublicKey(), + routerTable["systemProgram"], + routerTable["sysVarInstruction"], + ) + + for _, v := range addAccounts { + base.AccountMetaSlice = append(base.AccountMetaSlice, solana.Meta(v)) + } + ix, err := base.ValidateAndBuild() + require.NoError(t, err) + return ix + } + + // ccip execute test messages + instruction ----------------------- + executeEmpty := ccip_router.ExecutionReportSingleChain{ + SourceChainSelector: 0, + Message: ccip_router.Any2SolanaRampMessage{ + Header: ccip_router.RampMessageHeader{ + MessageId: [32]uint8{}, + SourceChainSelector: 0, + DestChainSelector: 0, + SequenceNumber: 0, + Nonce: 0, + }, + Sender: make([]byte, 20), // EVM sender + Data: []byte{}, + Receiver: [32]byte{}, + TokenAmounts: []ccip_router.Any2SolanaTokenTransfer{}, + ExtraArgs: ccip_router.SolanaExtraArgs{ + ComputeUnits: 0, + Accounts: []ccip_router.SolanaAccountMeta{}, + }, + }, + OffchainTokenData: [][]byte{}, + Root: [32]uint8{}, + Proofs: [][32]uint8{}, // single message merkle root (added roots consume 32 bytes) + TokenIndexes: []byte{}, + } + executeSingleToken := ccip_router.ExecutionReportSingleChain{ + SourceChainSelector: 0, + Message: ccip_router.Any2SolanaRampMessage{ + Header: ccip_router.RampMessageHeader{ + MessageId: [32]uint8{}, + SourceChainSelector: 0, + DestChainSelector: 0, + SequenceNumber: 0, + Nonce: 0, + }, + Sender: make([]byte, 20), // EVM sender + Data: []byte{}, + Receiver: [32]byte{}, + TokenAmounts: []ccip_router.Any2SolanaTokenTransfer{{ + SourcePoolAddress: make([]byte, 20), // EVM origin token pool + DestTokenAddress: [32]byte{}, + DestGasAmount: 0, + ExtraData: []byte{}, + Amount: [32]uint8{}, + }}, + ExtraArgs: ccip_router.SolanaExtraArgs{ + ComputeUnits: 0, + Accounts: []ccip_router.SolanaAccountMeta{}, + }, + }, + OffchainTokenData: [][]byte{}, + Root: [32]uint8{}, + Proofs: [][32]uint8{}, // single message merkle root (added roots consume 32 bytes) + TokenIndexes: []byte{0}, + } + + ixExecute := func(report ccip_router.ExecutionReportSingleChain, addAccounts solana.PublicKeySlice) solana.Instruction { + base := ccip_router.NewExecuteInstruction( + report, + [3][32]byte{}, // report context + routerTable["routerConfig"], + routerTable["originChainConfig"], + mustRandomPubkey(), // commit report PDA + routerTable["arbMessagingSigner"], + auth.PublicKey(), + routerTable["systemProgram"], + routerTable["sysVarInstruction"], + routerTable["routerTokenPoolSigner"], + ) + + for _, v := range addAccounts { + base.AccountMetaSlice = append(base.AccountMetaSlice, solana.Meta(v)) + } + ix, err := base.ValidateAndBuild() + require.NoError(t, err) + return ix + } + + // runner --------------------------------------------------------- + params := []struct { + name string + ix solana.Instruction + tables map[solana.PublicKey]solana.PublicKeySlice + }{ + { + "ccipSend:noToken", + ixCcipSend(sendNoTokens, nil), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + }, + }, + { + "ccipSend:singleToken", + ixCcipSend(sendSingleMinimalToken, append([]solana.PublicKey{ + mustRandomPubkey(), // user ATA + mustRandomPubkey(), // token billing config + mustRandomPubkey(), // token pool chain config + }, maps.Values(tokenTable)...)), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + tokenTable["poolLookupTable"]: maps.Values(tokenTable), + }, + }, + { + "commit:noPrices", + ixCommit(commitNoPrices, nil), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + }, + }, + { + "commit:withPrices", + ixCommit(commitWithPrices, solana.PublicKeySlice{ + routerTable["billingTokenConfig"], // token price update + routerTable["destChainConfig"], // gas price update + }), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + }, + }, + { + "execute:noToken", + ixExecute(executeEmpty, nil), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + }, + }, + { + "execute:singleToken", + ixExecute(executeSingleToken, append([]solana.PublicKey{ + mustRandomPubkey(), // user ATA + mustRandomPubkey(), // token billing config + mustRandomPubkey(), // token pool chain config + }, maps.Values(tokenTable)...)), + map[solana.PublicKey]solana.PublicKeySlice{ + mustRandomPubkey(): maps.Values(routerTable), + tokenTable["poolLookupTable"]: maps.Values(tokenTable), + }, + }, + } + + divider := strings.Repeat("-", 78) + outputs := []string{"TX SIZE ANALYSIS", divider} + for _, p := range params { + for _, l := range []string{"", " +lookupTable"} { + var tables map[solana.PublicKey]solana.PublicKeySlice + if strings.Contains(l, "+lookupTable") { + tables = p.tables + } + + outputs = append(outputs, + run(p.name+l, p.ix, tables), + run(p.name+l+" +cuLimit", p.ix, tables, utils.AddComputeUnitLimit(0)), + run(p.name+l+" +cuPrice", p.ix, tables, utils.AddComputeUnitPrice(0)), + run(p.name+l+" +cuPrice +cuLimit", p.ix, tables, utils.AddComputeUnitLimit(0), utils.AddComputeUnitPrice(0)), + divider, + ) + } + } + t.Logf("\n%s\n", strings.Join(outputs, "\n")) +} diff --git a/chains/solana/contracts/tests/utils/anchor.go b/chains/solana/contracts/tests/utils/anchor.go index 30a657a1..b5b78470 100644 --- a/chains/solana/contracts/tests/utils/anchor.go +++ b/chains/solana/contracts/tests/utils/anchor.go @@ -10,8 +10,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" - "strconv" "strings" "testing" "time" @@ -49,6 +47,12 @@ func To28BytesLE(value uint64) [28]byte { return [28]byte(le) } +func To28BytesBE(value uint64) [28]byte { + be := make([]byte, 28) + binary.BigEndian.PutUint64(be[20:], value) + return [28]byte(be) +} + func Map[T, V any](ts []T, fn func(T) V) []V { result := make([]V, len(ts)) for i, t := range ts { @@ -181,138 +185,51 @@ func ParseMultipleEvents[T any](logs []string, event string, shouldPrint bool) ( return results, nil } -type AnchorInstruction struct { - Name string - ProgramID string - Logs []string - ComputeUnits int - InnerCalls []*AnchorInstruction -} +func GetBlockTime(ctx context.Context, client *rpc.Client, commitment rpc.CommitmentType) (*solana.UnixTimeSeconds, error) { + block, err := client.GetBlockHeight(ctx, commitment) + if err != nil { + return nil, fmt.Errorf("failed to get block height: %w", err) + } -// Parses the log messages from an Anchor program and returns a list of AnchorInstructions. -func ParseLogMessages(logMessages []string) []*AnchorInstruction { - var instructions []*AnchorInstruction - var stack []*AnchorInstruction - var currentInstruction *AnchorInstruction - - programInvokeRegex := regexp.MustCompile(`Program (\w+) invoke`) - programSuccessRegex := regexp.MustCompile(`Program (\w+) success`) - computeUnitsRegex := regexp.MustCompile(`Program (\w+) consumed (\d+) of \d+ compute units`) - - for _, line := range logMessages { - line = strings.TrimSpace(line) - - // Program invocation - push to stack - if match := programInvokeRegex.FindStringSubmatch(line); len(match) > 1 { - newInstruction := &AnchorInstruction{ - ProgramID: match[1], - Name: "", - Logs: []string{}, - ComputeUnits: 0, - InnerCalls: []*AnchorInstruction{}, - } + blockTime, err := client.GetBlockTime(ctx, block) + if err != nil { + return nil, fmt.Errorf("failed to get block time: %w", err) + } - if len(stack) == 0 { - instructions = append(instructions, newInstruction) - } else { - stack[len(stack)-1].InnerCalls = append(stack[len(stack)-1].InnerCalls, newInstruction) - } + return blockTime, nil +} - stack = append(stack, newInstruction) - currentInstruction = newInstruction - continue - } +func WaitForTheNextBlock(client *rpc.Client, timeout time.Duration, commitment rpc.CommitmentType) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() - // Program success - pop from stack - if match := programSuccessRegex.FindStringSubmatch(line); len(match) > 1 { - if len(stack) > 0 { - stack = stack[:len(stack)-1] // pop - if len(stack) > 0 { - currentInstruction = stack[len(stack)-1] - } else { - currentInstruction = nil - } - } - continue - } + return WaitForNewBlock(ctx, client, 1, commitment) +} - // Instruction name - if strings.Contains(line, "Instruction:") { - if currentInstruction != nil { - currentInstruction.Name = strings.TrimSpace(strings.Split(line, "Instruction:")[1]) - } - continue - } +func WaitForNewBlock(ctx context.Context, client *rpc.Client, height uint64, commitment rpc.CommitmentType) error { + initialHeight, err := client.GetBlockHeight(ctx, commitment) + if err != nil { + return fmt.Errorf("failed to get initial block height: %w", err) + } - // Program logs - if strings.HasPrefix(line, "Program log:") { - if currentInstruction != nil { - logMessage := strings.TrimSpace(strings.TrimPrefix(line, "Program log:")) - currentInstruction.Logs = append(currentInstruction.Logs, logMessage) - } - continue - } + targetFinalHeight := initialHeight + height - // Compute units - if match := computeUnitsRegex.FindStringSubmatch(line); len(match) > 1 { - programID := match[1] - computeUnits, _ := strconv.Atoi(match[2]) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() - // Find the instruction in the stack that matches this program ID - for i := len(stack) - 1; i >= 0; i-- { - if stack[i].ProgramID == programID { - stack[i].ComputeUnits = computeUnits - break - } + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + currentHeight, err := client.GetBlockHeight(ctx, commitment) + if err != nil { + return fmt.Errorf("failed to get current block height: %w", err) } - } - } - - return instructions -} -// Pretty prints the given Anchor instructions. -// Example usage: -// parsed := utils.ParseLogMessages(result.Meta.LogMessages) -// output := utils.PrintInstructions(parsed) -// t.Logf("Parsed Instructions: %s", output) -func PrintInstructions(instructions []*AnchorInstruction) string { - var output strings.Builder - - var printInstruction func(*AnchorInstruction, int, string) - printInstruction = func(instruction *AnchorInstruction, index int, indent string) { - output.WriteString(fmt.Sprintf("%sInstruction %d: %s\n", indent, index, instruction.Name)) - output.WriteString(fmt.Sprintf("%s Program ID: %s\n", indent, instruction.ProgramID)) - output.WriteString(fmt.Sprintf("%s Compute Units: %d\n", indent, instruction.ComputeUnits)) - output.WriteString(fmt.Sprintf("%s Logs:\n", indent)) - for _, log := range instruction.Logs { - output.WriteString(fmt.Sprintf("%s %s\n", indent, log)) - } - if len(instruction.InnerCalls) > 0 { - output.WriteString(fmt.Sprintf("%s Inner Calls:\n", indent)) - for i, innerCall := range instruction.InnerCalls { - printInstruction(innerCall, i+1, indent+" ") + if currentHeight >= targetFinalHeight { + return nil } } } - - for i, instruction := range instructions { - printInstruction(instruction, i+1, "") - } - - return output.String() -} - -func GetBlockTime(ctx context.Context, client *rpc.Client, commitment rpc.CommitmentType) (*solana.UnixTimeSeconds, error) { - block, err := client.GetBlockHeight(ctx, commitment) - if err != nil { - return nil, fmt.Errorf("failed to get block height: %w", err) - } - - blockTime, err := client.GetBlockTime(ctx, block) - if err != nil { - return nil, fmt.Errorf("failed to get block time: %w", err) - } - - return blockTime, nil } diff --git a/chains/solana/contracts/tests/utils/anchor_test.go b/chains/solana/contracts/tests/utils/anchor_test.go deleted file mode 100644 index 1db2d6d5..00000000 --- a/chains/solana/contracts/tests/utils/anchor_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package utils - -import ( - "reflect" - "testing" -) - -func TestParseLogMessages(t *testing.T) { - tests := []struct { - name string - logs []string - expected []*AnchorInstruction - }{ - { - name: "Test Case 1 - Empty Instruction", - logs: []string{ - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", - "Program log: Instruction: Execute", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", - "Program log: Instruction: Empty", - "Program log: Called `empty` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps }", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13620 of 180083 compute units", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35400 of 200000 compute units", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", - }, - expected: []*AnchorInstruction{ - { - Name: "Execute", - ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", - Logs: []string{}, - ComputeUnits: 35400, - InnerCalls: []*AnchorInstruction{ - { - Name: "Empty", - ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", - Logs: []string{ - "Called `empty` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps }", - }, - ComputeUnits: 13620, - InnerCalls: []*AnchorInstruction{}, - }, - }, - }, - }, - }, - { - name: "Test Case 2 - U8InstructionData", - logs: []string{ - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", - "Program log: Instruction: Execute", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", - "Program log: Instruction: U8InstructionData", - "Program log: Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13648 of 180048 compute units", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35463 of 200000 compute units", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", - }, - expected: []*AnchorInstruction{ - { - Name: "Execute", - ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", - Logs: []string{}, - ComputeUnits: 35463, - InnerCalls: []*AnchorInstruction{ - { - Name: "U8InstructionData", - ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", - Logs: []string{ - "Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", - }, - ComputeUnits: 13648, - InnerCalls: []*AnchorInstruction{}, - }, - }, - }, - }, - }, - { - name: "Test Case 3 - StructInstructionData", - logs: []string{ - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", - "Program log: Instruction: Execute", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", - "Program log: Instruction: StructInstructionData", - "Program log: Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13920 of 180631 compute units", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35152 of 200000 compute units", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", - }, - expected: []*AnchorInstruction{ - { - Name: "Execute", - ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", - Logs: []string{}, - ComputeUnits: 35152, - InnerCalls: []*AnchorInstruction{ - { - Name: "StructInstructionData", - ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", - Logs: []string{ - "Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", - }, - ComputeUnits: 13920, - InnerCalls: []*AnchorInstruction{}, - }, - }, - }, - }, - }, - { - name: "Test Case 4 - AccountRead", - logs: []string{ - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", - "Program log: Instruction: Execute", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", - "Program log: Instruction: AccountRead", - "Program log: Called `account_read` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountRead { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: false, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } } }, remaining_accounts: [], bumps: AccountReadBumps { u8_value: 255 } }", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 45559 of 177765 compute units", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 69682 of 200000 compute units", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", - }, - expected: []*AnchorInstruction{ - { - Name: "Execute", - ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", - Logs: []string{}, - ComputeUnits: 69682, - InnerCalls: []*AnchorInstruction{ - { - Name: "AccountRead", - ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", - Logs: []string{ - "Called `account_read` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountRead { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: false, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } } }, remaining_accounts: [], bumps: AccountReadBumps { u8_value: 255 } }", - }, - ComputeUnits: 45559, - InnerCalls: []*AnchorInstruction{}, - }, - }, - }, - }, - }, - { - name: "Test Case 5 - AccountMut", - logs: []string{ - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", - "Program log: Instruction: Execute", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", - "Program log: Instruction: AccountMut", - "Program log: Called `account_mut` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountMut { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } }, stub_caller: Signer { info: AccountInfo { key: BUx7YZMoVXCnT2BewMZc2hr8yxoiihtHdDuoa19D9R5q, owner: 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX, is_signer: true, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 2874480, data.len: 285, data: 2c3eace1f603b2211266a21317e30848020102010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000, .. } }, system_program: Program { info: AccountInfo { key: 11111111111111111111111111111111, owner: NativeLoader1111111111111111111111111111111, is_signer: false, is_writable: false, executable: true, rent_epoch: 0, lamports: 1, data.len: 14, data: 73797374656d5f70726f6772616d, .. } } }, remaining_accounts: [], bumps: AccountMutBumps { u8_value: 255 } }", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 111015 of 173365 compute units", - "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 139571 of 200000 compute units", - "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", - }, - expected: []*AnchorInstruction{ - { - Name: "Execute", - ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", - Logs: []string{}, - ComputeUnits: 139571, - InnerCalls: []*AnchorInstruction{ - { - Name: "AccountMut", - ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", - Logs: []string{ - "Called `account_mut` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountMut { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } }, stub_caller: Signer { info: AccountInfo { key: BUx7YZMoVXCnT2BewMZc2hr8yxoiihtHdDuoa19D9R5q, owner: 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX, is_signer: true, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 2874480, data.len: 285, data: 2c3eace1f603b2211266a21317e30848020102010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000, .. } }, system_program: Program { info: AccountInfo { key: 11111111111111111111111111111111, owner: NativeLoader1111111111111111111111111111111, is_signer: false, is_writable: false, executable: true, rent_epoch: 0, lamports: 1, data.len: 14, data: 73797374656d5f70726f6772616d, .. } } }, remaining_accounts: [], bumps: AccountMutBumps { u8_value: 255 } }", - }, - ComputeUnits: 111015, - InnerCalls: []*AnchorInstruction{}, - }, - }, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ParseLogMessages(tt.logs) - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("Test %s failed.\nExpected:\n%#v\nGot:\n%#v", tt.name, tt.expected, result) - } - }) - } -} diff --git a/chains/solana/contracts/tests/utils/logparser.go b/chains/solana/contracts/tests/utils/logparser.go new file mode 100644 index 00000000..247f8426 --- /dev/null +++ b/chains/solana/contracts/tests/utils/logparser.go @@ -0,0 +1,267 @@ +package utils + +import ( + "encoding/base64" + "fmt" + "regexp" + "strconv" + "strings" + + bin "github.com/gagliardetto/binary" +) + +type AnchorInstruction struct { + Name string + ProgramID string + Logs []string + ComputeUnits int + InnerCalls []*AnchorInstruction + EventData []*EventData +} + +type EventMapping struct { + Name string + New func() interface{} +} + +type EventData struct { + Base64Data string + DecodedData []byte + EventName string + Data interface{} // Decoded data using the provided type +} + +func EventMappingFor[T any](name string) EventMapping { + return EventMapping{ + Name: name, + New: func() interface{} { + return new(T) + }, + } +} + +func NormalizeData(d []byte) []byte { + if d == nil { + return []byte{} + } + return d +} + +// holds the state while parsing log lines +type parser struct { + instructions []*AnchorInstruction + stack []*AnchorInstruction + currentInstruction *AnchorInstruction + expectedEvents []EventMapping + + // compiled regexes for performance + programInvokeRegex *regexp.Regexp + programSuccessRegex *regexp.Regexp + computeUnitsRegex *regexp.Regexp +} + +// creates a parser instance with precompiled regex +func newParser(expectedEvents []EventMapping) *parser { + return &parser{ + instructions: []*AnchorInstruction{}, + stack: []*AnchorInstruction{}, + expectedEvents: expectedEvents, + programInvokeRegex: regexp.MustCompile(`Program (\w+) invoke`), + programSuccessRegex: regexp.MustCompile(`Program (\w+) success`), + computeUnitsRegex: regexp.MustCompile(`Program (\w+) consumed (\d+) of \d+ compute units`), + } +} + +func (p *parser) handleProgramInvokeLine(line string) bool { + match := p.programInvokeRegex.FindStringSubmatch(line) + if len(match) <= 1 { + return false + } + + newInstruction := &AnchorInstruction{ + ProgramID: match[1], + Name: "", + Logs: []string{}, + ComputeUnits: 0, + InnerCalls: []*AnchorInstruction{}, + EventData: []*EventData{}, + } + + if len(p.stack) == 0 { + p.instructions = append(p.instructions, newInstruction) + } else { + p.stack[len(p.stack)-1].InnerCalls = append(p.stack[len(p.stack)-1].InnerCalls, newInstruction) + } + + p.stack = append(p.stack, newInstruction) + p.currentInstruction = newInstruction + return true +} + +// check if line is a "Program X success" line and updates stack +func (p *parser) handleProgramSuccessLine(line string) bool { + match := p.programSuccessRegex.FindStringSubmatch(line) + if len(match) <= 1 { + return false + } + + if len(p.stack) > 0 { + p.stack = p.stack[:len(p.stack)-1] // pop + if len(p.stack) > 0 { + p.currentInstruction = p.stack[len(p.stack)-1] + } else { + p.currentInstruction = nil + } + } + return true +} + +func (p *parser) handleInstructionNameLine(line string) bool { + if !strings.Contains(line, "Instruction:") { + return false + } + if p.currentInstruction != nil { + p.currentInstruction.Name = strings.TrimSpace(strings.Split(line, "Instruction:")[1]) + } + return true +} + +func (p *parser) handleProgramDataLine(line string) bool { + if !strings.Contains(line, "Program data:") { + return false + } + if p.currentInstruction == nil { + return true // line recognized but no current instruction, do nothing + } + + base64Data := strings.TrimSpace(strings.TrimPrefix(line, "Program data:")) + decodedData, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + return true // recognized line but decode failed, no event + } + + for _, event := range p.expectedEvents { + if IsEvent(event.Name, decodedData) { + obj := event.New() + // NOTE: skipping the first 8 bytes which are the discriminator + if err := bin.UnmarshalBorsh(&obj, decodedData[8:]); err != nil { + continue + } + + eventData := &EventData{ + Base64Data: base64Data, + DecodedData: decodedData, + EventName: event.Name, + Data: obj, + } + p.currentInstruction.EventData = append(p.currentInstruction.EventData, eventData) + } + } + return true +} + +func (p *parser) handleProgramLogLine(line string) bool { + if !strings.HasPrefix(line, "Program log:") { + return false + } + if p.currentInstruction != nil { + logMessage := strings.TrimSpace(strings.TrimPrefix(line, "Program log:")) + p.currentInstruction.Logs = append(p.currentInstruction.Logs, logMessage) + } + return true +} + +func (p *parser) handleComputeUnitsLine(line string) bool { + match := p.computeUnitsRegex.FindStringSubmatch(line) + if len(match) <= 1 { + return false + } + programID := match[1] + units, _ := strconv.Atoi(match[2]) + + // Find the instruction in the stack that matches this program ID + for i := len(p.stack) - 1; i >= 0; i-- { + if p.stack[i].ProgramID == programID { + p.stack[i].ComputeUnits = units + break + } + } + return true +} + +func ParseLogMessages(logMessages []string, expectedEvents []EventMapping) []*AnchorInstruction { + p := newParser(expectedEvents) + + for _, line := range logMessages { + line = strings.TrimSpace(line) + + // Try each handler in turn + if p.handleProgramInvokeLine(line) { + continue + } + if p.handleProgramSuccessLine(line) { + continue + } + if p.handleInstructionNameLine(line) { + continue + } + if p.handleProgramDataLine(line) { + continue + } + if p.handleProgramLogLine(line) { + continue + } + if p.handleComputeUnitsLine(line) { + continue + } + + // if none matched, this line might be irrelevant + } + + return p.instructions +} + +// Pretty prints the given Anchor instructions. +// Example usage: +// parsed := utils.ParseLogMessages(result.Meta.LogMessages) +// output := utils.LogTxResult(parsed) +// t.Logf("Tx logs: %s", output) +func LogTxResult(instructions []*AnchorInstruction) string { + var output strings.Builder + + var printInstruction func(*AnchorInstruction, int, string) + printInstruction = func(instruction *AnchorInstruction, index int, indent string) { + output.WriteString(fmt.Sprintf("%sInstruction %d: %s\n", indent, index, instruction.Name)) + output.WriteString(fmt.Sprintf("%s Program ID: %s\n", indent, instruction.ProgramID)) + output.WriteString(fmt.Sprintf("%s Compute Units: %d\n", indent, instruction.ComputeUnits)) + + // Print Events + if len(instruction.EventData) > 0 { + output.WriteString(fmt.Sprintf("%s Events:\n", indent)) + for _, event := range instruction.EventData { + output.WriteString(fmt.Sprintf("%s Event: %s:\n", indent, event.EventName)) + output.WriteString(fmt.Sprintf("%s Base64Data: %+v\n", indent, event.Base64Data)) + output.WriteString(fmt.Sprintf("%s DecodedData: %+v\n", indent, event.DecodedData)) + output.WriteString(fmt.Sprintf("%s Data: %+v\n", indent, event.Data)) + } + } + + output.WriteString(fmt.Sprintf("%s Logs:\n", indent)) + for _, log := range instruction.Logs { + output.WriteString(fmt.Sprintf("%s %s\n", indent, log)) + } + + if len(instruction.InnerCalls) > 0 { + output.WriteString(fmt.Sprintf("%s Inner Calls:\n", indent)) + for i, innerCall := range instruction.InnerCalls { + printInstruction(innerCall, i+1, indent+" ") + } + } + } + + for i, instruction := range instructions { + printInstruction(instruction, i+1, "") + } + + return output.String() +} diff --git a/chains/solana/contracts/tests/utils/logparser_test.go b/chains/solana/contracts/tests/utils/logparser_test.go new file mode 100644 index 00000000..603acaf5 --- /dev/null +++ b/chains/solana/contracts/tests/utils/logparser_test.go @@ -0,0 +1,369 @@ +package utils + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestParseLogMessages(t *testing.T) { + // basic log parsing tests(w/o events) + tests := []struct { + name string + logs []string + expected []*AnchorInstruction + }{ + { + name: "Test Case 1 - Empty Instruction", + logs: []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", + "Program log: Instruction: Empty", + "Program log: Called `empty` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps }", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13620 of 180083 compute units", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35400 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + }, + expected: []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + Logs: []string{}, + InnerCalls: []*AnchorInstruction{ + { + Name: "Empty", + ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", + Logs: []string{ + "Called `empty` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps }", + }, + InnerCalls: []*AnchorInstruction{}, + }, + }, + }, + }, + }, + { + name: "Test Case 2 - U8InstructionData", + logs: []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", + "Program log: Instruction: U8InstructionData", + "Program log: Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13648 of 180048 compute units", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35463 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + }, + expected: []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + Logs: []string{}, + InnerCalls: []*AnchorInstruction{ + { + Name: "U8InstructionData", + ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", + Logs: []string{ + "Called `u8_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data 123", + }, + InnerCalls: []*AnchorInstruction{}, + }, + }, + }, + }, + }, + { + name: "Test Case 3 - StructInstructionData", + logs: []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", + "Program log: Instruction: StructInstructionData", + "Program log: Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 13920 of 180631 compute units", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 35152 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + }, + expected: []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + Logs: []string{}, + InnerCalls: []*AnchorInstruction{ + { + Name: "StructInstructionData", + ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", + Logs: []string{ + "Called `struct_instruction_data` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: Empty, remaining_accounts: [], bumps: EmptyBumps } and data Value { value: 234 }", + }, + InnerCalls: []*AnchorInstruction{}, + }, + }, + }, + }, + }, + { + name: "Test Case 4 - AccountRead", + logs: []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", + "Program log: Instruction: AccountRead", + "Program log: Called `account_read` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountRead { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: false, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } } }, remaining_accounts: [], bumps: AccountReadBumps { u8_value: 255 } }", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 45559 of 177765 compute units", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 69682 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + }, + expected: []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + Logs: []string{}, + InnerCalls: []*AnchorInstruction{ + { + Name: "AccountRead", + ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", + Logs: []string{ + "Called `account_read` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountRead { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: false, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } } }, remaining_accounts: [], bumps: AccountReadBumps { u8_value: 255 } }", + }, + InnerCalls: []*AnchorInstruction{}, + }, + }, + }, + }, + }, + { + name: "Test Case 5 - AccountMut", + logs: []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ invoke [2]", + "Program log: Instruction: AccountMut", + "Program log: Called `account_mut` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountMut { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } }, stub_caller: Signer { info: AccountInfo { key: BUx7YZMoVXCnT2BewMZc2hr8yxoiihtHdDuoa19D9R5q, owner: 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX, is_signer: true, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 2874480, data.len: 285, data: 2c3eace1f603b2211266a21317e30848020102010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000, .. } }, system_program: Program { info: AccountInfo { key: 11111111111111111111111111111111, owner: NativeLoader1111111111111111111111111111111, is_signer: false, is_writable: false, executable: true, rent_epoch: 0, lamports: 1, data.len: 14, data: 73797374656d5f70726f6772616d, .. } } }, remaining_accounts: [], bumps: AccountMutBumps { u8_value: 255 } }", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ consumed 111015 of 173365 compute units", + "Program 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ success", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 139571 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + }, + expected: []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + Logs: []string{}, + InnerCalls: []*AnchorInstruction{ + { + Name: "AccountMut", + ProgramID: "4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ", + Logs: []string{ + "Called `account_mut` Context { program_id: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, accounts: AccountMut { u8_value: Account { account: Value { value: 1 }, info: AccountInfo { key: 8WGXBpVJrBATopzT8iXvRuvp5f3U63uB13tfQjGoi6rM, owner: 4HeqEoSyfYpeC2goFLj9eHgkxV33mR5G7JYAbRsN14uQ, is_signer: false, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 953520, data.len: 9, data: 879ef47548cb18c201, .. } }, stub_caller: Signer { info: AccountInfo { key: BUx7YZMoVXCnT2BewMZc2hr8yxoiihtHdDuoa19D9R5q, owner: 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX, is_signer: true, is_writable: true, executable: false, rent_epoch: 18446744073709551615, lamports: 2874480, data.len: 285, data: 2c3eace1f603b2211266a21317e30848020102010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000, .. } }, system_program: Program { info: AccountInfo { key: 11111111111111111111111111111111, owner: NativeLoader1111111111111111111111111111111, is_signer: false, is_writable: false, executable: true, rent_epoch: 0, lamports: 1, data.len: 14, data: 73797374656d5f70726f6772616d, .. } } }, remaining_accounts: [], bumps: AccountMutBumps { u8_value: 255 } }", + }, + InnerCalls: []*AnchorInstruction{}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseLogMessages(tt.logs, []EventMapping{}) + require.Equal(t, len(tt.expected), len(result), "Instruction count mismatch - expected: %d, got: %d", len(tt.expected), len(result)) + for i := range tt.expected { + assertInstructionEqual(t, tt.expected[i], result[i]) + } + }) + } + + // NOTE: events for test, unable to import due to circular dependency + // contracts/mcm_events + type OpExecuted struct { + Nonce uint64 // nonce + To solana.PublicKey // to + Data []byte // data: Vec + } + // contracts/timelock_events + type CallScheduled struct { + ID [32]byte // id + Index uint64 // index + Target solana.PublicKey // target + Predecessor [32]byte // predecessor + Salt [32]byte // salt + Delay uint64 // delay + Data []byte // data: Vec + } + + t.Run("should parse nested cpi events correctly", func(t *testing.T) { + // 18 mint events + logs := []string{ + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX invoke [1]", + "Program log: Instruction: Execute", + "Program LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn invoke [2]", + "Program log: Instruction: ScheduleBatch", + "Program data: v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgAAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADADodkgXAAAACQ==", + "Program data: v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgEAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADADQ7ZAuAAAACQ==", + "Program data: v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgIAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADAC4ZNlFAAAACQ==", + "Program LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn consumed 23402 of 165315 compute units", + "Program LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn success", + "Program data: 3Q/UHSP8/04EAAAAAAAAAAUSRxvy9oST12hyi0H01X8FgtbLw1CWTtStGNnHCsctMAAAAPKMV2pH4lYg/pL7Faga/wLXhVbv8Hmjh0+iLmKk8oVqrvjqNDeh+74BAAAAAAAAAA==", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX consumed 60678 of 200000 compute units", + "Program 6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX success", + } + + timelockPubkey := solana.PublicKey{} + err := timelockPubkey.Set("LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn") + require.NoError(t, err) + + tokenPubkey := solana.PublicKey{} + terr := tokenPubkey.Set("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") + require.NoError(t, terr) + + expected := []*AnchorInstruction{ + { + Name: "Execute", + ProgramID: "6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX", + EventData: []*EventData{ + { + EventName: "OpExecuted", + Base64Data: "3Q/UHSP8/04EAAAAAAAAAAUSRxvy9oST12hyi0H01X8FgtbLw1CWTtStGNnHCsctMAAAAPKMV2pH4lYg/pL7Faga/wLXhVbv8Hmjh0+iLmKk8oVqrvjqNDeh+74BAAAAAAAAAA==", + DecodedData: []byte{221, 15, 212, 29, 35, 252, 255, 78, 4, 0, 0, 0, 0, 0, 0, 0, 5, 18, 71, 27, 242, 246, 132, 147, 215, 104, 114, 139, 65, 244, 213, 127, 5, 130, 214, 203, 195, 80, 150, 78, 212, 173, 24, 217, 199, 10, 199, 45, 48, 0, 0, 0, 242, 140, 87, 106, 71, 226, 86, 32, 254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190, 1, 0, 0, 0, 0, 0, 0, 0}, + Data: &OpExecuted{ + Nonce: 4, + To: timelockPubkey, + Data: []byte{242, 140, 87, 106, 71, 226, 86, 32, 254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190, 1, 0, 0, 0, 0, 0, 0, 0}, + }, + }, + }, + InnerCalls: []*AnchorInstruction{ + { + Name: "ScheduleBatch", + ProgramID: "LoCoNsJFuhTkSQjfdDfn3yuwqhSYoPujmviRHVCzsqn", + EventData: []*EventData{{ + EventName: "CallScheduled", + Base64Data: "v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgAAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADADodkgXAAAACQ==", + DecodedData: []byte{191, 85, 90, 167, 132, 223, 184, 57, 254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190, 0, 0, 0, 0, 0, 0, 0, 0, 6, 221, 246, 225, 238, 117, 143, 222, 24, 66, 93, 188, 228, 108, 205, 218, 182, 26, 252, 77, 131, 185, 13, 39, 254, 189, 249, 40, 216, 161, 139, 252, 52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223, 0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 12, 0, 232, 118, 72, 23, 0, 0, 0, 9}, + Data: &CallScheduled{ + ID: [32]byte{254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190}, + Index: 0, + Target: tokenPubkey, + Predecessor: [32]byte{52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223}, + Salt: [32]byte{0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: 1, + Data: []byte{12, 0, 232, 118, 72, 23, 0, 0, 0, 9}, + }, + }, + { + EventName: "CallScheduled", + Base64Data: "v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgEAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADADQ7ZAuAAAACQ==", + DecodedData: []byte{191, 85, 90, 167, 132, 223, 184, 57, 254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190, 1, 0, 0, 0, 0, 0, 0, 0, 6, 221, 246, 225, 238, 117, 143, 222, 24, 66, 93, 188, 228, 108, 205, 218, 182, 26, 252, 77, 131, 185, 13, 39, 254, 189, 249, 40, 216, 161, 139, 252, 52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223, 0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 12, 0, 208, 237, 144, 46, 0, 0, 0, 9}, + Data: &CallScheduled{ + ID: [32]byte{254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190}, + Index: 1, + Target: tokenPubkey, + Predecessor: [32]byte{52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223}, + Salt: [32]byte{0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: 1, + Data: []byte{12, 0, 208, 237, 144, 46, 0, 0, 0, 9}, + }, + }, + { + EventName: "CallScheduled", + Base64Data: "v1Vap4TfuDn+kvsVqBr/AteFVu/weaOHT6IuYqTyhWqu+Oo0N6H7vgIAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/w0fAqd5gQfkYIohsaMAW0Tz4R1O58t9Hes5QbmGAti3wAAAZPU+0A5PuT/0+5So7QAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAADAC4ZNlFAAAACQ==", + DecodedData: []byte{191, 85, 90, 167, 132, 223, 184, 57, 254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190, 2, 0, 0, 0, 0, 0, 0, 0, 6, 221, 246, 225, 238, 117, 143, 222, 24, 66, 93, 188, 228, 108, 205, 218, 182, 26, 252, 77, 131, 185, 13, 39, 254, 189, 249, 40, 216, 161, 139, 252, 52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223, 0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 12, 0, 184, 100, 217, 69, 0, 0, 0, 9}, + Data: &CallScheduled{ + ID: [32]byte{254, 146, 251, 21, 168, 26, 255, 2, 215, 133, 86, 239, 240, 121, 163, 135, 79, 162, 46, 98, 164, 242, 133, 106, 174, 248, 234, 52, 55, 161, 251, 190}, + Index: 2, + Target: tokenPubkey, + Predecessor: [32]byte{52, 124, 10, 157, 230, 4, 31, 145, 130, 40, 134, 198, 140, 1, 109, 19, 207, 132, 117, 59, 159, 45, 244, 119, 172, 229, 6, 230, 24, 11, 98, 223}, + Salt: [32]byte{0, 0, 1, 147, 212, 251, 64, 57, 62, 228, 255, 211, 238, 82, 163, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: 1, + Data: []byte{12, 0, 184, 100, 217, 69, 0, 0, 0, 9}, + }, + }, + }, + }, + }, + }, + } + + result := ParseLogMessages(logs, []EventMapping{ + EventMappingFor[OpExecuted]("OpExecuted"), + EventMappingFor[CallScheduled]("CallScheduled"), + }) + require.Equal(t, len(expected), len(result), "Instruction count mismatch - expected: %d, got: %d", len(expected), len(result)) + + // verify Execute instruction result + for i, instruction := range result { + require.Equal(t, expected[i].Name, instruction.Name) + require.Equal(t, expected[i].ProgramID, instruction.ProgramID) + + // verify OpExecuted event + require.Equal(t, len(expected[i].EventData), len(instruction.EventData)) + if len(expected[i].EventData) > 0 { + require.Equal(t, expected[i].EventData[0].EventName, instruction.EventData[0].EventName) + require.Equal(t, expected[i].EventData[0].Base64Data, instruction.EventData[0].Base64Data) + require.Equal(t, expected[i].EventData[0].DecodedData, instruction.EventData[0].DecodedData) + require.Equal(t, expected[i].EventData[0].Data.(*OpExecuted).To, instruction.EventData[0].Data.(*OpExecuted).To) + } + + // verify inner calls (ScheduleBatch) + require.Equal(t, len(expected[i].InnerCalls), len(instruction.InnerCalls)) + if len(instruction.InnerCalls) > 0 { + innerCall := instruction.InnerCalls[0] + expectedInner := expected[i].InnerCalls[0] + + require.Equal(t, expectedInner.Name, innerCall.Name) + require.Equal(t, expectedInner.ProgramID, innerCall.ProgramID) + + // verify CallScheduled events and their indices + require.Equal(t, len(expectedInner.EventData), len(innerCall.EventData)) + for j := range innerCall.EventData { + event := innerCall.EventData[j] + expectedEvent := expectedInner.EventData[j] + + require.Equal(t, expectedEvent.EventName, event.EventName) + require.Equal(t, expectedEvent.Base64Data, event.Base64Data) + require.Equal(t, expectedEvent.DecodedData, event.DecodedData) + + scheduledEvent, ok := event.Data.(*CallScheduled) + require.True(t, ok) + + expectedEventData, ok := expectedEvent.Data.(*CallScheduled) + require.True(t, ok) + + require.Equal(t, uint64(j), scheduledEvent.Index, "Event index mismatch at position %d", j) + require.Equal(t, expectedEventData.ID, scheduledEvent.ID, "Event ID mismatch at position %d", j) + require.Equal(t, expectedEventData.Target, scheduledEvent.Target, "Event target mismatch at position %d", j) + require.Equal(t, expectedEventData.Predecessor, scheduledEvent.Predecessor, "Event predecessor mismatch at position %d", j) + require.Equal(t, expectedEventData.Salt, scheduledEvent.Salt, "Event salt mismatch at position %d", j) + require.Equal(t, expectedEventData.Delay, scheduledEvent.Delay, "Event delay mismatch at position %d", j) + require.Equal(t, expectedEventData.Data, scheduledEvent.Data, "Event data mismatch at position %d", j) + } + } + } + }) +} + +func assertInstructionEqual(t *testing.T, expected, actual *AnchorInstruction) { + t.Helper() + + require.Equal(t, expected.Name, actual.Name, "Instruction name mismatch - expected: %s, got: %s", expected.Name, actual.Name) + require.Equal(t, expected.ProgramID, actual.ProgramID, "Program ID mismatch - expected: %s, got: %s", expected.ProgramID, actual.ProgramID) + require.Equal(t, len(expected.Logs), len(actual.Logs), "Log count mismatch - expected: %d, got: %d", len(expected.Logs), len(actual.Logs)) + + for i := range expected.Logs { + require.Equal(t, expected.Logs[i], actual.Logs[i], "Log mismatch at index %d - expected: %s, got: %s", i, expected.Logs[i], actual.Logs[i]) + } + + require.Equal(t, len(expected.InnerCalls), len(actual.InnerCalls), "Inner calls count mismatch - expected: %d, got: %d", len(expected.InnerCalls), len(actual.InnerCalls)) + + for i := range expected.InnerCalls { + assertInstructionEqual(t, expected.InnerCalls[i], actual.InnerCalls[i]) + } +} diff --git a/chains/solana/contracts/tests/utils/mcms/common.go b/chains/solana/contracts/tests/utils/mcms/common.go index eae848a3..4c22c05d 100644 --- a/chains/solana/contracts/tests/utils/mcms/common.go +++ b/chains/solana/contracts/tests/utils/mcms/common.go @@ -1,16 +1,14 @@ package mcms import ( - "bytes" crypto_rand "crypto/rand" "encoding/binary" "errors" "fmt" "math" - "slices" "time" - "github.com/gagliardetto/solana-go" + "golang.org/x/crypto/sha3" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/utils/eth" ) @@ -71,10 +69,10 @@ func NewValidMcmConfig(msigName [32]byte, signerPrivateKeys []string, signerGrou return config, nil } -func FindInSortedList(list []solana.PublicKey, target solana.PublicKey) (int, bool) { - return slices.BinarySearchFunc(list, target, func(a, b solana.PublicKey) int { - return bytes.Compare(a.Bytes(), b.Bytes()) - }) +func Keccak256(data []byte) []byte { + hash := sha3.NewLegacyKeccak256() + hash.Write(data) + return hash.Sum(nil) } func SafeToUint8(n int) (uint8, error) { diff --git a/chains/solana/contracts/tests/utils/transactions.go b/chains/solana/contracts/tests/utils/transactions.go index 3f26e836..d76ca2b8 100644 --- a/chains/solana/contracts/tests/utils/transactions.go +++ b/chains/solana/contracts/tests/utils/transactions.go @@ -8,8 +8,6 @@ import ( "testing" "time" - "strconv" - bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -43,7 +41,7 @@ func SendAndFailWith(ctx context.Context, t *testing.T, rpcClient *rpc.Client, i txres := sendTransactionWithLookupTables(ctx, rpcClient, t, instructions, signer, commitment, true, emptyLookupTables, opts...) // skipPreflight when expected to fail so revert captured onchain require.NotNil(t, txres.Meta) - require.NotNil(t, txres.Meta.Err) + require.NotNil(t, txres.Meta.Err, fmt.Sprintf("tx should have reverted with: %+v", expectedErrors)) logs := strings.Join(txres.Meta.LogMessages, " ") for _, expectedError := range expectedErrors { require.Contains(t, logs, expectedError, fmt.Sprintf("The logs did not contain '%s'. The logs were: %s", expectedError, logs)) @@ -111,6 +109,12 @@ func AddComputeUnitLimit(v fees.ComputeUnitLimit) TxModifier { } } +func AddComputeUnitPrice(v fees.ComputeUnitPrice) TxModifier { + return func(tx *solana.Transaction, _ map[solana.PublicKey]solana.PrivateKey) error { + return fees.SetComputeUnitPrice(tx, v) + } +} + func sendTransactionWithLookupTables(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, lookupTables map[solana.PublicKey]solana.PublicKeySlice, opts ...TxModifier) *rpc.GetTransactionResult { hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) @@ -199,6 +203,7 @@ func ExtractReturnValue(ctx context.Context, t *testing.T, logs []string, progra if logs == nil { return []byte{} } + for _, log := range logs { if strings.HasPrefix(log, "Program return: "+programID) { parts := strings.Split(log, " ") @@ -211,20 +216,17 @@ func ExtractReturnValue(ctx context.Context, t *testing.T, logs []string, progra return []byte{} } -func ExtractReturnedError(ctx context.Context, t *testing.T, logs []string, programID string) *int { +func ExtractReturnedError(ctx context.Context, t *testing.T, logs []string, programID string) *string { if logs == nil { return nil } for _, log := range logs { - if strings.Contains(log, "Error Number: ") { - // extract error number from the string - parts := strings.Split(log, "Error Number: ") + if strings.Contains(log, "Error Code: ") { + parts := strings.Split(log, "Error Code: ") if len(parts) > 1 { - numberPart := strings.Split(parts[1], ".")[0] - errorNumber, err := strconv.Atoi(strings.TrimSpace(numberPart)) - require.NoError(t, err) - return &errorNumber + codePart := strings.Split(parts[1], ".")[0] + return &codePart } } } diff --git a/chains/solana/gobindings/ccip_router/AddChainSelector.go b/chains/solana/gobindings/ccip_router/AddChainSelector.go index 64c33f4f..e61b5319 100644 --- a/chains/solana/gobindings/ccip_router/AddChainSelector.go +++ b/chains/solana/gobindings/ccip_router/AddChainSelector.go @@ -27,20 +27,22 @@ type AddChainSelector struct { SourceChainConfig *SourceChainConfig DestChainConfig *DestChainConfig - // [0] = [WRITE] chainState + // [0] = [WRITE] sourceChainState // - // [1] = [] config + // [1] = [WRITE] destChainState // - // [2] = [WRITE, SIGNER] authority + // [2] = [] config // - // [3] = [] systemProgram + // [3] = [WRITE, SIGNER] authority + // + // [4] = [] systemProgram ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewAddChainSelectorInstructionBuilder creates a new `AddChainSelector` instruction builder. func NewAddChainSelectorInstructionBuilder() *AddChainSelector { nd := &AddChainSelector{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 4), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 5), } return nd } @@ -63,48 +65,59 @@ func (inst *AddChainSelector) SetDestChainConfig(destChainConfig DestChainConfig return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *AddChainSelector) SetChainStateAccount(chainState ag_solanago.PublicKey) *AddChainSelector { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState).WRITE() +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *AddChainSelector) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *AddChainSelector { + inst.AccountMetaSlice[0] = ag_solanago.Meta(sourceChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *AddChainSelector) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *AddChainSelector) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetDestChainStateAccount sets the "destChainState" account. +func (inst *AddChainSelector) SetDestChainStateAccount(destChainState ag_solanago.PublicKey) *AddChainSelector { + inst.AccountMetaSlice[1] = ag_solanago.Meta(destChainState).WRITE() + return inst +} + +// GetDestChainStateAccount gets the "destChainState" account. +func (inst *AddChainSelector) GetDestChainStateAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetConfigAccount sets the "config" account. func (inst *AddChainSelector) SetConfigAccount(config ag_solanago.PublicKey) *AddChainSelector { - inst.AccountMetaSlice[1] = ag_solanago.Meta(config) + inst.AccountMetaSlice[2] = ag_solanago.Meta(config) return inst } // GetConfigAccount gets the "config" account. func (inst *AddChainSelector) GetConfigAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } // SetAuthorityAccount sets the "authority" account. func (inst *AddChainSelector) SetAuthorityAccount(authority ag_solanago.PublicKey) *AddChainSelector { - inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).WRITE().SIGNER() + inst.AccountMetaSlice[3] = ag_solanago.Meta(authority).WRITE().SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *AddChainSelector) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[2] + return inst.AccountMetaSlice[3] } // SetSystemProgramAccount sets the "systemProgram" account. func (inst *AddChainSelector) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *AddChainSelector { - inst.AccountMetaSlice[3] = ag_solanago.Meta(systemProgram) + inst.AccountMetaSlice[4] = ag_solanago.Meta(systemProgram) return inst } // GetSystemProgramAccount gets the "systemProgram" account. func (inst *AddChainSelector) GetSystemProgramAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[3] + return inst.AccountMetaSlice[4] } func (inst AddChainSelector) Build() *Instruction { @@ -141,15 +154,18 @@ func (inst *AddChainSelector) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.DestChainState is not set") } if inst.AccountMetaSlice[2] == nil { - return errors.New("accounts.Authority is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[3] == nil { + return errors.New("accounts.Authority is not set") + } + if inst.AccountMetaSlice[4] == nil { return errors.New("accounts.SystemProgram is not set") } } @@ -172,11 +188,12 @@ func (inst *AddChainSelector) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) - accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[3])) + instructionBranch.Child("Accounts[len=5]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta("sourceChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" destChainState", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[3])) + accountsBranch.Child(ag_format.Meta(" systemProgram", inst.AccountMetaSlice[4])) }) }) }) @@ -226,7 +243,8 @@ func NewAddChainSelectorInstruction( sourceChainConfig SourceChainConfig, destChainConfig DestChainConfig, // Accounts: - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, + destChainState ag_solanago.PublicKey, config ag_solanago.PublicKey, authority ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey) *AddChainSelector { @@ -234,7 +252,8 @@ func NewAddChainSelectorInstruction( SetNewChainSelector(newChainSelector). SetSourceChainConfig(sourceChainConfig). SetDestChainConfig(destChainConfig). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). + SetDestChainStateAccount(destChainState). SetConfigAccount(config). SetAuthorityAccount(authority). SetSystemProgramAccount(systemProgram) diff --git a/chains/solana/gobindings/ccip_router/CcipSend.go b/chains/solana/gobindings/ccip_router/CcipSend.go index 12c9e55c..c8aca920 100644 --- a/chains/solana/gobindings/ccip_router/CcipSend.go +++ b/chains/solana/gobindings/ccip_router/CcipSend.go @@ -30,7 +30,7 @@ type CcipSend struct { // [0] = [] config // - // [1] = [WRITE] chainState + // [1] = [WRITE] destChainState // // [2] = [WRITE] nonce // @@ -46,7 +46,9 @@ type CcipSend struct { // // [7] = [] feeTokenConfig // - // [8] = [WRITE] feeTokenUserAssociatedAccount + // [8] = [] feeTokenUserAssociatedAccount + // ··········· CHECK this is the associated token account for the user paying the fee. + // ··········· If paying with native SOL, this must be the zero address. // // [9] = [WRITE] feeTokenReceiver // @@ -87,14 +89,14 @@ func (inst *CcipSend) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetChainStateAccount sets the "chainState" account. -func (inst *CcipSend) SetChainStateAccount(chainState ag_solanago.PublicKey) *CcipSend { - inst.AccountMetaSlice[1] = ag_solanago.Meta(chainState).WRITE() +// SetDestChainStateAccount sets the "destChainState" account. +func (inst *CcipSend) SetDestChainStateAccount(destChainState ag_solanago.PublicKey) *CcipSend { + inst.AccountMetaSlice[1] = ag_solanago.Meta(destChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *CcipSend) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetDestChainStateAccount gets the "destChainState" account. +func (inst *CcipSend) GetDestChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -169,12 +171,16 @@ func (inst *CcipSend) GetFeeTokenConfigAccount() *ag_solanago.AccountMeta { } // SetFeeTokenUserAssociatedAccountAccount sets the "feeTokenUserAssociatedAccount" account. +// CHECK this is the associated token account for the user paying the fee. +// If paying with native SOL, this must be the zero address. func (inst *CcipSend) SetFeeTokenUserAssociatedAccountAccount(feeTokenUserAssociatedAccount ag_solanago.PublicKey) *CcipSend { - inst.AccountMetaSlice[8] = ag_solanago.Meta(feeTokenUserAssociatedAccount).WRITE() + inst.AccountMetaSlice[8] = ag_solanago.Meta(feeTokenUserAssociatedAccount) return inst } // GetFeeTokenUserAssociatedAccountAccount gets the "feeTokenUserAssociatedAccount" account. +// CHECK this is the associated token account for the user paying the fee. +// If paying with native SOL, this must be the zero address. func (inst *CcipSend) GetFeeTokenUserAssociatedAccountAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[8] } @@ -246,7 +252,7 @@ func (inst *CcipSend) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.DestChainState is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Nonce is not set") @@ -299,7 +305,7 @@ func (inst *CcipSend) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=12]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" destChainState", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta(" nonce", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta(" systemProgram", inst.AccountMetaSlice[4])) @@ -349,7 +355,7 @@ func NewCcipSendInstruction( message Solana2AnyMessage, // Accounts: config ag_solanago.PublicKey, - chainState ag_solanago.PublicKey, + destChainState ag_solanago.PublicKey, nonce ag_solanago.PublicKey, authority ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey, @@ -364,7 +370,7 @@ func NewCcipSendInstruction( SetDestChainSelector(destChainSelector). SetMessage(message). SetConfigAccount(config). - SetChainStateAccount(chainState). + SetDestChainStateAccount(destChainState). SetNonceAccount(nonce). SetAuthorityAccount(authority). SetSystemProgramAccount(systemProgram). diff --git a/chains/solana/gobindings/ccip_router/Commit.go b/chains/solana/gobindings/ccip_router/Commit.go index 48c5ecca..a6986892 100644 --- a/chains/solana/gobindings/ccip_router/Commit.go +++ b/chains/solana/gobindings/ccip_router/Commit.go @@ -38,9 +38,9 @@ type Commit struct { Report *CommitInput Signatures *[][65]uint8 - // [0] = [WRITE] config + // [0] = [] config // - // [1] = [WRITE] chainState + // [1] = [WRITE] sourceChainState // // [2] = [WRITE] commitReport // @@ -80,7 +80,7 @@ func (inst *Commit) SetSignatures(signatures [][65]uint8) *Commit { // SetConfigAccount sets the "config" account. func (inst *Commit) SetConfigAccount(config ag_solanago.PublicKey) *Commit { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config).WRITE() + inst.AccountMetaSlice[0] = ag_solanago.Meta(config) return inst } @@ -89,14 +89,14 @@ func (inst *Commit) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetChainStateAccount sets the "chainState" account. -func (inst *Commit) SetChainStateAccount(chainState ag_solanago.PublicKey) *Commit { - inst.AccountMetaSlice[1] = ag_solanago.Meta(chainState).WRITE() +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *Commit) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *Commit { + inst.AccountMetaSlice[1] = ag_solanago.Meta(sourceChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *Commit) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *Commit) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -181,7 +181,7 @@ func (inst *Commit) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.CommitReport is not set") @@ -217,7 +217,7 @@ func (inst *Commit) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=6]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" sourceChainState", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta(" commitReport", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta(" systemProgram", inst.AccountMetaSlice[4])) @@ -272,7 +272,7 @@ func NewCommitInstruction( signatures [][65]uint8, // Accounts: config ag_solanago.PublicKey, - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, commitReport ag_solanago.PublicKey, authority ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey, @@ -282,7 +282,7 @@ func NewCommitInstruction( SetReport(report). SetSignatures(signatures). SetConfigAccount(config). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). SetCommitReportAccount(commitReport). SetAuthorityAccount(authority). SetSystemProgramAccount(systemProgram). diff --git a/chains/solana/gobindings/ccip_router/DisableDestChainSelector.go b/chains/solana/gobindings/ccip_router/DisableDestChainSelector.go index 1c308590..7555a468 100644 --- a/chains/solana/gobindings/ccip_router/DisableDestChainSelector.go +++ b/chains/solana/gobindings/ccip_router/DisableDestChainSelector.go @@ -21,7 +21,7 @@ import ( type DisableDestChainSelector struct { DestChainSelector *uint64 - // [0] = [WRITE] chainState + // [0] = [WRITE] destChainState // // [1] = [] config // @@ -43,14 +43,14 @@ func (inst *DisableDestChainSelector) SetDestChainSelector(destChainSelector uin return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *DisableDestChainSelector) SetChainStateAccount(chainState ag_solanago.PublicKey) *DisableDestChainSelector { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState).WRITE() +// SetDestChainStateAccount sets the "destChainState" account. +func (inst *DisableDestChainSelector) SetDestChainStateAccount(destChainState ag_solanago.PublicKey) *DisableDestChainSelector { + inst.AccountMetaSlice[0] = ag_solanago.Meta(destChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *DisableDestChainSelector) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetDestChainStateAccount gets the "destChainState" account. +func (inst *DisableDestChainSelector) GetDestChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } @@ -104,7 +104,7 @@ func (inst *DisableDestChainSelector) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.DestChainState is not set") } if inst.AccountMetaSlice[1] == nil { return errors.New("accounts.Config is not set") @@ -131,9 +131,9 @@ func (inst *DisableDestChainSelector) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta("chainState", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("destChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) }) }) }) @@ -161,12 +161,12 @@ func NewDisableDestChainSelectorInstruction( // Parameters: destChainSelector uint64, // Accounts: - chainState ag_solanago.PublicKey, + destChainState ag_solanago.PublicKey, config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *DisableDestChainSelector { return NewDisableDestChainSelectorInstructionBuilder(). SetDestChainSelector(destChainSelector). - SetChainStateAccount(chainState). + SetDestChainStateAccount(destChainState). SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/DisableSourceChainSelector.go b/chains/solana/gobindings/ccip_router/DisableSourceChainSelector.go index 2fa7652c..90f72920 100644 --- a/chains/solana/gobindings/ccip_router/DisableSourceChainSelector.go +++ b/chains/solana/gobindings/ccip_router/DisableSourceChainSelector.go @@ -21,7 +21,7 @@ import ( type DisableSourceChainSelector struct { SourceChainSelector *uint64 - // [0] = [WRITE] chainState + // [0] = [WRITE] sourceChainState // // [1] = [] config // @@ -43,14 +43,14 @@ func (inst *DisableSourceChainSelector) SetSourceChainSelector(sourceChainSelect return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *DisableSourceChainSelector) SetChainStateAccount(chainState ag_solanago.PublicKey) *DisableSourceChainSelector { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState).WRITE() +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *DisableSourceChainSelector) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *DisableSourceChainSelector { + inst.AccountMetaSlice[0] = ag_solanago.Meta(sourceChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *DisableSourceChainSelector) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *DisableSourceChainSelector) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } @@ -104,7 +104,7 @@ func (inst *DisableSourceChainSelector) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[1] == nil { return errors.New("accounts.Config is not set") @@ -131,9 +131,9 @@ func (inst *DisableSourceChainSelector) EncodeToTree(parent ag_treeout.Branches) // Accounts of the instruction: instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta("chainState", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("sourceChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) }) }) }) @@ -161,12 +161,12 @@ func NewDisableSourceChainSelectorInstruction( // Parameters: sourceChainSelector uint64, // Accounts: - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *DisableSourceChainSelector { return NewDisableSourceChainSelectorInstructionBuilder(). SetSourceChainSelector(sourceChainSelector). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/Execute.go b/chains/solana/gobindings/ccip_router/Execute.go index 25edb55c..cd136970 100644 --- a/chains/solana/gobindings/ccip_router/Execute.go +++ b/chains/solana/gobindings/ccip_router/Execute.go @@ -38,7 +38,7 @@ type Execute struct { // [0] = [] config // - // [1] = [] chainState + // [1] = [] sourceChainState // // [2] = [WRITE] commitReport // @@ -85,14 +85,14 @@ func (inst *Execute) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetChainStateAccount sets the "chainState" account. -func (inst *Execute) SetChainStateAccount(chainState ag_solanago.PublicKey) *Execute { - inst.AccountMetaSlice[1] = ag_solanago.Meta(chainState) +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *Execute) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *Execute { + inst.AccountMetaSlice[1] = ag_solanago.Meta(sourceChainState) return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *Execute) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *Execute) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -196,7 +196,7 @@ func (inst *Execute) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.CommitReport is not set") @@ -237,7 +237,7 @@ func (inst *Execute) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=8]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" sourceChainState", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta(" commitReport", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta("externalExecutionConfig", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[4])) @@ -283,7 +283,7 @@ func NewExecuteInstruction( reportContextByteWords [3][32]uint8, // Accounts: config ag_solanago.PublicKey, - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, commitReport ag_solanago.PublicKey, externalExecutionConfig ag_solanago.PublicKey, authority ag_solanago.PublicKey, @@ -294,7 +294,7 @@ func NewExecuteInstruction( SetExecutionReport(executionReport). SetReportContextByteWords(reportContextByteWords). SetConfigAccount(config). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). SetCommitReportAccount(commitReport). SetExternalExecutionConfigAccount(externalExecutionConfig). SetAuthorityAccount(authority). diff --git a/chains/solana/gobindings/ccip_router/GetFee.go b/chains/solana/gobindings/ccip_router/GetFee.go index 0f8a00d6..2442a459 100644 --- a/chains/solana/gobindings/ccip_router/GetFee.go +++ b/chains/solana/gobindings/ccip_router/GetFee.go @@ -25,7 +25,7 @@ type GetFee struct { DestChainSelector *uint64 Message *Solana2AnyMessage - // [0] = [] chainState + // [0] = [] destChainState // // [1] = [] billingTokenConfig ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` @@ -51,14 +51,14 @@ func (inst *GetFee) SetMessage(message Solana2AnyMessage) *GetFee { return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *GetFee) SetChainStateAccount(chainState ag_solanago.PublicKey) *GetFee { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState) +// SetDestChainStateAccount sets the "destChainState" account. +func (inst *GetFee) SetDestChainStateAccount(destChainState ag_solanago.PublicKey) *GetFee { + inst.AccountMetaSlice[0] = ag_solanago.Meta(destChainState) return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *GetFee) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetDestChainStateAccount gets the "destChainState" account. +func (inst *GetFee) GetDestChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } @@ -104,7 +104,7 @@ func (inst *GetFee) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.DestChainState is not set") } if inst.AccountMetaSlice[1] == nil { return errors.New("accounts.BillingTokenConfig is not set") @@ -129,7 +129,7 @@ func (inst *GetFee) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" destChainState", inst.AccountMetaSlice[0])) accountsBranch.Child(ag_format.Meta("billingTokenConfig", inst.AccountMetaSlice[1])) }) }) @@ -169,11 +169,11 @@ func NewGetFeeInstruction( destChainSelector uint64, message Solana2AnyMessage, // Accounts: - chainState ag_solanago.PublicKey, + destChainState ag_solanago.PublicKey, billingTokenConfig ag_solanago.PublicKey) *GetFee { return NewGetFeeInstructionBuilder(). SetDestChainSelector(destChainSelector). SetMessage(message). - SetChainStateAccount(chainState). + SetDestChainStateAccount(destChainState). SetBillingTokenConfigAccount(billingTokenConfig) } diff --git a/chains/solana/gobindings/ccip_router/Initialize.go b/chains/solana/gobindings/ccip_router/Initialize.go index 2448207b..63fdb997 100644 --- a/chains/solana/gobindings/ccip_router/Initialize.go +++ b/chains/solana/gobindings/ccip_router/Initialize.go @@ -29,24 +29,26 @@ type Initialize struct { // [0] = [WRITE] config // - // [1] = [WRITE, SIGNER] authority + // [1] = [WRITE] state // - // [2] = [] systemProgram + // [2] = [WRITE, SIGNER] authority // - // [3] = [] program + // [3] = [] systemProgram // - // [4] = [] programData + // [4] = [] program // - // [5] = [WRITE] externalExecutionConfig + // [5] = [] programData // - // [6] = [WRITE] tokenPoolsSigner + // [6] = [WRITE] externalExecutionConfig + // + // [7] = [WRITE] tokenPoolsSigner ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewInitializeInstructionBuilder creates a new `Initialize` instruction builder. func NewInitializeInstructionBuilder() *Initialize { nd := &Initialize{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 7), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 8), } return nd } @@ -86,70 +88,81 @@ func (inst *Initialize) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetStateAccount sets the "state" account. +func (inst *Initialize) SetStateAccount(state ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[1] = ag_solanago.Meta(state).WRITE() + return inst +} + +// GetStateAccount gets the "state" account. +func (inst *Initialize) GetStateAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *Initialize) SetAuthorityAccount(authority ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).WRITE().SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).WRITE().SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *Initialize) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } // SetSystemProgramAccount sets the "systemProgram" account. func (inst *Initialize) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[2] = ag_solanago.Meta(systemProgram) + inst.AccountMetaSlice[3] = ag_solanago.Meta(systemProgram) return inst } // GetSystemProgramAccount gets the "systemProgram" account. func (inst *Initialize) GetSystemProgramAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[2] + return inst.AccountMetaSlice[3] } // SetProgramAccount sets the "program" account. func (inst *Initialize) SetProgramAccount(program ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[3] = ag_solanago.Meta(program) + inst.AccountMetaSlice[4] = ag_solanago.Meta(program) return inst } // GetProgramAccount gets the "program" account. func (inst *Initialize) GetProgramAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[3] + return inst.AccountMetaSlice[4] } // SetProgramDataAccount sets the "programData" account. func (inst *Initialize) SetProgramDataAccount(programData ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[4] = ag_solanago.Meta(programData) + inst.AccountMetaSlice[5] = ag_solanago.Meta(programData) return inst } // GetProgramDataAccount gets the "programData" account. func (inst *Initialize) GetProgramDataAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[4] + return inst.AccountMetaSlice[5] } // SetExternalExecutionConfigAccount sets the "externalExecutionConfig" account. func (inst *Initialize) SetExternalExecutionConfigAccount(externalExecutionConfig ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[5] = ag_solanago.Meta(externalExecutionConfig).WRITE() + inst.AccountMetaSlice[6] = ag_solanago.Meta(externalExecutionConfig).WRITE() return inst } // GetExternalExecutionConfigAccount gets the "externalExecutionConfig" account. func (inst *Initialize) GetExternalExecutionConfigAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[5] + return inst.AccountMetaSlice[6] } // SetTokenPoolsSignerAccount sets the "tokenPoolsSigner" account. func (inst *Initialize) SetTokenPoolsSignerAccount(tokenPoolsSigner ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[6] = ag_solanago.Meta(tokenPoolsSigner).WRITE() + inst.AccountMetaSlice[7] = ag_solanago.Meta(tokenPoolsSigner).WRITE() return inst } // GetTokenPoolsSignerAccount gets the "tokenPoolsSigner" account. func (inst *Initialize) GetTokenPoolsSignerAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[6] + return inst.AccountMetaSlice[7] } func (inst Initialize) Build() *Instruction { @@ -192,21 +205,24 @@ func (inst *Initialize) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Authority is not set") + return errors.New("accounts.State is not set") } if inst.AccountMetaSlice[2] == nil { - return errors.New("accounts.SystemProgram is not set") + return errors.New("accounts.Authority is not set") } if inst.AccountMetaSlice[3] == nil { - return errors.New("accounts.Program is not set") + return errors.New("accounts.SystemProgram is not set") } if inst.AccountMetaSlice[4] == nil { - return errors.New("accounts.ProgramData is not set") + return errors.New("accounts.Program is not set") } if inst.AccountMetaSlice[5] == nil { - return errors.New("accounts.ExternalExecutionConfig is not set") + return errors.New("accounts.ProgramData is not set") } if inst.AccountMetaSlice[6] == nil { + return errors.New("accounts.ExternalExecutionConfig is not set") + } + if inst.AccountMetaSlice[7] == nil { return errors.New("accounts.TokenPoolsSigner is not set") } } @@ -230,14 +246,15 @@ func (inst *Initialize) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=7]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts[len=8]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" systemProgram", inst.AccountMetaSlice[2])) - accountsBranch.Child(ag_format.Meta(" program", inst.AccountMetaSlice[3])) - accountsBranch.Child(ag_format.Meta(" programData", inst.AccountMetaSlice[4])) - accountsBranch.Child(ag_format.Meta("externalExecutionConfig", inst.AccountMetaSlice[5])) - accountsBranch.Child(ag_format.Meta(" tokenPoolsSigner", inst.AccountMetaSlice[6])) + accountsBranch.Child(ag_format.Meta(" state", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta(" systemProgram", inst.AccountMetaSlice[3])) + accountsBranch.Child(ag_format.Meta(" program", inst.AccountMetaSlice[4])) + accountsBranch.Child(ag_format.Meta(" programData", inst.AccountMetaSlice[5])) + accountsBranch.Child(ag_format.Meta("externalExecutionConfig", inst.AccountMetaSlice[6])) + accountsBranch.Child(ag_format.Meta(" tokenPoolsSigner", inst.AccountMetaSlice[7])) }) }) }) @@ -299,6 +316,7 @@ func NewInitializeInstruction( enableExecutionAfter int64, // Accounts: config ag_solanago.PublicKey, + state ag_solanago.PublicKey, authority ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey, program ag_solanago.PublicKey, @@ -311,6 +329,7 @@ func NewInitializeInstruction( SetDefaultAllowOutOfOrderExecution(defaultAllowOutOfOrderExecution). SetEnableExecutionAfter(enableExecutionAfter). SetConfigAccount(config). + SetStateAccount(state). SetAuthorityAccount(authority). SetSystemProgramAccount(systemProgram). SetProgramAccount(program). diff --git a/chains/solana/gobindings/ccip_router/ManuallyExecute.go b/chains/solana/gobindings/ccip_router/ManuallyExecute.go index 7195db89..cfc565bd 100644 --- a/chains/solana/gobindings/ccip_router/ManuallyExecute.go +++ b/chains/solana/gobindings/ccip_router/ManuallyExecute.go @@ -25,7 +25,7 @@ type ManuallyExecute struct { // [0] = [] config // - // [1] = [] chainState + // [1] = [] sourceChainState // // [2] = [WRITE] commitReport // @@ -66,14 +66,14 @@ func (inst *ManuallyExecute) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetChainStateAccount sets the "chainState" account. -func (inst *ManuallyExecute) SetChainStateAccount(chainState ag_solanago.PublicKey) *ManuallyExecute { - inst.AccountMetaSlice[1] = ag_solanago.Meta(chainState) +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *ManuallyExecute) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *ManuallyExecute { + inst.AccountMetaSlice[1] = ag_solanago.Meta(sourceChainState) return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *ManuallyExecute) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *ManuallyExecute) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -174,7 +174,7 @@ func (inst *ManuallyExecute) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.CommitReport is not set") @@ -214,7 +214,7 @@ func (inst *ManuallyExecute) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=8]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" chainState", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" sourceChainState", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta(" commitReport", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta("externalExecutionConfig", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[4])) @@ -249,7 +249,7 @@ func NewManuallyExecuteInstruction( executionReport ExecutionReportSingleChain, // Accounts: config ag_solanago.PublicKey, - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, commitReport ag_solanago.PublicKey, externalExecutionConfig ag_solanago.PublicKey, authority ag_solanago.PublicKey, @@ -259,7 +259,7 @@ func NewManuallyExecuteInstruction( return NewManuallyExecuteInstructionBuilder(). SetExecutionReport(executionReport). SetConfigAccount(config). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). SetCommitReportAccount(commitReport). SetExternalExecutionConfigAccount(externalExecutionConfig). SetAuthorityAccount(authority). diff --git a/chains/solana/gobindings/ccip_router/SetOcrConfig.go b/chains/solana/gobindings/ccip_router/SetOcrConfig.go index 0f7b038b..d43bf530 100644 --- a/chains/solana/gobindings/ccip_router/SetOcrConfig.go +++ b/chains/solana/gobindings/ccip_router/SetOcrConfig.go @@ -28,14 +28,16 @@ type SetOcrConfig struct { // [0] = [WRITE] config // - // [1] = [SIGNER] authority + // [1] = [WRITE] state + // + // [2] = [SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewSetOcrConfigInstructionBuilder creates a new `SetOcrConfig` instruction builder. func NewSetOcrConfigInstructionBuilder() *SetOcrConfig { nd := &SetOcrConfig{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), } return nd } @@ -75,15 +77,26 @@ func (inst *SetOcrConfig) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetStateAccount sets the "state" account. +func (inst *SetOcrConfig) SetStateAccount(state ag_solanago.PublicKey) *SetOcrConfig { + inst.AccountMetaSlice[1] = ag_solanago.Meta(state).WRITE() + return inst +} + +// GetStateAccount gets the "state" account. +func (inst *SetOcrConfig) GetStateAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *SetOcrConfig) SetAuthorityAccount(authority ag_solanago.PublicKey) *SetOcrConfig { - inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *SetOcrConfig) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } func (inst SetOcrConfig) Build() *Instruction { @@ -126,6 +139,9 @@ func (inst *SetOcrConfig) Validate() error { return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.State is not set") + } + if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Authority is not set") } } @@ -149,9 +165,10 @@ func (inst *SetOcrConfig) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" state", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[2])) }) }) }) @@ -213,6 +230,7 @@ func NewSetOcrConfigInstruction( transmitters []ag_solanago.PublicKey, // Accounts: config ag_solanago.PublicKey, + state ag_solanago.PublicKey, authority ag_solanago.PublicKey) *SetOcrConfig { return NewSetOcrConfigInstructionBuilder(). SetPluginType(pluginType). @@ -220,5 +238,6 @@ func NewSetOcrConfigInstruction( SetSigners(signers). SetTransmitters(transmitters). SetConfigAccount(config). + SetStateAccount(state). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/UpdateDestChainConfig.go b/chains/solana/gobindings/ccip_router/UpdateDestChainConfig.go index 86654a83..1958a4e7 100644 --- a/chains/solana/gobindings/ccip_router/UpdateDestChainConfig.go +++ b/chains/solana/gobindings/ccip_router/UpdateDestChainConfig.go @@ -23,7 +23,7 @@ type UpdateDestChainConfig struct { DestChainSelector *uint64 DestChainConfig *DestChainConfig - // [0] = [WRITE] chainState + // [0] = [WRITE] destChainState // // [1] = [] config // @@ -51,14 +51,14 @@ func (inst *UpdateDestChainConfig) SetDestChainConfig(destChainConfig DestChainC return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *UpdateDestChainConfig) SetChainStateAccount(chainState ag_solanago.PublicKey) *UpdateDestChainConfig { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState).WRITE() +// SetDestChainStateAccount sets the "destChainState" account. +func (inst *UpdateDestChainConfig) SetDestChainStateAccount(destChainState ag_solanago.PublicKey) *UpdateDestChainConfig { + inst.AccountMetaSlice[0] = ag_solanago.Meta(destChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *UpdateDestChainConfig) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetDestChainStateAccount gets the "destChainState" account. +func (inst *UpdateDestChainConfig) GetDestChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } @@ -115,7 +115,7 @@ func (inst *UpdateDestChainConfig) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.DestChainState is not set") } if inst.AccountMetaSlice[1] == nil { return errors.New("accounts.Config is not set") @@ -143,9 +143,9 @@ func (inst *UpdateDestChainConfig) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta("chainState", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("destChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) }) }) }) @@ -184,13 +184,13 @@ func NewUpdateDestChainConfigInstruction( destChainSelector uint64, destChainConfig DestChainConfig, // Accounts: - chainState ag_solanago.PublicKey, + destChainState ag_solanago.PublicKey, config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *UpdateDestChainConfig { return NewUpdateDestChainConfigInstructionBuilder(). SetDestChainSelector(destChainSelector). SetDestChainConfig(destChainConfig). - SetChainStateAccount(chainState). + SetDestChainStateAccount(destChainState). SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/UpdateSourceChainConfig.go b/chains/solana/gobindings/ccip_router/UpdateSourceChainConfig.go index ba055777..baf89890 100644 --- a/chains/solana/gobindings/ccip_router/UpdateSourceChainConfig.go +++ b/chains/solana/gobindings/ccip_router/UpdateSourceChainConfig.go @@ -23,7 +23,7 @@ type UpdateSourceChainConfig struct { SourceChainSelector *uint64 SourceChainConfig *SourceChainConfig - // [0] = [WRITE] chainState + // [0] = [WRITE] sourceChainState // // [1] = [] config // @@ -51,14 +51,14 @@ func (inst *UpdateSourceChainConfig) SetSourceChainConfig(sourceChainConfig Sour return inst } -// SetChainStateAccount sets the "chainState" account. -func (inst *UpdateSourceChainConfig) SetChainStateAccount(chainState ag_solanago.PublicKey) *UpdateSourceChainConfig { - inst.AccountMetaSlice[0] = ag_solanago.Meta(chainState).WRITE() +// SetSourceChainStateAccount sets the "sourceChainState" account. +func (inst *UpdateSourceChainConfig) SetSourceChainStateAccount(sourceChainState ag_solanago.PublicKey) *UpdateSourceChainConfig { + inst.AccountMetaSlice[0] = ag_solanago.Meta(sourceChainState).WRITE() return inst } -// GetChainStateAccount gets the "chainState" account. -func (inst *UpdateSourceChainConfig) GetChainStateAccount() *ag_solanago.AccountMeta { +// GetSourceChainStateAccount gets the "sourceChainState" account. +func (inst *UpdateSourceChainConfig) GetSourceChainStateAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } @@ -115,7 +115,7 @@ func (inst *UpdateSourceChainConfig) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.ChainState is not set") + return errors.New("accounts.SourceChainState is not set") } if inst.AccountMetaSlice[1] == nil { return errors.New("accounts.Config is not set") @@ -143,9 +143,9 @@ func (inst *UpdateSourceChainConfig) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta("chainState", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("sourceChainState", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) }) }) }) @@ -184,13 +184,13 @@ func NewUpdateSourceChainConfigInstruction( sourceChainSelector uint64, sourceChainConfig SourceChainConfig, // Accounts: - chainState ag_solanago.PublicKey, + sourceChainState ag_solanago.PublicKey, config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *UpdateSourceChainConfig { return NewUpdateSourceChainConfigInstructionBuilder(). SetSourceChainSelector(sourceChainSelector). SetSourceChainConfig(sourceChainConfig). - SetChainStateAccount(chainState). + SetSourceChainStateAccount(sourceChainState). SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/accounts.go b/chains/solana/gobindings/ccip_router/accounts.go index f4d4baa8..a36be501 100644 --- a/chains/solana/gobindings/ccip_router/accounts.go +++ b/chains/solana/gobindings/ccip_router/accounts.go @@ -20,8 +20,6 @@ type Config struct { EnableManualExecutionAfter int64 Padding2 [8]uint8 Ocr3 [2]Ocr3Config - PaddingBeforeBilling [8]uint8 - LatestPriceSequenceNumber uint64 } var ConfigDiscriminator = [8]byte{155, 12, 170, 224, 30, 250, 204, 130} @@ -87,16 +85,6 @@ func (obj Config) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { if err != nil { return err } - // Serialize `PaddingBeforeBilling` param: - err = encoder.Encode(obj.PaddingBeforeBilling) - if err != nil { - return err - } - // Serialize `LatestPriceSequenceNumber` param: - err = encoder.Encode(obj.LatestPriceSequenceNumber) - if err != nil { - return err - } return nil } @@ -169,11 +157,43 @@ func (obj *Config) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) if err != nil { return err } - // Deserialize `PaddingBeforeBilling`: - err = decoder.Decode(&obj.PaddingBeforeBilling) + return nil +} + +type GlobalState struct { + LatestPriceSequenceNumber uint64 +} + +var GlobalStateDiscriminator = [8]byte{163, 46, 74, 168, 216, 123, 133, 98} + +func (obj GlobalState) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Write account discriminator: + err = encoder.WriteBytes(GlobalStateDiscriminator[:], false) if err != nil { return err } + // Serialize `LatestPriceSequenceNumber` param: + err = encoder.Encode(obj.LatestPriceSequenceNumber) + if err != nil { + return err + } + return nil +} + +func (obj *GlobalState) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Read and check account discriminator: + { + discriminator, err := decoder.ReadTypeID() + if err != nil { + return err + } + if !discriminator.Equal(GlobalStateDiscriminator[:]) { + return fmt.Errorf( + "wrong discriminator: wanted %s, got %s", + "[163 46 74 168 216 123 133 98]", + fmt.Sprint(discriminator[:])) + } + } // Deserialize `LatestPriceSequenceNumber`: err = decoder.Decode(&obj.LatestPriceSequenceNumber) if err != nil { @@ -182,17 +202,81 @@ func (obj *Config) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) return nil } -type ChainState struct { - Version uint8 - SourceChain SourceChain - DestChain DestChain +type SourceChain struct { + Version uint8 + State SourceChainState + Config SourceChainConfig +} + +var SourceChainDiscriminator = [8]byte{242, 235, 220, 98, 252, 121, 191, 216} + +func (obj SourceChain) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Write account discriminator: + err = encoder.WriteBytes(SourceChainDiscriminator[:], false) + if err != nil { + return err + } + // Serialize `Version` param: + err = encoder.Encode(obj.Version) + if err != nil { + return err + } + // Serialize `State` param: + err = encoder.Encode(obj.State) + if err != nil { + return err + } + // Serialize `Config` param: + err = encoder.Encode(obj.Config) + if err != nil { + return err + } + return nil +} + +func (obj *SourceChain) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Read and check account discriminator: + { + discriminator, err := decoder.ReadTypeID() + if err != nil { + return err + } + if !discriminator.Equal(SourceChainDiscriminator[:]) { + return fmt.Errorf( + "wrong discriminator: wanted %s, got %s", + "[242 235 220 98 252 121 191 216]", + fmt.Sprint(discriminator[:])) + } + } + // Deserialize `Version`: + err = decoder.Decode(&obj.Version) + if err != nil { + return err + } + // Deserialize `State`: + err = decoder.Decode(&obj.State) + if err != nil { + return err + } + // Deserialize `Config`: + err = decoder.Decode(&obj.Config) + if err != nil { + return err + } + return nil } -var ChainStateDiscriminator = [8]byte{130, 46, 94, 156, 79, 53, 170, 50} +type DestChain struct { + Version uint8 + State DestChainState + Config DestChainConfig +} -func (obj ChainState) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { +var DestChainDiscriminator = [8]byte{77, 18, 241, 132, 212, 54, 218, 16} + +func (obj DestChain) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { // Write account discriminator: - err = encoder.WriteBytes(ChainStateDiscriminator[:], false) + err = encoder.WriteBytes(DestChainDiscriminator[:], false) if err != nil { return err } @@ -201,30 +285,30 @@ func (obj ChainState) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) if err != nil { return err } - // Serialize `SourceChain` param: - err = encoder.Encode(obj.SourceChain) + // Serialize `State` param: + err = encoder.Encode(obj.State) if err != nil { return err } - // Serialize `DestChain` param: - err = encoder.Encode(obj.DestChain) + // Serialize `Config` param: + err = encoder.Encode(obj.Config) if err != nil { return err } return nil } -func (obj *ChainState) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { +func (obj *DestChain) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { // Read and check account discriminator: { discriminator, err := decoder.ReadTypeID() if err != nil { return err } - if !discriminator.Equal(ChainStateDiscriminator[:]) { + if !discriminator.Equal(DestChainDiscriminator[:]) { return fmt.Errorf( "wrong discriminator: wanted %s, got %s", - "[130 46 94 156 79 53 170 50]", + "[77 18 241 132 212 54 218 16]", fmt.Sprint(discriminator[:])) } } @@ -233,13 +317,13 @@ func (obj *ChainState) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err err if err != nil { return err } - // Deserialize `SourceChain`: - err = decoder.Decode(&obj.SourceChain) + // Deserialize `State`: + err = decoder.Decode(&obj.State) if err != nil { return err } - // Deserialize `DestChain`: - err = decoder.Decode(&obj.DestChain) + // Deserialize `Config`: + err = decoder.Decode(&obj.Config) if err != nil { return err } diff --git a/chains/solana/gobindings/ccip_router/types.go b/chains/solana/gobindings/ccip_router/types.go index f10f1277..a062a0b8 100644 --- a/chains/solana/gobindings/ccip_router/types.go +++ b/chains/solana/gobindings/ccip_router/types.go @@ -205,257 +205,6 @@ func (obj *MerkleRoot) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err err return nil } -type Solana2AnyMessage struct { - Receiver []byte - Data []byte - TokenAmounts []SolanaTokenAmount - FeeToken ag_solanago.PublicKey - ExtraArgs ExtraArgsInput - TokenIndexes []byte -} - -func (obj Solana2AnyMessage) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `Receiver` param: - err = encoder.Encode(obj.Receiver) - if err != nil { - return err - } - // Serialize `Data` param: - err = encoder.Encode(obj.Data) - if err != nil { - return err - } - // Serialize `TokenAmounts` param: - err = encoder.Encode(obj.TokenAmounts) - if err != nil { - return err - } - // Serialize `FeeToken` param: - err = encoder.Encode(obj.FeeToken) - if err != nil { - return err - } - // Serialize `ExtraArgs` param: - err = encoder.Encode(obj.ExtraArgs) - if err != nil { - return err - } - // Serialize `TokenIndexes` param: - err = encoder.Encode(obj.TokenIndexes) - if err != nil { - return err - } - return nil -} - -func (obj *Solana2AnyMessage) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `Receiver`: - err = decoder.Decode(&obj.Receiver) - if err != nil { - return err - } - // Deserialize `Data`: - err = decoder.Decode(&obj.Data) - if err != nil { - return err - } - // Deserialize `TokenAmounts`: - err = decoder.Decode(&obj.TokenAmounts) - if err != nil { - return err - } - // Deserialize `FeeToken`: - err = decoder.Decode(&obj.FeeToken) - if err != nil { - return err - } - // Deserialize `ExtraArgs`: - err = decoder.Decode(&obj.ExtraArgs) - if err != nil { - return err - } - // Deserialize `TokenIndexes`: - err = decoder.Decode(&obj.TokenIndexes) - if err != nil { - return err - } - return nil -} - -type SolanaTokenAmount struct { - Token ag_solanago.PublicKey - Amount uint64 -} - -func (obj SolanaTokenAmount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `Token` param: - err = encoder.Encode(obj.Token) - if err != nil { - return err - } - // Serialize `Amount` param: - err = encoder.Encode(obj.Amount) - if err != nil { - return err - } - return nil -} - -func (obj *SolanaTokenAmount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `Token`: - err = decoder.Decode(&obj.Token) - if err != nil { - return err - } - // Deserialize `Amount`: - err = decoder.Decode(&obj.Amount) - if err != nil { - return err - } - return nil -} - -type ExtraArgsInput struct { - GasLimit *ag_binary.Uint128 `bin:"optional"` - AllowOutOfOrderExecution *bool `bin:"optional"` -} - -func (obj ExtraArgsInput) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `GasLimit` param (optional): - { - if obj.GasLimit == nil { - err = encoder.WriteBool(false) - if err != nil { - return err - } - } else { - err = encoder.WriteBool(true) - if err != nil { - return err - } - err = encoder.Encode(obj.GasLimit) - if err != nil { - return err - } - } - } - // Serialize `AllowOutOfOrderExecution` param (optional): - { - if obj.AllowOutOfOrderExecution == nil { - err = encoder.WriteBool(false) - if err != nil { - return err - } - } else { - err = encoder.WriteBool(true) - if err != nil { - return err - } - err = encoder.Encode(obj.AllowOutOfOrderExecution) - if err != nil { - return err - } - } - } - return nil -} - -func (obj *ExtraArgsInput) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `GasLimit` (optional): - { - ok, err := decoder.ReadBool() - if err != nil { - return err - } - if ok { - err = decoder.Decode(&obj.GasLimit) - if err != nil { - return err - } - } - } - // Deserialize `AllowOutOfOrderExecution` (optional): - { - ok, err := decoder.ReadBool() - if err != nil { - return err - } - if ok { - err = decoder.Decode(&obj.AllowOutOfOrderExecution) - if err != nil { - return err - } - } - } - return nil -} - -type Any2SolanaMessage struct { - MessageId [32]uint8 - SourceChainSelector uint64 - Sender []byte - Data []byte - TokenAmounts []SolanaTokenAmount -} - -func (obj Any2SolanaMessage) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `MessageId` param: - err = encoder.Encode(obj.MessageId) - if err != nil { - return err - } - // Serialize `SourceChainSelector` param: - err = encoder.Encode(obj.SourceChainSelector) - if err != nil { - return err - } - // Serialize `Sender` param: - err = encoder.Encode(obj.Sender) - if err != nil { - return err - } - // Serialize `Data` param: - err = encoder.Encode(obj.Data) - if err != nil { - return err - } - // Serialize `TokenAmounts` param: - err = encoder.Encode(obj.TokenAmounts) - if err != nil { - return err - } - return nil -} - -func (obj *Any2SolanaMessage) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `MessageId`: - err = decoder.Decode(&obj.MessageId) - if err != nil { - return err - } - // Deserialize `SourceChainSelector`: - err = decoder.Decode(&obj.SourceChainSelector) - if err != nil { - return err - } - // Deserialize `Sender`: - err = decoder.Decode(&obj.Sender) - if err != nil { - return err - } - // Deserialize `Data`: - err = decoder.Decode(&obj.Data) - if err != nil { - return err - } - // Deserialize `TokenAmounts`: - err = decoder.Decode(&obj.TokenAmounts) - if err != nil { - return err - } - return nil -} - type RampMessageHeader struct { MessageId [32]uint8 SourceChainSelector uint64 @@ -665,12 +414,12 @@ func (obj *SolanaExtraArgs) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (er return nil } -type EvmExtraArgs struct { +type AnyExtraArgs struct { GasLimit ag_binary.Uint128 AllowOutOfOrderExecution bool } -func (obj EvmExtraArgs) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { +func (obj AnyExtraArgs) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { // Serialize `GasLimit` param: err = encoder.Encode(obj.GasLimit) if err != nil { @@ -684,7 +433,7 @@ func (obj EvmExtraArgs) MarshalWithEncoder(encoder *ag_binary.Encoder) (err erro return nil } -func (obj *EvmExtraArgs) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { +func (obj *AnyExtraArgs) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { // Deserialize `GasLimit`: err = decoder.Decode(&obj.GasLimit) if err != nil { @@ -780,7 +529,7 @@ type Solana2AnyRampMessage struct { Sender ag_solanago.PublicKey Data []byte Receiver []byte - ExtraArgs EvmExtraArgs + ExtraArgs AnyExtraArgs FeeToken ag_solanago.PublicKey TokenAmounts []Solana2AnyTokenTransfer } @@ -1220,11 +969,262 @@ func (obj *ReleaseOrMintOutV1) UnmarshalWithDecoder(decoder *ag_binary.Decoder) return nil } -type ReportContext struct { - ByteWords [3][32]uint8 -} - -func (obj ReportContext) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { +type Solana2AnyMessage struct { + Receiver []byte + Data []byte + TokenAmounts []SolanaTokenAmount + FeeToken ag_solanago.PublicKey + ExtraArgs ExtraArgsInput + TokenIndexes []byte +} + +func (obj Solana2AnyMessage) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `Receiver` param: + err = encoder.Encode(obj.Receiver) + if err != nil { + return err + } + // Serialize `Data` param: + err = encoder.Encode(obj.Data) + if err != nil { + return err + } + // Serialize `TokenAmounts` param: + err = encoder.Encode(obj.TokenAmounts) + if err != nil { + return err + } + // Serialize `FeeToken` param: + err = encoder.Encode(obj.FeeToken) + if err != nil { + return err + } + // Serialize `ExtraArgs` param: + err = encoder.Encode(obj.ExtraArgs) + if err != nil { + return err + } + // Serialize `TokenIndexes` param: + err = encoder.Encode(obj.TokenIndexes) + if err != nil { + return err + } + return nil +} + +func (obj *Solana2AnyMessage) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `Receiver`: + err = decoder.Decode(&obj.Receiver) + if err != nil { + return err + } + // Deserialize `Data`: + err = decoder.Decode(&obj.Data) + if err != nil { + return err + } + // Deserialize `TokenAmounts`: + err = decoder.Decode(&obj.TokenAmounts) + if err != nil { + return err + } + // Deserialize `FeeToken`: + err = decoder.Decode(&obj.FeeToken) + if err != nil { + return err + } + // Deserialize `ExtraArgs`: + err = decoder.Decode(&obj.ExtraArgs) + if err != nil { + return err + } + // Deserialize `TokenIndexes`: + err = decoder.Decode(&obj.TokenIndexes) + if err != nil { + return err + } + return nil +} + +type SolanaTokenAmount struct { + Token ag_solanago.PublicKey + Amount uint64 +} + +func (obj SolanaTokenAmount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `Token` param: + err = encoder.Encode(obj.Token) + if err != nil { + return err + } + // Serialize `Amount` param: + err = encoder.Encode(obj.Amount) + if err != nil { + return err + } + return nil +} + +func (obj *SolanaTokenAmount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `Token`: + err = decoder.Decode(&obj.Token) + if err != nil { + return err + } + // Deserialize `Amount`: + err = decoder.Decode(&obj.Amount) + if err != nil { + return err + } + return nil +} + +type ExtraArgsInput struct { + GasLimit *ag_binary.Uint128 `bin:"optional"` + AllowOutOfOrderExecution *bool `bin:"optional"` +} + +func (obj ExtraArgsInput) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `GasLimit` param (optional): + { + if obj.GasLimit == nil { + err = encoder.WriteBool(false) + if err != nil { + return err + } + } else { + err = encoder.WriteBool(true) + if err != nil { + return err + } + err = encoder.Encode(obj.GasLimit) + if err != nil { + return err + } + } + } + // Serialize `AllowOutOfOrderExecution` param (optional): + { + if obj.AllowOutOfOrderExecution == nil { + err = encoder.WriteBool(false) + if err != nil { + return err + } + } else { + err = encoder.WriteBool(true) + if err != nil { + return err + } + err = encoder.Encode(obj.AllowOutOfOrderExecution) + if err != nil { + return err + } + } + } + return nil +} + +func (obj *ExtraArgsInput) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `GasLimit` (optional): + { + ok, err := decoder.ReadBool() + if err != nil { + return err + } + if ok { + err = decoder.Decode(&obj.GasLimit) + if err != nil { + return err + } + } + } + // Deserialize `AllowOutOfOrderExecution` (optional): + { + ok, err := decoder.ReadBool() + if err != nil { + return err + } + if ok { + err = decoder.Decode(&obj.AllowOutOfOrderExecution) + if err != nil { + return err + } + } + } + return nil +} + +type Any2SolanaMessage struct { + MessageId [32]uint8 + SourceChainSelector uint64 + Sender []byte + Data []byte + TokenAmounts []SolanaTokenAmount +} + +func (obj Any2SolanaMessage) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `MessageId` param: + err = encoder.Encode(obj.MessageId) + if err != nil { + return err + } + // Serialize `SourceChainSelector` param: + err = encoder.Encode(obj.SourceChainSelector) + if err != nil { + return err + } + // Serialize `Sender` param: + err = encoder.Encode(obj.Sender) + if err != nil { + return err + } + // Serialize `Data` param: + err = encoder.Encode(obj.Data) + if err != nil { + return err + } + // Serialize `TokenAmounts` param: + err = encoder.Encode(obj.TokenAmounts) + if err != nil { + return err + } + return nil +} + +func (obj *Any2SolanaMessage) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `MessageId`: + err = decoder.Decode(&obj.MessageId) + if err != nil { + return err + } + // Deserialize `SourceChainSelector`: + err = decoder.Decode(&obj.SourceChainSelector) + if err != nil { + return err + } + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return err + } + // Deserialize `Data`: + err = decoder.Decode(&obj.Data) + if err != nil { + return err + } + // Deserialize `TokenAmounts`: + err = decoder.Decode(&obj.TokenAmounts) + if err != nil { + return err + } + return nil +} + +type ReportContext struct { + ByteWords [3][32]uint8 +} + +func (obj ReportContext) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { // Serialize `ByteWords` param: err = encoder.Encode(obj.ByteWords) if err != nil { @@ -1407,39 +1407,6 @@ func (obj *SourceChainState) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (e return nil } -type SourceChain struct { - State SourceChainState - Config SourceChainConfig -} - -func (obj SourceChain) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `State` param: - err = encoder.Encode(obj.State) - if err != nil { - return err - } - // Serialize `Config` param: - err = encoder.Encode(obj.Config) - if err != nil { - return err - } - return nil -} - -func (obj *SourceChain) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `State`: - err = decoder.Decode(&obj.State) - if err != nil { - return err - } - // Deserialize `Config`: - err = decoder.Decode(&obj.Config) - if err != nil { - return err - } - return nil -} - type DestChainState struct { SequenceNumber uint64 UsdPerUnitGas TimestampedPackedU224 @@ -1671,39 +1638,6 @@ func (obj *DestChainConfig) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (er return nil } -type DestChain struct { - State DestChainState - Config DestChainConfig -} - -func (obj DestChain) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { - // Serialize `State` param: - err = encoder.Encode(obj.State) - if err != nil { - return err - } - // Serialize `Config` param: - err = encoder.Encode(obj.Config) - if err != nil { - return err - } - return nil -} - -func (obj *DestChain) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { - // Deserialize `State`: - err = decoder.Decode(&obj.State) - if err != nil { - return err - } - // Deserialize `Config`: - err = decoder.Decode(&obj.Config) - if err != nil { - return err - } - return nil -} - type TokenBilling struct { MinFeeUsdcents uint32 MaxFeeUsdcents uint32 @@ -2018,6 +1952,16 @@ const ( StaleCommitReport_CcipRouterError DestinationChainDisabled_CcipRouterError FeeTokenDisabled_CcipRouterError + MessageTooLarge_CcipRouterError + UnsupportedNumberOfTokens_CcipRouterError + UnsupportedChainFamilySelector_CcipRouterError + InvalidEVMAddress_CcipRouterError + InvalidEncoding_CcipRouterError + InvalidInputsAtaAddress_CcipRouterError + InvalidInputsAtaWritable_CcipRouterError + InvalidTokenPrice_CcipRouterError + StaleGasPrice_CcipRouterError + InsufficientLamports_CcipRouterError ) func (value CcipRouterError) String() string { @@ -2068,6 +2012,26 @@ func (value CcipRouterError) String() string { return "DestinationChainDisabled" case FeeTokenDisabled_CcipRouterError: return "FeeTokenDisabled" + case MessageTooLarge_CcipRouterError: + return "MessageTooLarge" + case UnsupportedNumberOfTokens_CcipRouterError: + return "UnsupportedNumberOfTokens" + case UnsupportedChainFamilySelector_CcipRouterError: + return "UnsupportedChainFamilySelector" + case InvalidEVMAddress_CcipRouterError: + return "InvalidEVMAddress" + case InvalidEncoding_CcipRouterError: + return "InvalidEncoding" + case InvalidInputsAtaAddress_CcipRouterError: + return "InvalidInputsAtaAddress" + case InvalidInputsAtaWritable_CcipRouterError: + return "InvalidInputsAtaWritable" + case InvalidTokenPrice_CcipRouterError: + return "InvalidTokenPrice" + case StaleGasPrice_CcipRouterError: + return "StaleGasPrice" + case InsufficientLamports_CcipRouterError: + return "InsufficientLamports" default: return "" } diff --git a/chains/solana/gobindings/mcm/accounts.go b/chains/solana/gobindings/mcm/accounts.go index 8c4d06ae..f7bb2254 100644 --- a/chains/solana/gobindings/mcm/accounts.go +++ b/chains/solana/gobindings/mcm/accounts.go @@ -12,7 +12,6 @@ type ConfigSigners struct { SignerAddresses [][20]uint8 TotalSigners uint8 IsFinalized bool - Bump uint8 } var ConfigSignersDiscriminator = [8]byte{147, 137, 80, 98, 50, 225, 190, 163} @@ -38,11 +37,6 @@ func (obj ConfigSigners) MarshalWithEncoder(encoder *ag_binary.Encoder) (err err if err != nil { return err } - // Serialize `Bump` param: - err = encoder.Encode(obj.Bump) - if err != nil { - return err - } return nil } @@ -75,11 +69,6 @@ func (obj *ConfigSigners) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err if err != nil { return err } - // Deserialize `Bump`: - err = decoder.Decode(&obj.Bump) - if err != nil { - return err - } return nil } @@ -195,7 +184,6 @@ type RootSignatures struct { TotalSignatures uint8 Signatures []Signature IsFinalized bool - Bump uint8 } var RootSignaturesDiscriminator = [8]byte{21, 186, 10, 33, 117, 215, 246, 76} @@ -221,11 +209,6 @@ func (obj RootSignatures) MarshalWithEncoder(encoder *ag_binary.Encoder) (err er if err != nil { return err } - // Serialize `Bump` param: - err = encoder.Encode(obj.Bump) - if err != nil { - return err - } return nil } @@ -258,11 +241,6 @@ func (obj *RootSignatures) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err if err != nil { return err } - // Deserialize `Bump`: - err = decoder.Decode(&obj.Bump) - if err != nil { - return err - } return nil } diff --git a/chains/solana/gobindings/mcm/types.go b/chains/solana/gobindings/mcm/types.go index e5824756..26fbbd13 100644 --- a/chains/solana/gobindings/mcm/types.go +++ b/chains/solana/gobindings/mcm/types.go @@ -160,3 +160,117 @@ func (obj *RootMetadataInput) UnmarshalWithDecoder(decoder *ag_binary.Decoder) ( } return nil } + +type McmError ag_binary.BorshEnum + +const ( + InvalidInputs_McmError McmError = iota + Overflow_McmError + WrongMultiSig_McmError + WrongChainId_McmError + InvalidSignature_McmError + FailedEcdsaRecover_McmError + InvalidRootLen_McmError + SignersNotFinalized_McmError + SignersAlreadyFinalized_McmError + SignaturesAlreadyFinalized_McmError + SignatureCountMismatch_McmError + TooManySignatures_McmError + SignaturesNotFinalized_McmError + SignaturesRootMismatch_McmError + SignaturesValidUntilMismatch_McmError + MismatchedInputSignerVectorsLength_McmError + OutOfBoundsNumOfSigners_McmError + MismatchedInputGroupArraysLength_McmError + GroupTreeNotWellFormed_McmError + SignerInDisabledGroup_McmError + OutOfBoundsGroupQuorum_McmError + SignersAddressesMustBeStrictlyIncreasing_McmError + SignedHashAlreadySeen_McmError + InvalidSigner_McmError + MissingConfig_McmError + InsufficientSigners_McmError + ValidUntilHasAlreadyPassed_McmError + ProofCannotBeVerified_McmError + PendingOps_McmError + WrongPreOpCount_McmError + WrongPostOpCount_McmError + PostOpCountReached_McmError + RootExpired_McmError + WrongNonce_McmError +) + +func (value McmError) String() string { + switch value { + case InvalidInputs_McmError: + return "InvalidInputs" + case Overflow_McmError: + return "Overflow" + case WrongMultiSig_McmError: + return "WrongMultiSig" + case WrongChainId_McmError: + return "WrongChainId" + case InvalidSignature_McmError: + return "InvalidSignature" + case FailedEcdsaRecover_McmError: + return "FailedEcdsaRecover" + case InvalidRootLen_McmError: + return "InvalidRootLen" + case SignersNotFinalized_McmError: + return "SignersNotFinalized" + case SignersAlreadyFinalized_McmError: + return "SignersAlreadyFinalized" + case SignaturesAlreadyFinalized_McmError: + return "SignaturesAlreadyFinalized" + case SignatureCountMismatch_McmError: + return "SignatureCountMismatch" + case TooManySignatures_McmError: + return "TooManySignatures" + case SignaturesNotFinalized_McmError: + return "SignaturesNotFinalized" + case SignaturesRootMismatch_McmError: + return "SignaturesRootMismatch" + case SignaturesValidUntilMismatch_McmError: + return "SignaturesValidUntilMismatch" + case MismatchedInputSignerVectorsLength_McmError: + return "MismatchedInputSignerVectorsLength" + case OutOfBoundsNumOfSigners_McmError: + return "OutOfBoundsNumOfSigners" + case MismatchedInputGroupArraysLength_McmError: + return "MismatchedInputGroupArraysLength" + case GroupTreeNotWellFormed_McmError: + return "GroupTreeNotWellFormed" + case SignerInDisabledGroup_McmError: + return "SignerInDisabledGroup" + case OutOfBoundsGroupQuorum_McmError: + return "OutOfBoundsGroupQuorum" + case SignersAddressesMustBeStrictlyIncreasing_McmError: + return "SignersAddressesMustBeStrictlyIncreasing" + case SignedHashAlreadySeen_McmError: + return "SignedHashAlreadySeen" + case InvalidSigner_McmError: + return "InvalidSigner" + case MissingConfig_McmError: + return "MissingConfig" + case InsufficientSigners_McmError: + return "InsufficientSigners" + case ValidUntilHasAlreadyPassed_McmError: + return "ValidUntilHasAlreadyPassed" + case ProofCannotBeVerified_McmError: + return "ProofCannotBeVerified" + case PendingOps_McmError: + return "PendingOps" + case WrongPreOpCount_McmError: + return "WrongPreOpCount" + case WrongPostOpCount_McmError: + return "WrongPostOpCount" + case PostOpCountReached_McmError: + return "PostOpCountReached" + case RootExpired_McmError: + return "RootExpired" + case WrongNonce_McmError: + return "WrongNonce" + default: + return "" + } +} diff --git a/chains/solana/gobindings/timelock/AppendInstructions.go b/chains/solana/gobindings/timelock/AppendInstructions.go index f92f1c1c..e2b0f8b9 100644 --- a/chains/solana/gobindings/timelock/AppendInstructions.go +++ b/chains/solana/gobindings/timelock/AppendInstructions.go @@ -17,16 +17,18 @@ type AppendInstructions struct { // [0] = [WRITE] operation // - // [1] = [WRITE, SIGNER] authority + // [1] = [] config // - // [2] = [] systemProgram + // [2] = [WRITE, SIGNER] authority + // + // [3] = [] systemProgram ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewAppendInstructionsInstructionBuilder creates a new `AppendInstructions` instruction builder. func NewAppendInstructionsInstructionBuilder() *AppendInstructions { nd := &AppendInstructions{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 4), } return nd } @@ -54,26 +56,37 @@ func (inst *AppendInstructions) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetConfigAccount sets the "config" account. +func (inst *AppendInstructions) SetConfigAccount(config ag_solanago.PublicKey) *AppendInstructions { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) + return inst +} + +// GetConfigAccount gets the "config" account. +func (inst *AppendInstructions) GetConfigAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *AppendInstructions) SetAuthorityAccount(authority ag_solanago.PublicKey) *AppendInstructions { - inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).WRITE().SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).WRITE().SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *AppendInstructions) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } // SetSystemProgramAccount sets the "systemProgram" account. func (inst *AppendInstructions) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *AppendInstructions { - inst.AccountMetaSlice[2] = ag_solanago.Meta(systemProgram) + inst.AccountMetaSlice[3] = ag_solanago.Meta(systemProgram) return inst } // GetSystemProgramAccount gets the "systemProgram" account. func (inst *AppendInstructions) GetSystemProgramAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[2] + return inst.AccountMetaSlice[3] } func (inst AppendInstructions) Build() *Instruction { @@ -110,9 +123,12 @@ func (inst *AppendInstructions) Validate() error { return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Authority is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[2] == nil { + return errors.New("accounts.Authority is not set") + } + if inst.AccountMetaSlice[3] == nil { return errors.New("accounts.SystemProgram is not set") } } @@ -134,10 +150,11 @@ func (inst *AppendInstructions) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[3])) }) }) }) @@ -177,12 +194,14 @@ func NewAppendInstructionsInstruction( instructionsBatch []InstructionData, // Accounts: operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, authority ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey) *AppendInstructions { return NewAppendInstructionsInstructionBuilder(). SetId(id). SetInstructionsBatch(instructionsBatch). SetOperationAccount(operation). + SetConfigAccount(config). SetAuthorityAccount(authority). SetSystemProgramAccount(systemProgram) } diff --git a/chains/solana/gobindings/timelock/BlockFunctionSelector.go b/chains/solana/gobindings/timelock/BlockFunctionSelector.go index 9723f9fa..dad390be 100644 --- a/chains/solana/gobindings/timelock/BlockFunctionSelector.go +++ b/chains/solana/gobindings/timelock/BlockFunctionSelector.go @@ -14,7 +14,7 @@ import ( type BlockFunctionSelector struct { Selector *[8]uint8 - // [0] = [] config + // [0] = [WRITE] config // // [1] = [SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` @@ -36,7 +36,7 @@ func (inst *BlockFunctionSelector) SetSelector(selector [8]uint8) *BlockFunction // SetConfigAccount sets the "config" account. func (inst *BlockFunctionSelector) SetConfigAccount(config ag_solanago.PublicKey) *BlockFunctionSelector { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) + inst.AccountMetaSlice[0] = ag_solanago.Meta(config).WRITE() return inst } diff --git a/chains/solana/gobindings/timelock/BypasserExecuteBatch.go b/chains/solana/gobindings/timelock/BypasserExecuteBatch.go index 6c7b7381..3a07483f 100644 --- a/chains/solana/gobindings/timelock/BypasserExecuteBatch.go +++ b/chains/solana/gobindings/timelock/BypasserExecuteBatch.go @@ -14,11 +14,11 @@ import ( type BypasserExecuteBatch struct { Id *[32]uint8 - // [0] = [] config + // [0] = [WRITE] operation // - // [1] = [] timelockSigner + // [1] = [] config // - // [2] = [WRITE] operation + // [2] = [] timelockSigner // // [3] = [] roleAccessController // @@ -40,36 +40,36 @@ func (inst *BypasserExecuteBatch) SetId(id [32]uint8) *BypasserExecuteBatch { return inst } +// SetOperationAccount sets the "operation" account. +func (inst *BypasserExecuteBatch) SetOperationAccount(operation ag_solanago.PublicKey) *BypasserExecuteBatch { + inst.AccountMetaSlice[0] = ag_solanago.Meta(operation).WRITE() + return inst +} + +// GetOperationAccount gets the "operation" account. +func (inst *BypasserExecuteBatch) GetOperationAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + // SetConfigAccount sets the "config" account. func (inst *BypasserExecuteBatch) SetConfigAccount(config ag_solanago.PublicKey) *BypasserExecuteBatch { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) return inst } // GetConfigAccount gets the "config" account. func (inst *BypasserExecuteBatch) GetConfigAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[0] + return inst.AccountMetaSlice[1] } // SetTimelockSignerAccount sets the "timelockSigner" account. func (inst *BypasserExecuteBatch) SetTimelockSignerAccount(timelockSigner ag_solanago.PublicKey) *BypasserExecuteBatch { - inst.AccountMetaSlice[1] = ag_solanago.Meta(timelockSigner) + inst.AccountMetaSlice[2] = ag_solanago.Meta(timelockSigner) return inst } // GetTimelockSignerAccount gets the "timelockSigner" account. func (inst *BypasserExecuteBatch) GetTimelockSignerAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] -} - -// SetOperationAccount sets the "operation" account. -func (inst *BypasserExecuteBatch) SetOperationAccount(operation ag_solanago.PublicKey) *BypasserExecuteBatch { - inst.AccountMetaSlice[2] = ag_solanago.Meta(operation).WRITE() - return inst -} - -// GetOperationAccount gets the "operation" account. -func (inst *BypasserExecuteBatch) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[2] } @@ -123,13 +123,13 @@ func (inst *BypasserExecuteBatch) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.TimelockSigner is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[2] == nil { - return errors.New("accounts.Operation is not set") + return errors.New("accounts.TimelockSigner is not set") } if inst.AccountMetaSlice[3] == nil { return errors.New("accounts.RoleAccessController is not set") @@ -156,9 +156,9 @@ func (inst *BypasserExecuteBatch) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=5]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" timelockSigner", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" timelockSigner", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta("roleAccessController", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[4])) }) @@ -188,16 +188,16 @@ func NewBypasserExecuteBatchInstruction( // Parameters: id [32]uint8, // Accounts: + operation ag_solanago.PublicKey, config ag_solanago.PublicKey, timelockSigner ag_solanago.PublicKey, - operation ag_solanago.PublicKey, roleAccessController ag_solanago.PublicKey, authority ag_solanago.PublicKey) *BypasserExecuteBatch { return NewBypasserExecuteBatchInstructionBuilder(). SetId(id). + SetOperationAccount(operation). SetConfigAccount(config). SetTimelockSignerAccount(timelockSigner). - SetOperationAccount(operation). SetRoleAccessControllerAccount(roleAccessController). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/Cancel.go b/chains/solana/gobindings/timelock/Cancel.go index 32444c6f..4970af81 100644 --- a/chains/solana/gobindings/timelock/Cancel.go +++ b/chains/solana/gobindings/timelock/Cancel.go @@ -14,9 +14,9 @@ import ( type Cancel struct { Id *[32]uint8 - // [0] = [] config + // [0] = [WRITE] operation // - // [1] = [WRITE] operation + // [1] = [] config // // [2] = [] roleAccessController // @@ -38,25 +38,25 @@ func (inst *Cancel) SetId(id [32]uint8) *Cancel { return inst } -// SetConfigAccount sets the "config" account. -func (inst *Cancel) SetConfigAccount(config ag_solanago.PublicKey) *Cancel { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) +// SetOperationAccount sets the "operation" account. +func (inst *Cancel) SetOperationAccount(operation ag_solanago.PublicKey) *Cancel { + inst.AccountMetaSlice[0] = ag_solanago.Meta(operation).WRITE() return inst } -// GetConfigAccount gets the "config" account. -func (inst *Cancel) GetConfigAccount() *ag_solanago.AccountMeta { +// GetOperationAccount gets the "operation" account. +func (inst *Cancel) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetOperationAccount sets the "operation" account. -func (inst *Cancel) SetOperationAccount(operation ag_solanago.PublicKey) *Cancel { - inst.AccountMetaSlice[1] = ag_solanago.Meta(operation).WRITE() +// SetConfigAccount sets the "config" account. +func (inst *Cancel) SetConfigAccount(config ag_solanago.PublicKey) *Cancel { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) return inst } -// GetOperationAccount gets the "operation" account. -func (inst *Cancel) GetOperationAccount() *ag_solanago.AccountMeta { +// GetConfigAccount gets the "config" account. +func (inst *Cancel) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -110,10 +110,10 @@ func (inst *Cancel) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Operation is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.RoleAccessController is not set") @@ -140,8 +140,8 @@ func (inst *Cancel) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta("roleAccessController", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[3])) }) @@ -171,14 +171,14 @@ func NewCancelInstruction( // Parameters: id [32]uint8, // Accounts: - config ag_solanago.PublicKey, operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, roleAccessController ag_solanago.PublicKey, authority ag_solanago.PublicKey) *Cancel { return NewCancelInstructionBuilder(). SetId(id). - SetConfigAccount(config). SetOperationAccount(operation). + SetConfigAccount(config). SetRoleAccessControllerAccount(roleAccessController). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/ClearOperation.go b/chains/solana/gobindings/timelock/ClearOperation.go index 6226b697..8ef60bd3 100644 --- a/chains/solana/gobindings/timelock/ClearOperation.go +++ b/chains/solana/gobindings/timelock/ClearOperation.go @@ -16,14 +16,16 @@ type ClearOperation struct { // [0] = [WRITE] operation // - // [1] = [WRITE, SIGNER] authority + // [1] = [] config + // + // [2] = [WRITE, SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewClearOperationInstructionBuilder creates a new `ClearOperation` instruction builder. func NewClearOperationInstructionBuilder() *ClearOperation { nd := &ClearOperation{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), } return nd } @@ -45,15 +47,26 @@ func (inst *ClearOperation) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetConfigAccount sets the "config" account. +func (inst *ClearOperation) SetConfigAccount(config ag_solanago.PublicKey) *ClearOperation { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) + return inst +} + +// GetConfigAccount gets the "config" account. +func (inst *ClearOperation) GetConfigAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *ClearOperation) SetAuthorityAccount(authority ag_solanago.PublicKey) *ClearOperation { - inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).WRITE().SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).WRITE().SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *ClearOperation) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } func (inst ClearOperation) Build() *Instruction { @@ -87,6 +100,9 @@ func (inst *ClearOperation) Validate() error { return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Config is not set") + } + if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Authority is not set") } } @@ -107,9 +123,10 @@ func (inst *ClearOperation) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta("operation", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[2])) }) }) }) @@ -138,9 +155,11 @@ func NewClearOperationInstruction( id [32]uint8, // Accounts: operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *ClearOperation { return NewClearOperationInstructionBuilder(). SetId(id). SetOperationAccount(operation). + SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/ExecuteBatch.go b/chains/solana/gobindings/timelock/ExecuteBatch.go index 6dd8326b..76b97b14 100644 --- a/chains/solana/gobindings/timelock/ExecuteBatch.go +++ b/chains/solana/gobindings/timelock/ExecuteBatch.go @@ -14,13 +14,13 @@ import ( type ExecuteBatch struct { Id *[32]uint8 - // [0] = [] config + // [0] = [WRITE] operation // - // [1] = [] timelockSigner + // [1] = [] predecessorOperation // - // [2] = [WRITE] operation + // [2] = [] config // - // [3] = [] predecessorOperation + // [3] = [] timelockSigner // // [4] = [] roleAccessController // @@ -42,47 +42,47 @@ func (inst *ExecuteBatch) SetId(id [32]uint8) *ExecuteBatch { return inst } -// SetConfigAccount sets the "config" account. -func (inst *ExecuteBatch) SetConfigAccount(config ag_solanago.PublicKey) *ExecuteBatch { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) +// SetOperationAccount sets the "operation" account. +func (inst *ExecuteBatch) SetOperationAccount(operation ag_solanago.PublicKey) *ExecuteBatch { + inst.AccountMetaSlice[0] = ag_solanago.Meta(operation).WRITE() return inst } -// GetConfigAccount gets the "config" account. -func (inst *ExecuteBatch) GetConfigAccount() *ag_solanago.AccountMeta { +// GetOperationAccount gets the "operation" account. +func (inst *ExecuteBatch) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetTimelockSignerAccount sets the "timelockSigner" account. -func (inst *ExecuteBatch) SetTimelockSignerAccount(timelockSigner ag_solanago.PublicKey) *ExecuteBatch { - inst.AccountMetaSlice[1] = ag_solanago.Meta(timelockSigner) +// SetPredecessorOperationAccount sets the "predecessorOperation" account. +func (inst *ExecuteBatch) SetPredecessorOperationAccount(predecessorOperation ag_solanago.PublicKey) *ExecuteBatch { + inst.AccountMetaSlice[1] = ag_solanago.Meta(predecessorOperation) return inst } -// GetTimelockSignerAccount gets the "timelockSigner" account. -func (inst *ExecuteBatch) GetTimelockSignerAccount() *ag_solanago.AccountMeta { +// GetPredecessorOperationAccount gets the "predecessorOperation" account. +func (inst *ExecuteBatch) GetPredecessorOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } -// SetOperationAccount sets the "operation" account. -func (inst *ExecuteBatch) SetOperationAccount(operation ag_solanago.PublicKey) *ExecuteBatch { - inst.AccountMetaSlice[2] = ag_solanago.Meta(operation).WRITE() +// SetConfigAccount sets the "config" account. +func (inst *ExecuteBatch) SetConfigAccount(config ag_solanago.PublicKey) *ExecuteBatch { + inst.AccountMetaSlice[2] = ag_solanago.Meta(config) return inst } -// GetOperationAccount gets the "operation" account. -func (inst *ExecuteBatch) GetOperationAccount() *ag_solanago.AccountMeta { +// GetConfigAccount gets the "config" account. +func (inst *ExecuteBatch) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[2] } -// SetPredecessorOperationAccount sets the "predecessorOperation" account. -func (inst *ExecuteBatch) SetPredecessorOperationAccount(predecessorOperation ag_solanago.PublicKey) *ExecuteBatch { - inst.AccountMetaSlice[3] = ag_solanago.Meta(predecessorOperation) +// SetTimelockSignerAccount sets the "timelockSigner" account. +func (inst *ExecuteBatch) SetTimelockSignerAccount(timelockSigner ag_solanago.PublicKey) *ExecuteBatch { + inst.AccountMetaSlice[3] = ag_solanago.Meta(timelockSigner) return inst } -// GetPredecessorOperationAccount gets the "predecessorOperation" account. -func (inst *ExecuteBatch) GetPredecessorOperationAccount() *ag_solanago.AccountMeta { +// GetTimelockSignerAccount gets the "timelockSigner" account. +func (inst *ExecuteBatch) GetTimelockSignerAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[3] } @@ -136,16 +136,16 @@ func (inst *ExecuteBatch) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.TimelockSigner is not set") + return errors.New("accounts.PredecessorOperation is not set") } if inst.AccountMetaSlice[2] == nil { - return errors.New("accounts.Operation is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[3] == nil { - return errors.New("accounts.PredecessorOperation is not set") + return errors.New("accounts.TimelockSigner is not set") } if inst.AccountMetaSlice[4] == nil { return errors.New("accounts.RoleAccessController is not set") @@ -172,10 +172,10 @@ func (inst *ExecuteBatch) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=6]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" timelockSigner", inst.AccountMetaSlice[1])) - accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[2])) - accountsBranch.Child(ag_format.Meta("predecessorOperation", inst.AccountMetaSlice[3])) + accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta("predecessorOperation", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta(" timelockSigner", inst.AccountMetaSlice[3])) accountsBranch.Child(ag_format.Meta("roleAccessController", inst.AccountMetaSlice[4])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[5])) }) @@ -205,18 +205,18 @@ func NewExecuteBatchInstruction( // Parameters: id [32]uint8, // Accounts: - config ag_solanago.PublicKey, - timelockSigner ag_solanago.PublicKey, operation ag_solanago.PublicKey, predecessorOperation ag_solanago.PublicKey, + config ag_solanago.PublicKey, + timelockSigner ag_solanago.PublicKey, roleAccessController ag_solanago.PublicKey, authority ag_solanago.PublicKey) *ExecuteBatch { return NewExecuteBatchInstructionBuilder(). SetId(id). - SetConfigAccount(config). - SetTimelockSignerAccount(timelockSigner). SetOperationAccount(operation). SetPredecessorOperationAccount(predecessorOperation). + SetConfigAccount(config). + SetTimelockSignerAccount(timelockSigner). SetRoleAccessControllerAccount(roleAccessController). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/FinalizeOperation.go b/chains/solana/gobindings/timelock/FinalizeOperation.go index 8ea31911..2387ef65 100644 --- a/chains/solana/gobindings/timelock/FinalizeOperation.go +++ b/chains/solana/gobindings/timelock/FinalizeOperation.go @@ -16,14 +16,16 @@ type FinalizeOperation struct { // [0] = [WRITE] operation // - // [1] = [WRITE, SIGNER] authority + // [1] = [] config + // + // [2] = [WRITE, SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewFinalizeOperationInstructionBuilder creates a new `FinalizeOperation` instruction builder. func NewFinalizeOperationInstructionBuilder() *FinalizeOperation { nd := &FinalizeOperation{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), } return nd } @@ -45,15 +47,26 @@ func (inst *FinalizeOperation) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetConfigAccount sets the "config" account. +func (inst *FinalizeOperation) SetConfigAccount(config ag_solanago.PublicKey) *FinalizeOperation { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) + return inst +} + +// GetConfigAccount gets the "config" account. +func (inst *FinalizeOperation) GetConfigAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *FinalizeOperation) SetAuthorityAccount(authority ag_solanago.PublicKey) *FinalizeOperation { - inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).WRITE().SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).WRITE().SIGNER() return inst } // GetAuthorityAccount gets the "authority" account. func (inst *FinalizeOperation) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } func (inst FinalizeOperation) Build() *Instruction { @@ -87,6 +100,9 @@ func (inst *FinalizeOperation) Validate() error { return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Config is not set") + } + if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Authority is not set") } } @@ -107,9 +123,10 @@ func (inst *FinalizeOperation) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=2]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { accountsBranch.Child(ag_format.Meta("operation", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[2])) }) }) }) @@ -138,9 +155,11 @@ func NewFinalizeOperationInstruction( id [32]uint8, // Accounts: operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, authority ag_solanago.PublicKey) *FinalizeOperation { return NewFinalizeOperationInstructionBuilder(). SetId(id). SetOperationAccount(operation). + SetConfigAccount(config). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/InitializeOperation.go b/chains/solana/gobindings/timelock/InitializeOperation.go index 0be4f9ae..61dead7e 100644 --- a/chains/solana/gobindings/timelock/InitializeOperation.go +++ b/chains/solana/gobindings/timelock/InitializeOperation.go @@ -17,22 +17,20 @@ type InitializeOperation struct { Salt *[32]uint8 InstructionCount *uint32 - // [0] = [] config + // [0] = [WRITE] operation // - // [1] = [WRITE] operation + // [1] = [] config // // [2] = [WRITE, SIGNER] authority // - // [3] = [] proposer - // - // [4] = [] systemProgram + // [3] = [] systemProgram ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewInitializeOperationInstructionBuilder creates a new `InitializeOperation` instruction builder. func NewInitializeOperationInstructionBuilder() *InitializeOperation { nd := &InitializeOperation{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 5), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 4), } return nd } @@ -61,25 +59,25 @@ func (inst *InitializeOperation) SetInstructionCount(instructionCount uint32) *I return inst } -// SetConfigAccount sets the "config" account. -func (inst *InitializeOperation) SetConfigAccount(config ag_solanago.PublicKey) *InitializeOperation { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) +// SetOperationAccount sets the "operation" account. +func (inst *InitializeOperation) SetOperationAccount(operation ag_solanago.PublicKey) *InitializeOperation { + inst.AccountMetaSlice[0] = ag_solanago.Meta(operation).WRITE() return inst } -// GetConfigAccount gets the "config" account. -func (inst *InitializeOperation) GetConfigAccount() *ag_solanago.AccountMeta { +// GetOperationAccount gets the "operation" account. +func (inst *InitializeOperation) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetOperationAccount sets the "operation" account. -func (inst *InitializeOperation) SetOperationAccount(operation ag_solanago.PublicKey) *InitializeOperation { - inst.AccountMetaSlice[1] = ag_solanago.Meta(operation).WRITE() +// SetConfigAccount sets the "config" account. +func (inst *InitializeOperation) SetConfigAccount(config ag_solanago.PublicKey) *InitializeOperation { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) return inst } -// GetOperationAccount gets the "operation" account. -func (inst *InitializeOperation) GetOperationAccount() *ag_solanago.AccountMeta { +// GetConfigAccount gets the "config" account. +func (inst *InitializeOperation) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -94,26 +92,15 @@ func (inst *InitializeOperation) GetAuthorityAccount() *ag_solanago.AccountMeta return inst.AccountMetaSlice[2] } -// SetProposerAccount sets the "proposer" account. -func (inst *InitializeOperation) SetProposerAccount(proposer ag_solanago.PublicKey) *InitializeOperation { - inst.AccountMetaSlice[3] = ag_solanago.Meta(proposer) - return inst -} - -// GetProposerAccount gets the "proposer" account. -func (inst *InitializeOperation) GetProposerAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[3] -} - // SetSystemProgramAccount sets the "systemProgram" account. func (inst *InitializeOperation) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *InitializeOperation { - inst.AccountMetaSlice[4] = ag_solanago.Meta(systemProgram) + inst.AccountMetaSlice[3] = ag_solanago.Meta(systemProgram) return inst } // GetSystemProgramAccount gets the "systemProgram" account. func (inst *InitializeOperation) GetSystemProgramAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[4] + return inst.AccountMetaSlice[3] } func (inst InitializeOperation) Build() *Instruction { @@ -153,18 +140,15 @@ func (inst *InitializeOperation) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Operation is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Authority is not set") } if inst.AccountMetaSlice[3] == nil { - return errors.New("accounts.Proposer is not set") - } - if inst.AccountMetaSlice[4] == nil { return errors.New("accounts.SystemProgram is not set") } } @@ -188,12 +172,11 @@ func (inst *InitializeOperation) EncodeToTree(parent ag_treeout.Branches) { }) // Accounts of the instruction: - instructionBranch.Child("Accounts[len=5]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[1])) + instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) - accountsBranch.Child(ag_format.Meta(" proposer", inst.AccountMetaSlice[3])) - accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[4])) + accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[3])) }) }) }) @@ -254,19 +237,17 @@ func NewInitializeOperationInstruction( salt [32]uint8, instructionCount uint32, // Accounts: - config ag_solanago.PublicKey, operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, authority ag_solanago.PublicKey, - proposer ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey) *InitializeOperation { return NewInitializeOperationInstructionBuilder(). SetId(id). SetPredecessor(predecessor). SetSalt(salt). SetInstructionCount(instructionCount). - SetConfigAccount(config). SetOperationAccount(operation). + SetConfigAccount(config). SetAuthorityAccount(authority). - SetProposerAccount(proposer). SetSystemProgramAccount(systemProgram) } diff --git a/chains/solana/gobindings/timelock/ScheduleBatch.go b/chains/solana/gobindings/timelock/ScheduleBatch.go index 3f6a01cc..65de135f 100644 --- a/chains/solana/gobindings/timelock/ScheduleBatch.go +++ b/chains/solana/gobindings/timelock/ScheduleBatch.go @@ -15,9 +15,9 @@ type ScheduleBatch struct { Id *[32]uint8 Delay *uint64 - // [0] = [] config + // [0] = [WRITE] operation // - // [1] = [WRITE] operation + // [1] = [] config // // [2] = [] roleAccessController // @@ -45,25 +45,25 @@ func (inst *ScheduleBatch) SetDelay(delay uint64) *ScheduleBatch { return inst } -// SetConfigAccount sets the "config" account. -func (inst *ScheduleBatch) SetConfigAccount(config ag_solanago.PublicKey) *ScheduleBatch { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) +// SetOperationAccount sets the "operation" account. +func (inst *ScheduleBatch) SetOperationAccount(operation ag_solanago.PublicKey) *ScheduleBatch { + inst.AccountMetaSlice[0] = ag_solanago.Meta(operation).WRITE() return inst } -// GetConfigAccount gets the "config" account. -func (inst *ScheduleBatch) GetConfigAccount() *ag_solanago.AccountMeta { +// GetOperationAccount gets the "operation" account. +func (inst *ScheduleBatch) GetOperationAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetOperationAccount sets the "operation" account. -func (inst *ScheduleBatch) SetOperationAccount(operation ag_solanago.PublicKey) *ScheduleBatch { - inst.AccountMetaSlice[1] = ag_solanago.Meta(operation).WRITE() +// SetConfigAccount sets the "config" account. +func (inst *ScheduleBatch) SetConfigAccount(config ag_solanago.PublicKey) *ScheduleBatch { + inst.AccountMetaSlice[1] = ag_solanago.Meta(config) return inst } -// GetOperationAccount gets the "operation" account. -func (inst *ScheduleBatch) GetOperationAccount() *ag_solanago.AccountMeta { +// GetConfigAccount gets the "config" account. +func (inst *ScheduleBatch) GetConfigAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -120,10 +120,10 @@ func (inst *ScheduleBatch) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Config is not set") + return errors.New("accounts.Operation is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Operation is not set") + return errors.New("accounts.Config is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.RoleAccessController is not set") @@ -151,8 +151,8 @@ func (inst *ScheduleBatch) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" operation", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" config", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta("roleAccessController", inst.AccountMetaSlice[2])) accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[3])) }) @@ -193,15 +193,15 @@ func NewScheduleBatchInstruction( id [32]uint8, delay uint64, // Accounts: - config ag_solanago.PublicKey, operation ag_solanago.PublicKey, + config ag_solanago.PublicKey, roleAccessController ag_solanago.PublicKey, authority ag_solanago.PublicKey) *ScheduleBatch { return NewScheduleBatchInstructionBuilder(). SetId(id). SetDelay(delay). - SetConfigAccount(config). SetOperationAccount(operation). + SetConfigAccount(config). SetRoleAccessControllerAccount(roleAccessController). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/timelock/UnblockFunctionSelector.go b/chains/solana/gobindings/timelock/UnblockFunctionSelector.go index 3ab75140..1e336f4c 100644 --- a/chains/solana/gobindings/timelock/UnblockFunctionSelector.go +++ b/chains/solana/gobindings/timelock/UnblockFunctionSelector.go @@ -14,7 +14,7 @@ import ( type UnblockFunctionSelector struct { Selector *[8]uint8 - // [0] = [] config + // [0] = [WRITE] config // // [1] = [SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` @@ -36,7 +36,7 @@ func (inst *UnblockFunctionSelector) SetSelector(selector [8]uint8) *UnblockFunc // SetConfigAccount sets the "config" account. func (inst *UnblockFunctionSelector) SetConfigAccount(config ag_solanago.PublicKey) *UnblockFunctionSelector { - inst.AccountMetaSlice[0] = ag_solanago.Meta(config) + inst.AccountMetaSlice[0] = ag_solanago.Meta(config).WRITE() return inst } diff --git a/chains/solana/gobindings/timelock/accounts.go b/chains/solana/gobindings/timelock/accounts.go index 15468da0..ad213c0e 100644 --- a/chains/solana/gobindings/timelock/accounts.go +++ b/chains/solana/gobindings/timelock/accounts.go @@ -16,6 +16,7 @@ type Config struct { CancellerRoleAccessController ag_solanago.PublicKey BypasserRoleAccessController ag_solanago.PublicKey MinDelay uint64 + BlockedSelectors BlockedSelectors } var ConfigDiscriminator = [8]byte{155, 12, 170, 224, 30, 250, 204, 130} @@ -61,6 +62,11 @@ func (obj Config) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { if err != nil { return err } + // Serialize `BlockedSelectors` param: + err = encoder.Encode(obj.BlockedSelectors) + if err != nil { + return err + } return nil } @@ -113,6 +119,11 @@ func (obj *Config) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) if err != nil { return err } + // Deserialize `BlockedSelectors`: + err = decoder.Decode(&obj.BlockedSelectors) + if err != nil { + return err + } return nil } diff --git a/chains/solana/gobindings/timelock/types.go b/chains/solana/gobindings/timelock/types.go index ff1d6e11..430d58b9 100644 --- a/chains/solana/gobindings/timelock/types.go +++ b/chains/solana/gobindings/timelock/types.go @@ -7,6 +7,39 @@ import ( ag_solanago "github.com/gagliardetto/solana-go" ) +type BlockedSelectors struct { + Xs [32][8]uint8 + Len uint64 +} + +func (obj BlockedSelectors) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `Xs` param: + err = encoder.Encode(obj.Xs) + if err != nil { + return err + } + // Serialize `Len` param: + err = encoder.Encode(obj.Len) + if err != nil { + return err + } + return nil +} + +func (obj *BlockedSelectors) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `Xs`: + err = decoder.Decode(&obj.Xs) + if err != nil { + return err + } + // Deserialize `Len`: + err = decoder.Decode(&obj.Len) + if err != nil { + return err + } + return nil +} + type InstructionData struct { ProgramId ag_solanago.PublicKey Data []byte @@ -98,8 +131,7 @@ func (obj *InstructionAccount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) type TimelockError ag_binary.BorshEnum const ( - Unauthorized_TimelockError TimelockError = iota - InvalidInput_TimelockError + InvalidInput_TimelockError TimelockError = iota Overflow_TimelockError InvalidId_TimelockError OperationNotFinalized_TimelockError @@ -110,14 +142,16 @@ const ( OperationNotCancellable_TimelockError OperationNotReady_TimelockError MissingDependency_TimelockError - BlockedSelector_TimelockError InvalidAccessController_TimelockError + BlockedSelector_TimelockError + AlreadyBlocked_TimelockError + SelectorNotFound_TimelockError + InvalidInstructionData_TimelockError + MaxCapacityReached_TimelockError ) func (value TimelockError) String() string { switch value { - case Unauthorized_TimelockError: - return "Unauthorized" case InvalidInput_TimelockError: return "InvalidInput" case Overflow_TimelockError: @@ -140,10 +174,18 @@ func (value TimelockError) String() string { return "OperationNotReady" case MissingDependency_TimelockError: return "MissingDependency" - case BlockedSelector_TimelockError: - return "BlockedSelector" case InvalidAccessController_TimelockError: return "InvalidAccessController" + case BlockedSelector_TimelockError: + return "BlockedSelector" + case AlreadyBlocked_TimelockError: + return "AlreadyBlocked" + case SelectorNotFound_TimelockError: + return "SelectorNotFound" + case InvalidInstructionData_TimelockError: + return "InvalidInstructionData" + case MaxCapacityReached_TimelockError: + return "MaxCapacityReached" default: return "" } diff --git a/commit/chainfee/observation.go b/commit/chainfee/observation.go index f6716078..a0e0df29 100644 --- a/commit/chainfee/observation.go +++ b/commit/chainfee/observation.go @@ -31,8 +31,9 @@ func (p *processor) Observation( return Observation{}, err } + supportedChains.Remove(p.destChain) if supportedChains.Cardinality() == 0 { - p.lggr.Info("no supported chains, nothing to observe") + p.lggr.Info("no supported chains other than dest chain to observe") return Observation{}, nil } diff --git a/commit/chainfee/observation_test.go b/commit/chainfee/observation_test.go index 60c520d6..8fa3bf4c 100644 --- a/commit/chainfee/observation_test.go +++ b/commit/chainfee/observation_test.go @@ -3,9 +3,12 @@ package chainfee import ( "math/big" "math/rand" + "sort" "testing" "time" + "golang.org/x/exp/maps" + mapset "github.com/deckarep/golang-set/v2" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -32,11 +35,15 @@ func Test_processor_Observation(t *testing.T) { fChain map[ccipocr3.ChainSelector]int expectedChainFeePriceUpdates map[ccipocr3.ChainSelector]Update - expErr bool + dstChain ccipocr3.ChainSelector + + expErr bool + emptyObs bool }{ { - name: "two chains", - supportedChains: []ccipocr3.ChainSelector{1}, + name: "two chains excluding dest", + supportedChains: []ccipocr3.ChainSelector{1, 2, 3}, + dstChain: 3, chainFeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ 1: { ExecutionFee: big.NewInt(10), @@ -86,9 +93,16 @@ func Test_processor_Observation(t *testing.T) { fChain: map[ccipocr3.ChainSelector]int{ 1: 1, 2: 2, + 3: 1, }, expErr: false, }, + { + name: "only dest chain", + supportedChains: []ccipocr3.ChainSelector{1}, + dstChain: 1, + emptyObs: true, + }, } for _, tc := range testCases { @@ -103,25 +117,32 @@ func Test_processor_Observation(t *testing.T) { p := &processor{ lggr: lggr, chainSupport: cs, + destChain: tc.dstChain, ccipReader: ccipReader, oracleID: oracleID, homeChain: homeChain, metricsReporter: NoopMetrics{}, } + supportedSet := mapset.NewSet(tc.supportedChains...) + cs.EXPECT().DestChain().Return(tc.dstChain).Maybe() cs.EXPECT().SupportedChains(oracleID). - Return(mapset.NewSet(tc.supportedChains...), nil) + Return(supportedSet, nil).Maybe() + + supportedSet.Remove(tc.dstChain) + slicesWithoutDst := supportedSet.ToSlice() + sort.Slice(slicesWithoutDst, func(i, j int) bool { return slicesWithoutDst[i] < slicesWithoutDst[j] }) - ccipReader.EXPECT().GetChainsFeeComponents(ctx, tc.supportedChains). - Return(tc.chainFeeComponents) + ccipReader.EXPECT().GetChainsFeeComponents(ctx, slicesWithoutDst). + Return(tc.chainFeeComponents).Maybe() - ccipReader.EXPECT().GetWrappedNativeTokenPriceUSD(ctx, tc.supportedChains). - Return(tc.nativeTokenPrices) + ccipReader.EXPECT().GetWrappedNativeTokenPriceUSD(ctx, slicesWithoutDst). + Return(tc.nativeTokenPrices).Maybe() - ccipReader.EXPECT().GetChainFeePriceUpdate(ctx, tc.supportedChains). - Return(tc.existingChainFeePriceUpdates) + ccipReader.EXPECT().GetChainFeePriceUpdate(ctx, slicesWithoutDst). + Return(tc.existingChainFeePriceUpdates).Maybe() - homeChain.EXPECT().GetFChain().Return(tc.fChain, nil) + homeChain.EXPECT().GetFChain().Return(tc.fChain, nil).Maybe() tStart := time.Now() obs, err := p.Observation(ctx, Outcome{}, Query{}) @@ -130,13 +151,20 @@ func Test_processor_Observation(t *testing.T) { require.Error(t, err) return } + if tc.emptyObs { + require.Empty(t, obs) + return + } require.NoError(t, err) require.GreaterOrEqual(t, obs.TimestampNow.UnixNano(), tStart.UnixNano()) require.LessOrEqual(t, obs.TimestampNow.UnixNano(), tEnd.UnixNano()) require.Equal(t, tc.chainFeeComponents, obs.FeeComponents) + require.ElementsMatch(t, slicesWithoutDst, maps.Keys(obs.FeeComponents)) require.Equal(t, tc.nativeTokenPrices, obs.NativeTokenPrices) + require.ElementsMatch(t, slicesWithoutDst, maps.Keys(obs.NativeTokenPrices)) require.Equal(t, tc.expectedChainFeePriceUpdates, obs.ChainFeeUpdates) + require.ElementsMatch(t, slicesWithoutDst, maps.Keys(obs.ChainFeeUpdates)) require.Equal(t, tc.fChain, obs.FChain) }) } diff --git a/commit/chainfee/outcome.go b/commit/chainfee/outcome.go index e81bbd2c..a08c0124 100644 --- a/commit/chainfee/outcome.go +++ b/commit/chainfee/outcome.go @@ -50,11 +50,12 @@ func (p *processor) Outcome( // 1 LINK = 5.00 USD per full token, each full token is 1e18 units -> 5 * 1e18 * 1e18 / 1e18 = 5e18 usdPerFeeToken, ok := consensusObs.NativeTokenPrices[chain] if !ok { - p.lggr.Warnw("missing native token price for chain", + p.lggr.Warnw("missing native token price for chain, chain fee will not be updated", "chain", chain, ) continue } + p.lggr.Debugw("USD per fee token", "chain", chain, "usdPerFeeToken", usdPerFeeToken) // Example with Wei as the lowest denominator and Eth as the Fee token // usdPerEthToken = Xe18USD18 diff --git a/commit/merkleroot/observation.go b/commit/merkleroot/observation.go index 095c86bb..63eb18ab 100644 --- a/commit/merkleroot/observation.go +++ b/commit/merkleroot/observation.go @@ -222,9 +222,32 @@ func (p *Processor) getObservation( nextState := previousOutcome.nextState() switch nextState { case selectingRangesForReport: - offRampNextSeqNums := p.observer.ObserveOffRampNextSeqNums(ctx) - onRampLatestSeqNums := p.observer.ObserveLatestOnRampSeqNums(ctx, p.destChain) - rmnRemoteCfg := p.observer.ObserveRMNRemoteCfg(ctx, p.destChain) + var ( + offRampNextSeqNums []plugintypes.SeqNumChain + onRampLatestSeqNums []plugintypes.SeqNumChain + rmnRemoteCfg rmntypes.RemoteConfig + ) + + eg := &errgroup.Group{} + + eg.Go(func() error { + offRampNextSeqNums = p.observer.ObserveOffRampNextSeqNums(ctx) + return nil + }) + + eg.Go(func() error { + onRampLatestSeqNums = p.observer.ObserveLatestOnRampSeqNums(ctx, p.destChain) + return nil + }) + + eg.Go(func() error { + rmnRemoteCfg = p.observer.ObserveRMNRemoteCfg(ctx, p.destChain) + return nil + }) + + if err := eg.Wait(); err != nil { + return Observation{}, nextState, fmt.Errorf("failed to get observation: %w", err) + } return Observation{ OnRampMaxSeqNums: onRampLatestSeqNums, diff --git a/commit/plugin_e2e_test.go b/commit/plugin_e2e_test.go index 1afd9449..eb53b1ce 100644 --- a/commit/plugin_e2e_test.go +++ b/commit/plugin_e2e_test.go @@ -438,11 +438,11 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { nodes := make([]ocr3types.ReportingPlugin[[]byte], len(oracleIDs)) newFeeComponents, newNativePrice, packedGasPrice := newRandomFees() - expectedChainFeeOutcome := chainfee.Outcome{ + expectedChain1FeeOutcome := chainfee.Outcome{ GasPrices: []ccipocr3.GasPriceChain{ { GasPrice: packedGasPrice, - ChainSel: destChain, + ChainSel: sourceChain1, }, }, } @@ -464,10 +464,6 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { MerkleRootOutcome: merkleOutcome, ChainFeeOutcome: chainfee.Outcome{ GasPrices: []ccipocr3.GasPriceChain{ - { - GasPrice: packedGasPrice, - ChainSel: destChain, - }, { GasPrice: packedGasPrice, ChainSel: sourceChain1, @@ -485,7 +481,6 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GetChainsFeeComponents(params.ctx, mock.Anything). Return( map[ccipocr3.ChainSelector]types.ChainFeeComponents{ - destChain: newFeeComponents, sourceChain1: newFeeComponents, sourceChain2: newFeeComponents, }) @@ -493,7 +488,6 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { m.EXPECT(). GetWrappedNativeTokenPriceUSD(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]ccipocr3.BigInt{ - destChain: newNativePrice, sourceChain1: newNativePrice, sourceChain2: newNativePrice, }) @@ -504,7 +498,7 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { prevOutcome: committypes.Outcome{}, expOutcome: committypes.Outcome{ MerkleRootOutcome: merkleOutcome, - ChainFeeOutcome: expectedChainFeeOutcome, + ChainFeeOutcome: expectedChain1FeeOutcome, }, expTransmittedReportLen: 1, mockCCIPReader: func(m *readerpkg_mock.MockCCIPReader) { @@ -512,13 +506,12 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GetChainsFeeComponents(params.ctx, mock.Anything). Return( map[ccipocr3.ChainSelector]types.ChainFeeComponents{ - destChain: newFeeComponents, + sourceChain1: newFeeComponents, }) m.EXPECT(). GetWrappedNativeTokenPriceUSD(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]ccipocr3.BigInt{ - destChain: newNativePrice, sourceChain1: newNativePrice, sourceChain2: newNativePrice, }) @@ -528,11 +521,11 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { name: "fee components should not be updated within deviation", prevOutcome: committypes.Outcome{ MerkleRootOutcome: merkleOutcome, - ChainFeeOutcome: expectedChainFeeOutcome, + ChainFeeOutcome: expectedChain1FeeOutcome, }, expOutcome: committypes.Outcome{ MerkleRootOutcome: noReportMerkleOutcome(params.rmnReportCfg), - ChainFeeOutcome: expectedChainFeeOutcome, + ChainFeeOutcome: expectedChain1FeeOutcome, }, expTransmittedReportLen: 1, mockCCIPReader: func(m *readerpkg_mock.MockCCIPReader) { @@ -540,13 +533,13 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GetChainsFeeComponents(params.ctx, mock.Anything). Return( map[ccipocr3.ChainSelector]types.ChainFeeComponents{ - destChain: newFeeComponents, + sourceChain1: newFeeComponents, }) m.EXPECT(). GetWrappedNativeTokenPriceUSD(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]ccipocr3.BigInt{ - destChain: newNativePrice, + sourceChain1: newNativePrice, }) }, }, @@ -554,7 +547,7 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { name: "fresh fees (timestamped) should not be updated, even outside of deviation", prevOutcome: committypes.Outcome{ MerkleRootOutcome: merkleOutcome, - ChainFeeOutcome: expectedChainFeeOutcome, + ChainFeeOutcome: expectedChain1FeeOutcome, }, expOutcome: committypes.Outcome{ MerkleRootOutcome: noReportMerkleOutcome(params.rmnReportCfg), @@ -562,7 +555,7 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GasPrices: []ccipocr3.GasPriceChain{ { GasPrice: newPackedGasPrice2, - ChainSel: destChain, + ChainSel: sourceChain1, }, }, }, @@ -573,13 +566,13 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GetChainsFeeComponents(params.ctx, mock.Anything). Return( map[ccipocr3.ChainSelector]types.ChainFeeComponents{ - destChain: newFeeComponents2, + sourceChain1: newFeeComponents2, }) m.EXPECT(). GetWrappedNativeTokenPriceUSD(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]ccipocr3.BigInt{ - destChain: newNativePrice2, + sourceChain1: newNativePrice2, }) }, }, @@ -587,7 +580,7 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { name: "stale fees should be updated", prevOutcome: committypes.Outcome{ MerkleRootOutcome: merkleOutcome, - ChainFeeOutcome: expectedChainFeeOutcome, + ChainFeeOutcome: expectedChain1FeeOutcome, }, expOutcome: committypes.Outcome{ MerkleRootOutcome: noReportMerkleOutcome(params.rmnReportCfg), @@ -595,7 +588,7 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GasPrices: []ccipocr3.GasPriceChain{ { GasPrice: newPackedGasPrice2, - ChainSel: destChain, + ChainSel: sourceChain1, }, }, }, @@ -606,12 +599,12 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { GetChainsFeeComponents(params.ctx, mock.Anything). Return( map[ccipocr3.ChainSelector]types.ChainFeeComponents{ - destChain: newFeeComponents2, + sourceChain1: newFeeComponents2, }) m.EXPECT(). GetWrappedNativeTokenPriceUSD(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]ccipocr3.BigInt{ - destChain: newNativePrice2, + sourceChain1: newNativePrice2, }) m.EXPECT().GetChainFeePriceUpdate(params.ctx, mock.Anything).Unset() @@ -621,9 +614,9 @@ func TestPlugin_E2E_AllNodesAgree_ChainFee(t *testing.T) { m.EXPECT(). GetChainFeePriceUpdate(params.ctx, mock.Anything). Return(map[ccipocr3.ChainSelector]plugintypes.TimestampedBig{ - destChain: { + sourceChain1: { Timestamp: t, - Value: expectedChainFeeOutcome.GasPrices[0].GasPrice, + Value: expectedChain1FeeOutcome.GasPrices[0].GasPrice, }, }) }, diff --git a/commit/report.go b/commit/report.go index 1edb2aa6..56126274 100644 --- a/commit/report.go +++ b/commit/report.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "golang.org/x/exp/maps" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" @@ -125,8 +126,10 @@ func (p *Plugin) validateReport( seqNr uint64, r ocr3types.ReportWithInfo[[]byte], ) (bool, cciptypes.CommitPluginReport, error) { + lggr := logger.With(p.lggr, "seqNr", seqNr) + if r.Report == nil { - p.lggr.Warn("nil report", "seqNr", seqNr) + lggr.Warn("nil report") return false, cciptypes.CommitPluginReport{}, nil } @@ -136,7 +139,7 @@ func (p *Plugin) validateReport( } if decodedReport.IsEmpty() { - p.lggr.Warnw("empty report after decoding", "seqNr", seqNr, "decodedReport", decodedReport) + lggr.Warnw("empty report after decoding", "decodedReport", decodedReport) return false, cciptypes.CommitPluginReport{}, nil } @@ -148,7 +151,7 @@ func (p *Plugin) validateReport( if p.offchainCfg.RMNEnabled && len(decodedReport.MerkleRoots) > 0 && consensus.LtFPlusOne(int(reportInfo.RemoteF), len(decodedReport.RMNSignatures)) { - p.lggr.Infow("report with insufficient RMN signatures %d < %d+1", + lggr.Infof("report with insufficient RMN signatures %d < %d+1", len(decodedReport.RMNSignatures), reportInfo.RemoteF) return false, cciptypes.CommitPluginReport{}, nil } @@ -160,7 +163,7 @@ func (p *Plugin) validateReport( } if !supports { - p.lggr.Warnw("dest chain not supported, can't run report acceptance procedures") + lggr.Warnw("dest chain not supported, can't run report acceptance procedures") return false, cciptypes.CommitPluginReport{}, nil } @@ -170,7 +173,7 @@ func (p *Plugin) validateReport( } if !bytes.Equal(offRampConfigDigest[:], p.reportingCfg.ConfigDigest[:]) { - p.lggr.Warnw("my config digest doesn't match offramp's config digest, not accepting report", + lggr.Warnw("my config digest doesn't match offramp's config digest, not accepting report", "myConfigDigest", p.reportingCfg.ConfigDigest, "offRampConfigDigest", hex.EncodeToString(offRampConfigDigest[:]), ) @@ -188,7 +191,7 @@ func (p *Plugin) validateReport( err = merkleroot.ValidateMerkleRootsState(ctx, decodedReport.MerkleRoots, p.ccipReader) if err != nil { - p.lggr.Warnw("report reached transmission protocol but not transmitted, invalid merkle roots state", + lggr.Infow("report reached transmission protocol but not transmitted, invalid merkle roots state", "err", err, "merkleRoots", decodedReport.MerkleRoots) return false, cciptypes.CommitPluginReport{}, nil } @@ -205,7 +208,7 @@ func (p *Plugin) ShouldAcceptAttestedReport( } if !valid { - p.lggr.Warnw("report not valid, not accepting", "seqNr", seqNr) + p.lggr.Infow("report is not accepted", "seqNr", seqNr) return false, nil } @@ -266,7 +269,7 @@ func (p *Plugin) ShouldTransmitAcceptedReport( } if !valid { - p.lggr.Warnw("report not valid, not transmitting", "seqNr", seqNr) + p.lggr.Infow("report not valid, not transmitting", "seqNr", seqNr) return false, nil } diff --git a/execute/plugin.go b/execute/plugin.go index 2070a1f0..1bcde3ba 100644 --- a/execute/plugin.go +++ b/execute/plugin.go @@ -341,9 +341,11 @@ func (p *Plugin) validateReport( seqNr uint64, r ocr3types.ReportWithInfo[[]byte], ) (valid bool, decodedReport cciptypes.ExecutePluginReport, err error) { + lggr := logger.With(p.lggr, "seqNr", seqNr) + // Just a safety check, should never happen. if r.Report == nil { - p.lggr.Warn("skipping nil report", "seqNr", seqNr) + lggr.Warn("skipping nil report") return false, cciptypes.ExecutePluginReport{}, nil } @@ -353,7 +355,7 @@ func (p *Plugin) validateReport( } if len(decodedReport.ChainReports) == 0 { - p.lggr.Info("skipping empty report", "seqNr", seqNr) + lggr.Infow("skipping empty report") return false, cciptypes.ExecutePluginReport{}, nil } @@ -364,7 +366,7 @@ func (p *Plugin) validateReport( } if !supports { - p.lggr.Warnw("dest chain not supported, can't run report acceptance procedures", "seqNr", seqNr) + lggr.Warnw("dest chain not supported, can't run report acceptance procedures") return false, cciptypes.ExecutePluginReport{}, nil } @@ -374,10 +376,9 @@ func (p *Plugin) validateReport( } if !bytes.Equal(offRampConfigDigest[:], p.reportingCfg.ConfigDigest[:]) { - p.lggr.Warnw("my config digest doesn't match offramp's config digest, not accepting/transmitting report", + lggr.Warnw("my config digest doesn't match offramp's config digest, not accepting/transmitting report", "myConfigDigest", p.reportingCfg.ConfigDigest, "offRampConfigDigest", hex.EncodeToString(offRampConfigDigest[:]), - "seqNr", seqNr, ) return false, cciptypes.ExecutePluginReport{}, nil } @@ -394,7 +395,7 @@ func (p *Plugin) ShouldAcceptAttestedReport( } if !valid { - p.lggr.Warnw("report not valid, not accepting", "seqNr", seqNr) + p.lggr.Infow("report is not accepted", "seqNr", seqNr) return false, nil } @@ -417,7 +418,10 @@ func (p *Plugin) ShouldAcceptAttestedReport( return false, nil } - p.lggr.Info("ShouldAcceptAttestedReport returns true, report accepted") + p.lggr.Infow("ShouldAcceptAttestedReport returns true, report accepted", + "seqNr", seqNr, + "reports", decodedReport.ChainReports, + ) return true, nil } @@ -430,11 +434,14 @@ func (p *Plugin) ShouldTransmitAcceptedReport( } if !valid { - p.lggr.Warnw("report not valid, not transmitting", "seqNr", seqNr) + p.lggr.Infow("report not accepted for transmit", "seqNr", seqNr) return false, nil } - p.lggr.Infow("transmitting report", "reports", decodedReport.ChainReports) + p.lggr.Infow("ShouldTransmitAttestedReport returns true, report accepted", + "seqNr", seqNr, + "reports", decodedReport.ChainReports, + ) return true, nil } diff --git a/internal/plugincommon/chain_support.go b/internal/plugincommon/chain_support.go index 61df07ce..8f6ebe09 100644 --- a/internal/plugincommon/chain_support.go +++ b/internal/plugincommon/chain_support.go @@ -87,7 +87,7 @@ func (c ccipChainSupport) SupportedChains(oracleID commontypes.OracleID) (mapset return mapset.NewSet[cciptypes.ChainSelector](), fmt.Errorf("error getting supported chains: %w", err) } - return supportedChains, nil + return supportedChains.Clone(), nil } // SupportsDestChain returns true if the given oracle supports the dest chain, returns false otherwise diff --git a/internal/plugincommon/consensus/consensus.go b/internal/plugincommon/consensus/consensus.go index 3738ae0a..cfb9e787 100644 --- a/internal/plugincommon/consensus/consensus.go +++ b/internal/plugincommon/consensus/consensus.go @@ -60,7 +60,7 @@ func GetConsensusMapAggregator[K comparable, T any]( for key, values := range items { if thresh, ok := f.Get(key); ok && len(values) < int(thresh) { - lggr.Warnf("could not reach consensus on %s for key %v", objectName, key) + lggr.Debugf("could not reach consensus on %s for key %v", objectName, key) continue } consensus[key] = agg(values) diff --git a/internal/plugincommon/discovery/processor.go b/internal/plugincommon/discovery/processor.go index 6fee6ea6..8e17d80d 100644 --- a/internal/plugincommon/discovery/processor.go +++ b/internal/plugincommon/discovery/processor.go @@ -155,14 +155,14 @@ func aggregateObservations( for chain, addr := range ao.Observation.Addresses[consts.ContractNameOnRamp] { // we don't want invalid observations to "poison" the consensus. if isZero(addr) { - lggr.Warnf("skipping empty onramp address in observation from Oracle %d", ao.OracleID) + lggr.Debugf("skipping empty onramp address in observation from Oracle %d", ao.OracleID) continue } obs.onrampAddrs[chain] = append(obs.onrampAddrs[chain], addr) } if isZero(ao.Observation.Addresses[consts.ContractNameNonceManager][dest]) { - lggr.Warnf("skipping empty nonce manager address in observation from Oracle %d", ao.OracleID) + lggr.Debugf("skipping empty nonce manager address in observation from Oracle %d", ao.OracleID) } else { obs.nonceManagerAddrs[dest] = append( obs.nonceManagerAddrs[dest], @@ -171,7 +171,7 @@ func aggregateObservations( } if isZero(ao.Observation.Addresses[consts.ContractNameRMNRemote][dest]) { - lggr.Warnf("skipping empty RMNRemote address in observation from Oracle %d", ao.OracleID) + lggr.Debugf("skipping empty RMNRemote address in observation from Oracle %d", ao.OracleID) } else { obs.rmnRemoteAddrs[dest] = append( obs.rmnRemoteAddrs[dest], @@ -181,7 +181,7 @@ func aggregateObservations( for chain, addr := range ao.Observation.Addresses[consts.ContractNameRouter] { if isZero(addr) { - lggr.Warnf("skipping empty Router address in observation from Oracle %d", ao.OracleID) + lggr.Debugf("skipping empty Router address in observation from Oracle %d", ao.OracleID) continue } obs.routerAddrs[chain] = append( @@ -193,7 +193,7 @@ func aggregateObservations( for chain, addr := range ao.Observation.Addresses[consts.ContractNameFeeQuoter] { // we don't want invalid observations to "poison" the consensus. if isZero(addr) { - lggr.Warnf("skipping empty fee quoter address in observation from Oracle %d", ao.OracleID) + lggr.Debugf("skipping empty fee quoter address in observation from Oracle %d", ao.OracleID) continue } obs.feeQuoterAddrs[chain] = append(obs.feeQuoterAddrs[chain], addr) @@ -240,7 +240,7 @@ func (cdp *ContractDiscoveryProcessor) Outcome( "fChainThresh", fChainThresh, ) if len(onrampConsensus) == 0 { - cdp.lggr.Warnw("No consensus on onramps, onrampConsensus map is empty") + cdp.lggr.Debugw("No consensus on onramps, onrampConsensus map is empty") } contracts[consts.ContractNameOnRamp] = onrampConsensus @@ -256,7 +256,7 @@ func (cdp *ContractDiscoveryProcessor) Outcome( "fChainThresh", fChainThresh, ) if len(nonceManagerConsensus) == 0 { - cdp.lggr.Warnw("No consensus on nonce manager, nonceManagerConsensus map is empty") + cdp.lggr.Debugw("No consensus on nonce manager, nonceManagerConsensus map is empty") } contracts[consts.ContractNameNonceManager] = nonceManagerConsensus @@ -273,7 +273,7 @@ func (cdp *ContractDiscoveryProcessor) Outcome( "fChainThresh", fChainThresh, ) if len(rmnRemoteConsensus) == 0 { - cdp.lggr.Warnw("No consensus on RMNRemote, rmnRemoteConsensus map is empty") + cdp.lggr.Debugw("No consensus on RMNRemote, rmnRemoteConsensus map is empty") } contracts[consts.ContractNameRMNRemote] = rmnRemoteConsensus @@ -289,7 +289,7 @@ func (cdp *ContractDiscoveryProcessor) Outcome( "fChainThresh", fChainThresh, ) if len(feeQuoterConsensus) == 0 { - cdp.lggr.Warnw("No consensus on fee quoters, feeQuoterConsensus map is empty") + cdp.lggr.Debugw("No consensus on fee quoters, feeQuoterConsensus map is empty") } contracts[consts.ContractNameFeeQuoter] = feeQuoterConsensus @@ -306,7 +306,7 @@ func (cdp *ContractDiscoveryProcessor) Outcome( "fChainThresh", fChainThresh, ) if len(routerConsensus) == 0 { - cdp.lggr.Warnw("No consensus on router, routerConsensus map is empty") + cdp.lggr.Debugw("No consensus on router, routerConsensus map is empty") } contracts[consts.ContractNameRouter] = routerConsensus diff --git a/pluginconfig/execute.go b/pluginconfig/execute.go index e4b9bcf9..37a0373b 100644 --- a/pluginconfig/execute.go +++ b/pluginconfig/execute.go @@ -3,6 +3,7 @@ package pluginconfig import ( "encoding/json" "errors" + "time" commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" ) @@ -36,9 +37,23 @@ type ExecuteOffchainConfig struct { // TokenDataObservers registers different strategies for processing token data. TokenDataObservers []TokenDataObserverConfig `json:"tokenDataObservers"` + + // transmissionDelayMultiplier is used to calculate the transmission delay for each oracle. + TransmissionDelayMultiplier time.Duration `json:"transmissionDelayMultiplier"` +} + +func (e *ExecuteOffchainConfig) ApplyDefaultsAndValidate() error { + e.applyDefaults() + return e.Validate() +} + +func (e *ExecuteOffchainConfig) applyDefaults() { + if e.TransmissionDelayMultiplier == 0 { + e.TransmissionDelayMultiplier = defaultTransmissionDelayMultiplier + } } -func (e ExecuteOffchainConfig) Validate() error { +func (e *ExecuteOffchainConfig) Validate() error { // TODO: this doesn't really make much sense for non-EVM chains. // Maybe we need to have a field in the config that is not JSON-encoded // that indicates chain family? @@ -77,7 +92,7 @@ func (e ExecuteOffchainConfig) Validate() error { return nil } -func (e ExecuteOffchainConfig) IsUSDCEnabled() bool { +func (e *ExecuteOffchainConfig) IsUSDCEnabled() bool { for _, ob := range e.TokenDataObservers { if ob.WellFormed() != nil { continue