diff --git a/chains/solana/contracts/programs/ccip-router/src/instructions/v1/pools.rs b/chains/solana/contracts/programs/ccip-router/src/instructions/v1/pools.rs index 6cd99b0db..895bc74b2 100644 --- a/chains/solana/contracts/programs/ccip-router/src/instructions/v1/pools.rs +++ b/chains/solana/contracts/programs/ccip-router/src/instructions/v1/pools.rs @@ -1,6 +1,5 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::get_associated_token_address_with_program_id; -use anchor_spl::token::spl_token::native_mint; use anchor_spl::token_2022::spl_token_2022::{self, instruction::transfer_checked, state::Mint}; use anchor_spl::token_interface::TokenAccount; use solana_program::{ @@ -108,17 +107,8 @@ pub fn validate_and_parse_token_accounts<'info>( CcipRouterError::InvalidInputsPoolAccounts ); - let (expected_fee_token_config, _) = Pubkey::find_program_address( - &[ - FEE_BILLING_TOKEN_CONFIG, - if mint.key() == Pubkey::default() { - native_mint::ID.as_ref() // pre-2022 WSOL - } else { - mint.key.as_ref() - }, - ], - &router, - ); + let (expected_fee_token_config, _) = + Pubkey::find_program_address(&[FEE_BILLING_TOKEN_CONFIG, mint.key.as_ref()], &router); require!( fee_token_config.key() == expected_fee_token_config, CcipRouterError::InvalidInputsConfigAccounts @@ -188,29 +178,46 @@ pub fn validate_and_parse_token_accounts<'info>( .map_err(|_| CcipRouterError::InvalidInputsLookupTableAccounts)?; // reconstruct + validate expected values in token pool lookup table - // base set of constant accounts (8) + // base set of constant accounts (9) // + additional constant accounts (remaining_accounts) that are not required but may be used for additional token pool functionality (like CPI) - let mut expected_entries = vec![ - lookup_table.key(), - token_admin_registry.key(), - pool_program.key(), - pool_config.key(), - pool_token_account.key(), - pool_signer.key(), - token_program.key(), - mint.key(), - fee_token_config.key(), + let required_entries = [ + lookup_table, + token_admin_registry, + pool_program, + pool_config, + pool_token_account, + pool_signer, + token_program, + mint, + fee_token_config, ]; - let mut remaining_keys: Vec = remaining_accounts.iter().map(|x| x.key()).collect(); - expected_entries.append(&mut remaining_keys); - require!( - lookup_table_account.addresses.len() == expected_entries.len(), - CcipRouterError::InvalidInputsLookupTableAccounts - ); - require!( - lookup_table_account.addresses.as_ref() == expected_entries, - CcipRouterError::InvalidInputsLookupTableAccounts - ); + { + // validate pool addresses + let mut expected_keys: Vec = required_entries.iter().map(|x| x.key()).collect(); + let mut remaining_keys: Vec = + remaining_accounts.iter().map(|x| x.key()).collect(); + expected_keys.append(&mut remaining_keys); + require!( + lookup_table_account.addresses.as_ref() == expected_keys, + CcipRouterError::InvalidInputsLookupTableAccounts + ); + } + { + // validate pool address writable + // token admin registry contains an array (binary) of indexes that are writable + // check that the writability of the passed accounts match the writable configuration (using indexes) + let mut expected_is_writable: Vec = + required_entries.iter().map(|x| x.is_writable).collect(); + let mut remaining_is_writable: Vec = + remaining_accounts.iter().map(|x| x.is_writable).collect(); + expected_is_writable.append(&mut remaining_is_writable); + for (i, is_writable) in expected_is_writable.iter().enumerate() { + require!( + token_admin_registry_account.is_writable(i as u8) == *is_writable, + CcipRouterError::InvalidInputsLookupTableAccountWritable + ); + } + } } Ok(TokenAccounts { diff --git a/chains/solana/contracts/programs/ccip-router/src/instructions/v1/token_admin_registry.rs b/chains/solana/contracts/programs/ccip-router/src/instructions/v1/token_admin_registry.rs index a65d85839..c76389d9c 100644 --- a/chains/solana/contracts/programs/ccip-router/src/instructions/v1/token_admin_registry.rs +++ b/chains/solana/contracts/programs/ccip-router/src/instructions/v1/token_admin_registry.rs @@ -1,9 +1,14 @@ use anchor_lang::prelude::*; +use anchor_spl::associated_token::get_associated_token_address_with_program_id; +use bytemuck::Zeroable; +use solana_program::{address_lookup_table::state::AddressLookupTable, log::sol_log}; use crate::{ AcceptAdminRoleTokenAdminRegistry, AdministratorRegistered, AdministratorTransferRequested, AdministratorTransferred, CcipRouterError, ModifyTokenAdminRegistry, PoolSet, RegisterTokenAdminRegistryViaGetCCIPAdmin, RegisterTokenAdminRegistryViaOwner, + SetPoolTokenAdminRegistry, CCIP_TOKENPOOL_CONFIG, CCIP_TOKENPOOL_SIGNER, + FEE_BILLING_TOKEN_CONFIG, TOKEN_ADMIN_REGISTRY_SEED, }; pub fn register_token_admin_registry_via_get_ccip_admin( @@ -53,20 +58,87 @@ pub fn register_token_admin_registry_via_owner( } pub fn set_pool( - ctx: Context, + ctx: Context, mint: Pubkey, - pool_lookup_table: Pubkey, + writable_indexes: Vec, ) -> Result<()> { + // set new lookup table let token_admin_registry = &mut ctx.accounts.token_admin_registry; let previous_pool = token_admin_registry.lookup_table; - token_admin_registry.lookup_table = pool_lookup_table; + let new_pool = ctx.accounts.pool_lookuptable.key(); + token_admin_registry.lookup_table = new_pool; + + // set writable indexes + // used to build offchain tokenpool accounts in a transaction + // indexes can be checked to indicate is_writable for the specific account + // this is also used to validate to ensure the correct write permissions are used according the admin + token_admin_registry.reset_writable(); + for ind in writable_indexes { + token_admin_registry.set_writable(ind) + } - // TODO: Validate here that the lookup table has everything + // validate lookup table contains minimum required accounts if not zero address + if new_pool != Pubkey::zeroed() { + // deserialize lookup table account + let lookup_table_data = &mut &ctx.accounts.pool_lookuptable.data.borrow()[..]; + let lookup_table_account: AddressLookupTable = + AddressLookupTable::deserialize(lookup_table_data) + .map_err(|_| CcipRouterError::InvalidInputsLookupTableAccounts)?; + require!( + lookup_table_account.addresses.len() >= 9, + CcipRouterError::InvalidInputsLookupTableAccounts + ); + + // calculate or retrieve expected addresses + let (token_admin_registry, _) = Pubkey::find_program_address( + &[TOKEN_ADMIN_REGISTRY_SEED, mint.as_ref()], + ctx.program_id, + ); + let pool_program = lookup_table_account.addresses[2]; // cannot be calculated, can be custom per pool + let token_program = lookup_table_account.addresses[6]; // cannot be calculated, can be custom per token + let (pool_config, _) = + Pubkey::find_program_address(&[CCIP_TOKENPOOL_CONFIG, mint.as_ref()], &pool_program); + let (pool_signer, _) = + Pubkey::find_program_address(&[CCIP_TOKENPOOL_SIGNER, mint.as_ref()], &pool_program); + let (fee_billing_config, _) = Pubkey::find_program_address( + &[FEE_BILLING_TOKEN_CONFIG, mint.as_ref()], + ctx.program_id, + ); + + let min_accounts = [ + ctx.accounts.pool_lookuptable.key(), + token_admin_registry, + pool_program, + pool_config, + get_associated_token_address_with_program_id( + &pool_signer.key(), + &mint.key(), + &token_program.key(), + ), + pool_signer, + token_program, + mint, + fee_billing_config, + ]; + + for (i, acc) in min_accounts.iter().enumerate() { + // for easier debugging UX + if lookup_table_account.addresses[i] != *acc { + sol_log(&i.to_string()); + sol_log(&acc.to_string()); + sol_log(&lookup_table_account.addresses[i].to_string()); + } + require!( + lookup_table_account.addresses[i] == *acc, + CcipRouterError::InvalidInputsLookupTableAccounts + ); + } + } emit!(PoolSet { token: mint, previous_pool_lookup_table: previous_pool, - new_pool_lookup_table: pool_lookup_table, + new_pool_lookup_table: new_pool, }); Ok(()) diff --git a/chains/solana/contracts/programs/ccip-router/src/lib.rs b/chains/solana/contracts/programs/ccip-router/src/lib.rs index 943bfc478..c677d503e 100644 --- a/chains/solana/contracts/programs/ccip-router/src/lib.rs +++ b/chains/solana/contracts/programs/ccip-router/src/lib.rs @@ -324,12 +324,13 @@ pub mod ccip_router { /// * `ctx` - The context containing the accounts required for setting the pool. /// * `mint` - The public key of the token mint. /// * `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool. + /// * `is_writable` - index of account in lookup table that is writable pub fn set_pool( - ctx: Context, + ctx: Context, mint: Pubkey, - pool_lookup_table: Pubkey, + writable_indexes: Vec, ) -> Result<()> { - v1::token_admin_registry::set_pool(ctx, mint, pool_lookup_table) + v1::token_admin_registry::set_pool(ctx, mint, writable_indexes) } /// Transfers the admin role of the token admin registry to a new admin. @@ -625,6 +626,8 @@ pub enum CcipRouterError { InvalidInputsTokenAdminRegistryAccounts, #[msg("Invalid LookupTable account")] InvalidInputsLookupTableAccounts, + #[msg("Invalid LookupTable account writable access")] + InvalidInputsLookupTableAccountWritable, #[msg("Cannot send zero tokens")] InvalidInputsTokenAmount, #[msg("Release or mint balance mismatch")] diff --git a/chains/solana/contracts/programs/ccip-router/src/token_context.rs b/chains/solana/contracts/programs/ccip-router/src/token_context.rs index 876afa997..80222139c 100644 --- a/chains/solana/contracts/programs/ccip-router/src/token_context.rs +++ b/chains/solana/contracts/programs/ccip-router/src/token_context.rs @@ -13,6 +13,35 @@ pub struct TokenAdminRegistry { pub administrator: Pubkey, pub pending_administrator: Pubkey, pub lookup_table: Pubkey, + // binary representation of indexes that are writable in token pool lookup table + // lookup table can store 256 addresses + pub writable_indexes: [u128; 2], +} + +impl TokenAdminRegistry { + // set writable inserts bits from left to right + // index 0 is left-most bit + pub fn set_writable(&mut self, index: u8) { + match index < 128 { + true => { + self.writable_indexes[0] |= 1 << (127 - index); + } + false => { + self.writable_indexes[1] |= 1 << (255 - index); + } + } + } + + pub fn is_writable(&self, index: u8) -> bool { + match index < 128 { + true => self.writable_indexes[0] & 1 << (127 - index) != 0, + false => self.writable_indexes[1] & 1 << (255 - index) != 0, + } + } + + pub fn reset_writable(&mut self) { + self.writable_indexes = [0, 0]; + } } #[derive(Accounts)] @@ -73,6 +102,22 @@ pub struct ModifyTokenAdminRegistry<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction(mint: Pubkey)] +pub struct SetPoolTokenAdminRegistry<'info> { + #[account( + mut, + seeds = [TOKEN_ADMIN_REGISTRY_SEED, mint.as_ref()], + bump, + constraint = valid_version(token_admin_registry.version, MAX_TOKEN_REGISTRY_V) @ CcipRouterError::InvalidInputs, + )] + pub token_admin_registry: Account<'info, TokenAdminRegistry>, + /// CHECK: anchor does not support automatic lookup table deserialization + pub pool_lookuptable: UncheckedAccount<'info>, + #[account(mut, address = token_admin_registry.administrator @ CcipRouterError::Unauthorized)] + pub authority: Signer<'info>, +} + #[derive(Accounts)] #[instruction(mint: Pubkey)] pub struct AcceptAdminRoleTokenAdminRegistry<'info> { @@ -112,3 +157,73 @@ pub struct SetTokenBillingConfig<'info> { pub authority: Signer<'info>, pub system_program: Program<'info, System>, } + +#[cfg(test)] +mod tests { + use bytemuck::Zeroable; + use solana_program::pubkey::Pubkey; + + use crate::TokenAdminRegistry; + #[test] + fn set_writable() { + let mut state = TokenAdminRegistry { + version: 0, + administrator: Pubkey::zeroed(), + pending_administrator: Pubkey::zeroed(), + lookup_table: Pubkey::zeroed(), + writable_indexes: [0, 0], + }; + + state.set_writable(0); + state.set_writable(128); + assert_eq!(state.writable_indexes[0], 2u128.pow(127)); + assert_eq!(state.writable_indexes[1], 2u128.pow(127)); + + state.reset_writable(); + assert_eq!(state.writable_indexes[0], 0); + assert_eq!(state.writable_indexes[1], 0); + + state.set_writable(0); + state.set_writable(2); + state.set_writable(127); + state.set_writable(128); + state.set_writable(2 + 128); + state.set_writable(255); + assert_eq!( + state.writable_indexes[0], + 2u128.pow(127) + 2u128.pow(127 - 2) + 2u128.pow(0) + ); + assert_eq!( + state.writable_indexes[1], + 2u128.pow(127) + 2u128.pow(127 - 2) + 2u128.pow(0) + ); + } + + #[test] + fn check_writable() { + let state = TokenAdminRegistry { + version: 0, + administrator: Pubkey::zeroed(), + pending_administrator: Pubkey::zeroed(), + lookup_table: Pubkey::zeroed(), + writable_indexes: [ + 2u128.pow(127 - 7) + 2u128.pow(127 - 2) + 2u128.pow(127 - 4), + 2u128.pow(127 - 8) + 2u128.pow(127 - 56) + 2u128.pow(127 - 100), + ], + }; + + assert_eq!(state.is_writable(0), false); + assert_eq!(state.is_writable(128), false); + assert_eq!(state.is_writable(255), false); + + assert_eq!(state.writable_indexes[0].count_ones(), 3); + assert_eq!(state.writable_indexes[1].count_ones(), 3); + + assert_eq!(state.is_writable(7), true); + assert_eq!(state.is_writable(2), true); + assert_eq!(state.is_writable(4), true); + assert_eq!(state.is_writable(128 + 8), true); + assert_eq!(state.is_writable(128 + 56), true); + assert_eq!(state.is_writable(128 + 100), true); + } +} diff --git a/chains/solana/contracts/target/idl/ccip_router.json b/chains/solana/contracts/target/idl/ccip_router.json index b77b5bb8f..eb3376db9 100644 --- a/chains/solana/contracts/target/idl/ccip_router.json +++ b/chains/solana/contracts/target/idl/ccip_router.json @@ -648,7 +648,8 @@ "", "* `ctx` - The context containing the accounts required for setting the pool.", "* `mint` - The public key of the token mint.", - "* `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool." + "* `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool.", + "* `is_writable` - index of account in lookup table that is writable" ], "accounts": [ { @@ -656,6 +657,11 @@ "isMut": true, "isSigner": false }, + { + "name": "poolLookuptable", + "isMut": false, + "isSigner": false + }, { "name": "authority", "isMut": true, @@ -668,8 +674,8 @@ "type": "publicKey" }, { - "name": "poolLookupTable", - "type": "publicKey" + "name": "writableIndexes", + "type": "bytes" } ] }, @@ -1746,6 +1752,15 @@ { "name": "lookupTable", "type": "publicKey" + }, + { + "name": "writableIndexes", + "type": { + "array": [ + "u128", + 2 + ] + } } ] } @@ -2808,6 +2823,9 @@ { "name": "InvalidInputsLookupTableAccounts" }, + { + "name": "InvalidInputsLookupTableAccountWritable" + }, { "name": "InvalidInputsTokenAmount" }, diff --git a/chains/solana/contracts/tests/ccip/ccip_router_test.go b/chains/solana/contracts/tests/ccip/ccip_router_test.go index 1bf84768f..aeb47e08d 100644 --- a/chains/solana/contracts/tests/ccip/ccip_router_test.go +++ b/chains/solana/contracts/tests/ccip/ccip_router_test.go @@ -182,13 +182,7 @@ func TestCCIPRouter(t *testing.T) { }) t.Run("token-pool", func(t *testing.T) { - token0.PoolProgram = config.CcipTokenPoolProgram token0.AdditionalAccounts = append(token0.AdditionalAccounts, solana.MemoProgramID) // add test additional accounts in pool interactions - var err error - token0.PoolConfig, err = tokens.TokenPoolConfigAddress(token0.Mint.PublicKey()) - require.NoError(t, err) - token0.PoolSigner, err = tokens.TokenPoolSignerAddress(token0.Mint.PublicKey()) - require.NoError(t, err) ixInit, err := token_pool.NewInitializeInstruction( token_pool.BurnAndMint_PoolType, @@ -201,15 +195,19 @@ func TestCCIPRouter(t *testing.T) { ).ValidateAndBuild() require.NoError(t, err) - ixAta, addr, err := tokens.CreateAssociatedTokenAccount(token0.Program, token0.Mint.PublicKey(), token0.PoolSigner, tokenPoolAdmin.PublicKey()) + ixAta0, addr0, err := tokens.CreateAssociatedTokenAccount(token0.Program, token0.Mint.PublicKey(), token0.PoolSigner, tokenPoolAdmin.PublicKey()) require.NoError(t, err) - token0.PoolTokenAccount = addr + token0.PoolTokenAccount = addr0 token0.User[token0.PoolSigner] = token0.PoolTokenAccount + ixAta1, addr1, err := tokens.CreateAssociatedTokenAccount(token1.Program, token1.Mint.PublicKey(), token1.PoolSigner, tokenPoolAdmin.PublicKey()) + require.NoError(t, err) + token1.PoolTokenAccount = addr1 + token1.User[token1.PoolSigner] = token1.PoolTokenAccount ixAuth, err := tokens.SetTokenMintAuthority(token0.Program, token0.PoolSigner, token0.Mint.PublicKey(), tokenPoolAdmin.PublicKey()) require.NoError(t, err) - testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixInit, ixAta, ixAuth}, tokenPoolAdmin, config.DefaultCommitment) + testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixInit, ixAta0, ixAta1, ixAuth}, tokenPoolAdmin, config.DefaultCommitment) // Lookup Table for Tokens require.NoError(t, token0.SetupLookupTable(ctx, solanaGoClient, tokenPoolAdmin)) @@ -1464,8 +1462,9 @@ func TestCCIPRouter(t *testing.T) { t.Run("When any user wants to set up the pool, it fails", func(t *testing.T) { instruction, err := ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, user.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1477,8 +1476,9 @@ func TestCCIPRouter(t *testing.T) { transmitter := getTransmitter() instruction, err := ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, transmitter.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1489,8 +1489,22 @@ func TestCCIPRouter(t *testing.T) { t.Run("When admin wants to set up the pool, it fails", func(t *testing.T) { instruction, err := ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), + token0.WritableIndexes, + token0.AdminRegistry, token0.PoolLookupTable, + anotherAdmin.PublicKey(), + ).ValidateAndBuild() + require.NoError(t, err) + + testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherAdmin, config.DefaultCommitment, []string{ccip_router.Unauthorized_CcipRouterError.String()}) + }) + + t.Run("When setting pool to incorrect addresses in lookup table, it fails", func(t *testing.T) { + instruction, err := ccip_router.NewSetPoolInstruction( + token0.Mint.PublicKey(), + token0.WritableIndexes, token0.AdminRegistry, + token1.PoolLookupTable, // accounts do not match the expected mint related accounts anotherAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1501,8 +1515,9 @@ func TestCCIPRouter(t *testing.T) { t.Run("When Token Pool Admin wants to set up the pool, it succeeds", func(t *testing.T) { base := ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, tokenPoolAdmin.PublicKey(), ) @@ -1525,8 +1540,9 @@ func TestCCIPRouter(t *testing.T) { t.Run("When Token Pool Admin wants to set up the pool again to zero, it is none", func(t *testing.T) { instruction, err := ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - solana.PublicKey{}, + token0.WritableIndexes, token0.AdminRegistry, + solana.PublicKey{}, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1545,8 +1561,9 @@ func TestCCIPRouter(t *testing.T) { // Rollback to previous state instruction, err = ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1591,8 +1608,9 @@ func TestCCIPRouter(t *testing.T) { // check if the admin is still the same instruction, err = ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1602,8 +1620,9 @@ func TestCCIPRouter(t *testing.T) { // new one cant make changes yet instruction, err = ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, anotherTokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1633,8 +1652,9 @@ func TestCCIPRouter(t *testing.T) { // check old admin can not make changes anymore instruction, err = ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1644,8 +1664,9 @@ func TestCCIPRouter(t *testing.T) { // new one can make changes now instruction, err = ccip_router.NewSetPoolInstruction( token0.Mint.PublicKey(), - token0.PoolLookupTable, + token0.WritableIndexes, token0.AdminRegistry, + token0.PoolLookupTable, anotherTokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1732,8 +1753,9 @@ func TestCCIPRouter(t *testing.T) { t.Run("When Mint Authority wants to set up the pool, it succeeds", func(t *testing.T) { instruction, err := ccip_router.NewSetPoolInstruction( token1.Mint.PublicKey(), - token1.PoolLookupTable, + token1.WritableIndexes, token1.AdminRegistry, + token1.PoolLookupTable, anotherTokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1786,8 +1808,9 @@ func TestCCIPRouter(t *testing.T) { // check if the admin is still the same instruction, err = ccip_router.NewSetPoolInstruction( token1.Mint.PublicKey(), - token1.PoolLookupTable, + token1.WritableIndexes, token1.AdminRegistry, + token1.PoolLookupTable, anotherTokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1797,8 +1820,9 @@ func TestCCIPRouter(t *testing.T) { // new one cant make changes yet instruction, err = ccip_router.NewSetPoolInstruction( token1.Mint.PublicKey(), - token1.PoolLookupTable, + token1.WritableIndexes, token1.AdminRegistry, + token1.PoolLookupTable, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1828,8 +1852,9 @@ func TestCCIPRouter(t *testing.T) { // check old admin can not make changes anymore instruction, err = ccip_router.NewSetPoolInstruction( token1.Mint.PublicKey(), - token1.PoolLookupTable, + token1.WritableIndexes, token1.AdminRegistry, + token1.PoolLookupTable, anotherTokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -1839,8 +1864,9 @@ func TestCCIPRouter(t *testing.T) { // new one can make changes now instruction, err = ccip_router.NewSetPoolInstruction( token1.Mint.PublicKey(), - token1.PoolLookupTable, + token1.WritableIndexes, token1.AdminRegistry, + token1.PoolLookupTable, tokenPoolAdmin.PublicKey(), ).ValidateAndBuild() require.NoError(t, err) @@ -2665,7 +2691,7 @@ func TestCCIPRouter(t *testing.T) { inputs := []struct { name string index uint - replaceWith solana.PublicKey // default to zero address + replaceWith *solana.AccountMeta // default to zero address errorStr ccip_router.CcipRouterError }{ { @@ -2691,9 +2717,21 @@ func TestCCIPRouter(t *testing.T) { { name: "is pool config but for wrong token", index: 6, - replaceWith: token1.PoolConfig, + replaceWith: solana.Meta(token1.PoolConfig), errorStr: ccip_router.InvalidInputsPoolAccounts_CcipRouterError, }, + { + name: "is pool config but missing write permissions", + index: 6, + replaceWith: solana.Meta(token0.PoolConfig), + errorStr: ccip_router.InvalidInputsLookupTableAccountWritable_CcipRouterError, + }, + { + name: "is pool lookup table but has write permissions", + index: 3, + replaceWith: solana.Meta(token0.PoolLookupTable).WRITE(), + errorStr: ccip_router.InvalidInputsLookupTableAccountWritable_CcipRouterError, + }, { name: "incorrect pool signer", index: 8, @@ -2712,7 +2750,7 @@ func TestCCIPRouter(t *testing.T) { { name: "incorrect token pool lookup table", index: 3, - replaceWith: token1.PoolLookupTable, + replaceWith: solana.Meta(token1.PoolLookupTable), errorStr: ccip_router.InvalidInputsLookupTableAccounts_CcipRouterError, }, { @@ -2756,10 +2794,13 @@ func TestCCIPRouter(t *testing.T) { tokenMetas, addressTables, err := tokens.ParseTokenLookupTable(ctx, solanaGoClient, token0, userTokenAccount) require.NoError(t, err) // replace account meta with invalid account to trigger error or append + if in.replaceWith == nil { + in.replaceWith = solana.Meta(solana.PublicKey{}) // default 0 address + } if in.index >= uint(len(tokenMetas)) { - tokenMetas = append(tokenMetas, solana.Meta(in.replaceWith)) + tokenMetas = append(tokenMetas, in.replaceWith) } else { - tokenMetas[in.index] = solana.Meta(in.replaceWith) + tokenMetas[in.index] = in.replaceWith } tx.AccountMetaSlice = append(tx.AccountMetaSlice, tokenMetas...) diff --git a/chains/solana/gobindings/ccip_router/SetPool.go b/chains/solana/gobindings/ccip_router/SetPool.go index dce1c1ecc..ab5e969fe 100644 --- a/chains/solana/gobindings/ccip_router/SetPool.go +++ b/chains/solana/gobindings/ccip_router/SetPool.go @@ -19,20 +19,23 @@ import ( // * `ctx` - The context containing the accounts required for setting the pool. // * `mint` - The public key of the token mint. // * `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool. +// * `is_writable` - index of account in lookup table that is writable type SetPool struct { Mint *ag_solanago.PublicKey - PoolLookupTable *ag_solanago.PublicKey + WritableIndexes *[]byte // [0] = [WRITE] tokenAdminRegistry // - // [1] = [WRITE, SIGNER] authority + // [1] = [] poolLookuptable + // + // [2] = [WRITE, SIGNER] authority ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } // NewSetPoolInstructionBuilder creates a new `SetPool` instruction builder. func NewSetPoolInstructionBuilder() *SetPool { nd := &SetPool{ - AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), } return nd } @@ -43,9 +46,9 @@ func (inst *SetPool) SetMint(mint ag_solanago.PublicKey) *SetPool { return inst } -// SetPoolLookupTable sets the "poolLookupTable" parameter. -func (inst *SetPool) SetPoolLookupTable(poolLookupTable ag_solanago.PublicKey) *SetPool { - inst.PoolLookupTable = &poolLookupTable +// SetWritableIndexes sets the "writableIndexes" parameter. +func (inst *SetPool) SetWritableIndexes(writableIndexes []byte) *SetPool { + inst.WritableIndexes = &writableIndexes return inst } @@ -60,15 +63,26 @@ func (inst *SetPool) GetTokenAdminRegistryAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } +// SetPoolLookuptableAccount sets the "poolLookuptable" account. +func (inst *SetPool) SetPoolLookuptableAccount(poolLookuptable ag_solanago.PublicKey) *SetPool { + inst.AccountMetaSlice[1] = ag_solanago.Meta(poolLookuptable) + return inst +} + +// GetPoolLookuptableAccount gets the "poolLookuptable" account. +func (inst *SetPool) GetPoolLookuptableAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + // SetAuthorityAccount sets the "authority" account. func (inst *SetPool) SetAuthorityAccount(authority ag_solanago.PublicKey) *SetPool { - 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 *SetPool) GetAuthorityAccount() *ag_solanago.AccountMeta { - return inst.AccountMetaSlice[1] + return inst.AccountMetaSlice[2] } func (inst SetPool) Build() *Instruction { @@ -94,8 +108,8 @@ func (inst *SetPool) Validate() error { if inst.Mint == nil { return errors.New("Mint parameter is not set") } - if inst.PoolLookupTable == nil { - return errors.New("PoolLookupTable parameter is not set") + if inst.WritableIndexes == nil { + return errors.New("WritableIndexes parameter is not set") } } @@ -105,6 +119,9 @@ func (inst *SetPool) Validate() error { return errors.New("accounts.TokenAdminRegistry is not set") } if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.PoolLookuptable is not set") + } + if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.Authority is not set") } } @@ -122,13 +139,14 @@ func (inst *SetPool) EncodeToTree(parent ag_treeout.Branches) { // Parameters of the instruction: instructionBranch.Child("Params[len=2]").ParentFunc(func(paramsBranch ag_treeout.Branches) { paramsBranch.Child(ag_format.Param(" Mint", *inst.Mint)) - paramsBranch.Child(ag_format.Param("PoolLookupTable", *inst.PoolLookupTable)) + paramsBranch.Child(ag_format.Param("WritableIndexes", *inst.WritableIndexes)) }) // 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("tokenAdminRegistry", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" poolLookuptable", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.AccountMetaSlice[2])) }) }) }) @@ -140,8 +158,8 @@ func (obj SetPool) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { if err != nil { return err } - // Serialize `PoolLookupTable` param: - err = encoder.Encode(obj.PoolLookupTable) + // Serialize `WritableIndexes` param: + err = encoder.Encode(obj.WritableIndexes) if err != nil { return err } @@ -153,8 +171,8 @@ func (obj *SetPool) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) if err != nil { return err } - // Deserialize `PoolLookupTable`: - err = decoder.Decode(&obj.PoolLookupTable) + // Deserialize `WritableIndexes`: + err = decoder.Decode(&obj.WritableIndexes) if err != nil { return err } @@ -165,13 +183,15 @@ func (obj *SetPool) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) func NewSetPoolInstruction( // Parameters: mint ag_solanago.PublicKey, - poolLookupTable ag_solanago.PublicKey, + writableIndexes []byte, // Accounts: tokenAdminRegistry ag_solanago.PublicKey, + poolLookuptable ag_solanago.PublicKey, authority ag_solanago.PublicKey) *SetPool { return NewSetPoolInstructionBuilder(). SetMint(mint). - SetPoolLookupTable(poolLookupTable). + SetWritableIndexes(writableIndexes). SetTokenAdminRegistryAccount(tokenAdminRegistry). + SetPoolLookuptableAccount(poolLookuptable). SetAuthorityAccount(authority) } diff --git a/chains/solana/gobindings/ccip_router/accounts.go b/chains/solana/gobindings/ccip_router/accounts.go index 8fa91580a..b48a9dfa7 100644 --- a/chains/solana/gobindings/ccip_router/accounts.go +++ b/chains/solana/gobindings/ccip_router/accounts.go @@ -687,6 +687,7 @@ type TokenAdminRegistry struct { Administrator ag_solanago.PublicKey PendingAdministrator ag_solanago.PublicKey LookupTable ag_solanago.PublicKey + WritableIndexes [2]ag_binary.Uint128 } var TokenAdminRegistryDiscriminator = [8]byte{70, 92, 207, 200, 76, 17, 57, 114} @@ -717,6 +718,11 @@ func (obj TokenAdminRegistry) MarshalWithEncoder(encoder *ag_binary.Encoder) (er if err != nil { return err } + // Serialize `WritableIndexes` param: + err = encoder.Encode(obj.WritableIndexes) + if err != nil { + return err + } return nil } @@ -754,5 +760,10 @@ func (obj *TokenAdminRegistry) UnmarshalWithDecoder(decoder *ag_binary.Decoder) if err != nil { return err } + // Deserialize `WritableIndexes`: + err = decoder.Decode(&obj.WritableIndexes) + if err != nil { + return err + } return nil } diff --git a/chains/solana/gobindings/ccip_router/instructions.go b/chains/solana/gobindings/ccip_router/instructions.go index 7ab9b9800..56d66dac6 100644 --- a/chains/solana/gobindings/ccip_router/instructions.go +++ b/chains/solana/gobindings/ccip_router/instructions.go @@ -202,6 +202,7 @@ var ( // * `ctx` - The context containing the accounts required for setting the pool. // * `mint` - The public key of the token mint. // * `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool. + // * `is_writable` - index of account in lookup table that is writable Instruction_SetPool = ag_binary.TypeID([8]byte{119, 30, 14, 180, 115, 225, 167, 238}) // Transfers the admin role of the token admin registry to a new admin. diff --git a/chains/solana/gobindings/ccip_router/types.go b/chains/solana/gobindings/ccip_router/types.go index dfeeb08c4..19c5a3a7b 100644 --- a/chains/solana/gobindings/ccip_router/types.go +++ b/chains/solana/gobindings/ccip_router/types.go @@ -1946,6 +1946,7 @@ const ( InvalidInputsConfigAccounts_CcipRouterError InvalidInputsTokenAdminRegistryAccounts_CcipRouterError InvalidInputsLookupTableAccounts_CcipRouterError + InvalidInputsLookupTableAccountWritable_CcipRouterError InvalidInputsTokenAmount_CcipRouterError OfframpReleaseMintBalanceMismatch_CcipRouterError OfframpInvalidDataLength_CcipRouterError @@ -2003,6 +2004,8 @@ func (value CcipRouterError) String() string { return "InvalidInputsTokenAdminRegistryAccounts" case InvalidInputsLookupTableAccounts_CcipRouterError: return "InvalidInputsLookupTableAccounts" + case InvalidInputsLookupTableAccountWritable_CcipRouterError: + return "InvalidInputsLookupTableAccountWritable" case InvalidInputsTokenAmount_CcipRouterError: return "InvalidInputsTokenAmount" case OfframpReleaseMintBalanceMismatch_CcipRouterError: diff --git a/chains/solana/utils/tokens/tokenpool.go b/chains/solana/utils/tokens/tokenpool.go index f0ee0e8f8..16ed067de 100644 --- a/chains/solana/utils/tokens/tokenpool.go +++ b/chains/solana/utils/tokens/tokenpool.go @@ -3,12 +3,14 @@ package tokens import ( "context" "encoding/binary" + "fmt" "strings" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/token_pool" "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" ) @@ -25,6 +27,7 @@ type TokenPool struct { // pool details PoolProgram, PoolConfig, PoolSigner, PoolTokenAccount solana.PublicKey PoolLookupTable solana.PublicKey + WritableIndexes []uint8 AdditionalAccounts solana.PublicKeySlice @@ -40,15 +43,15 @@ type TokenPool struct { func (tp TokenPool) ToTokenPoolEntries() []solana.PublicKey { list := solana.PublicKeySlice{ - tp.PoolLookupTable, - tp.AdminRegistry, - tp.PoolProgram, - tp.PoolConfig, - tp.PoolTokenAccount, - tp.PoolSigner, - tp.Program, - tp.Mint.PublicKey(), - tp.FeeTokenConfig, + tp.PoolLookupTable, // 0 + tp.AdminRegistry, // 1 + tp.PoolProgram, // 2 + tp.PoolConfig, // 3 - writable + tp.PoolTokenAccount, // 4 - writable + tp.PoolSigner, // 5 + tp.Program, // 6 + tp.Mint.PublicKey(), // 7 - writable + tp.FeeTokenConfig, // 8 } return append(list, tp.AdditionalAccounts...) } @@ -83,13 +86,23 @@ func NewTokenPool(program solana.PublicKey) (TokenPool, error) { Mint: mint, FeeTokenConfig: tokenConfigPda, AdminRegistry: tokenAdminRegistryPDA, + PoolProgram: config.CcipTokenPoolProgram, PoolLookupTable: solana.PublicKey{}, + WritableIndexes: []uint8{3, 4, 7}, // see ToTokenPoolEntries for writable indexes User: map[solana.PublicKey]solana.PublicKey{}, Chain: map[uint64]solana.PublicKey{}, Billing: map[uint64]solana.PublicKey{}, } p.Chain[config.EvmChainSelector] = chainPDA p.Billing[config.EvmChainSelector] = billingPDA + p.PoolConfig, err = TokenPoolConfigAddress(p.Mint.PublicKey()) + if err != nil { + return TokenPool{}, err + } + p.PoolSigner, err = TokenPoolSignerAddress(p.Mint.PublicKey()) + if err != nil { + return TokenPool{}, err + } return p, nil } @@ -169,29 +182,39 @@ func ParseTokenLookupTable(ctx context.Context, client *rpc.Client, token TokenP tokenBillingConfig := token.Billing[config.EvmChainSelector] poolChainConfig := token.Chain[config.EvmChainSelector] + tokenAdminRegistry := ccip_router.TokenAdminRegistry{} + err := common.GetAccountDataBorshInto(ctx, client, token.AdminRegistry, config.DefaultCommitment, &tokenAdminRegistry) + if err != nil { + return nil, nil, err + } + lookupTableEntries, err := common.GetAddressLookupTable(ctx, client, token.PoolLookupTable) if err != nil { return nil, nil, err } + writableBytes := append(tokenAdminRegistry.WritableIndexes[0].Bytes(), tokenAdminRegistry.WritableIndexes[1].Bytes()...) + writableBits := "" + for _, b := range writableBytes { + writableBits += fmt.Sprintf("%08b", b) + } + + lookupTableMeta := []*solana.AccountMeta{} + for i := range lookupTableEntries { + meta := solana.Meta(lookupTableEntries[i]) + + if string(writableBits[i]) == "1" { + meta = meta.WRITE() + } + lookupTableMeta = append(lookupTableMeta, meta) + } + list := []*solana.AccountMeta{ solana.Meta(userTokenAccount).WRITE(), solana.Meta(tokenBillingConfig), solana.Meta(poolChainConfig).WRITE(), - solana.Meta(lookupTableEntries[0]), // lookup table - solana.Meta(lookupTableEntries[1]), // token admin registry - solana.Meta(lookupTableEntries[2]), // PoolProgram - solana.Meta(lookupTableEntries[3]).WRITE(), // PoolConfig - solana.Meta(lookupTableEntries[4]).WRITE(), // PoolTokenAccount - solana.Meta(lookupTableEntries[5]), // PoolSigner - solana.Meta(lookupTableEntries[6]), // TokenProgram - solana.Meta(lookupTableEntries[7]).WRITE(), // Mint - solana.Meta(lookupTableEntries[8]), // FeeTokenConfig - } - - for _, v := range token.AdditionalAccounts { - list = append(list, solana.Meta(v)) } + list = append(list, lookupTableMeta...) addressTables := make(map[solana.PublicKey]solana.PublicKeySlice) addressTables[token.PoolLookupTable] = lookupTableEntries