Browse Source

Token 22: Add functionality to update extra_account_metas after initializing. (#5894)

* Added update function for ExtraAccountMetaList

* Updated interface to handle new update instruction

* Updated Cli to handle update command

* updated example program to handle updating

* Rust fmt trailing whitespace fix

* Removed unused variable

* Added more explicit update instruction doc comment

* Allow for resizing to smaller account size

* Removed system program from update instruction

* Added helper fn to calculate transfer lamports

* Added unit tests for update function

* Added unit test for update instruction

* removed unnecessary  commented out code

* re-added checks on initialization

* turned of zero_init for realloc for performance

* Fixed update doc comments

* Used block-scoping rather than explicit drop()

* Removed unnecessary convert to vec

* refactored updated test into single test

* added additional off-chain test of update instruct

* made on-chain invoke update test to match original

* moved helper function up to others

* refactored create and update with helpers

* rustfmt: fix

* rustfmt: fix

* removed commented out system program in update

* renamed helpers and removed unnecessary helper

* moved test helper up

* fixed test attribute location

* removed multiple init extra account metas in test

* added instruction assert to update test

* renamed transfer address to extra account metas

* rustfmt: comment fix

* clippy: fix

* added update test with simple PDA

* made more changes to updated metas in test

* added check for if extra metas have be initialized

* spelling fix

* fixed initialized condition
tonton-sol 1 year ago
parent
commit
806d558276
4 changed files with 996 additions and 43 deletions
  1. 214 40
      clients/cli/src/main.rs
  2. 64 2
      interface/src/instruction.rs
  3. 68 0
      program/src/processor.rs
  4. 650 1
      program/tests/functional.rs

+ 214 - 40
clients/cli/src/main.rs

@@ -9,15 +9,16 @@ use {
     solana_remote_wallet::remote_wallet::RemoteWalletManager,
     solana_sdk::{
         commitment_config::CommitmentConfig,
-        instruction::AccountMeta,
+        instruction::{AccountMeta, Instruction},
         pubkey::Pubkey,
         signature::{Signature, Signer},
         system_instruction, system_program,
         transaction::Transaction,
     },
-    spl_tlv_account_resolution::state::ExtraAccountMetaList,
+    spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList},
     spl_transfer_hook_interface::{
-        get_extra_account_metas_address, instruction::initialize_extra_account_meta_list,
+        get_extra_account_metas_address,
+        instruction::{initialize_extra_account_meta_list, update_extra_account_meta_list},
     },
     std::{fmt, process::exit, rc::Rc, str::FromStr},
     strum_macros::{EnumString, IntoStaticStr},
@@ -56,6 +57,74 @@ fn clap_is_valid_pubkey(arg: &str) -> Result<(), String> {
     is_valid_pubkey(arg)
 }
 
+// Helper function to calculate the required lamports for rent
+async fn calculate_rent_lamports(
+    rpc_client: &RpcClient,
+    account_address: &Pubkey,
+    account_size: usize,
+) -> Result<u64, Box<dyn std::error::Error>> {
+    let required_lamports = rpc_client
+        .get_minimum_balance_for_rent_exemption(account_size)
+        .await
+        .map_err(|err| format!("error: unable to fetch rent-exemption: {err}"))?;
+    let account_info = rpc_client.get_account(account_address).await;
+    let current_lamports = account_info.map(|a| a.lamports).unwrap_or(0);
+    Ok(required_lamports.saturating_sub(current_lamports))
+}
+
+async fn build_transaction_with_rent_transfer(
+    rpc_client: &RpcClient,
+    payer: &dyn Signer,
+    extra_account_metas_address: &Pubkey,
+    extra_account_metas: &Vec<ExtraAccountMeta>,
+    instruction: Instruction,
+) -> Result<Transaction, Box<dyn std::error::Error>> {
+    let account_size = ExtraAccountMetaList::size_of(extra_account_metas.len())?;
+    let transfer_lamports =
+        calculate_rent_lamports(rpc_client, extra_account_metas_address, account_size).await?;
+
+    let mut instructions = vec![];
+    if transfer_lamports > 0 {
+        instructions.push(system_instruction::transfer(
+            &payer.pubkey(),
+            extra_account_metas_address,
+            transfer_lamports,
+        ));
+    }
+
+    instructions.push(instruction);
+
+    let transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
+
+    Ok(transaction)
+}
+
+async fn sign_and_send_transaction(
+    transaction: &mut Transaction,
+    rpc_client: &RpcClient,
+    payer: &dyn Signer,
+    mint_authority: &dyn Signer,
+) -> Result<Signature, Box<dyn std::error::Error>> {
+    let mut signers = vec![payer];
+    if payer.pubkey() != mint_authority.pubkey() {
+        signers.push(mint_authority);
+    }
+
+    let blockhash = rpc_client
+        .get_latest_blockhash()
+        .await
+        .map_err(|err| format!("error: unable to get latest blockhash: {err}"))?;
+
+    transaction
+        .try_sign(&signers, blockhash)
+        .map_err(|err| format!("error: failed to sign transaction: {err}"))?;
+
+    rpc_client
+        .send_and_confirm_transaction_with_spinner(transaction)
+        .await
+        .map_err(|err| format!("error: send transaction: {err}").into())
+}
+
 struct Config {
     commitment_config: CommitmentConfig,
     default_signer: Box<dyn Signer>,
@@ -72,59 +141,82 @@ async fn process_create_extra_account_metas(
     payer: &dyn Signer,
 ) -> Result<Signature, Box<dyn std::error::Error>> {
     let extra_account_metas_address = get_extra_account_metas_address(token, program_id);
-    let extra_account_metas = transfer_hook_accounts
-        .into_iter()
-        .map(|v| v.into())
-        .collect::<Vec<_>>();
 
-    let length = extra_account_metas.len();
-    let account_size = ExtraAccountMetaList::size_of(length)?;
-    let required_lamports = rpc_client
-        .get_minimum_balance_for_rent_exemption(account_size)
-        .await
-        .map_err(|err| format!("error: unable to fetch rent-exemption: {err}"))?;
+    // Check if the extra meta account has already been initialized
     let extra_account_metas_account = rpc_client.get_account(&extra_account_metas_address).await;
     if let Ok(account) = &extra_account_metas_account {
         if account.owner != system_program::id() {
             return Err(format!("error: extra account metas for mint {token} and program {program_id} already exists").into());
         }
     }
-    let current_lamports = extra_account_metas_account.map(|a| a.lamports).unwrap_or(0);
-    let transfer_lamports = required_lamports.saturating_sub(current_lamports);
 
-    let mut ixs = vec![];
-    if transfer_lamports > 0 {
-        ixs.push(system_instruction::transfer(
-            &payer.pubkey(),
-            &extra_account_metas_address,
-            transfer_lamports,
-        ));
-    }
-    ixs.push(initialize_extra_account_meta_list(
+    let extra_account_metas = transfer_hook_accounts
+        .into_iter()
+        .map(|v| v.into())
+        .collect::<Vec<_>>();
+
+    let instruction = initialize_extra_account_meta_list(
         program_id,
         &extra_account_metas_address,
         token,
         &mint_authority.pubkey(),
         &extra_account_metas,
-    ));
+    );
 
-    let mut transaction = Transaction::new_with_payer(&ixs, Some(&payer.pubkey()));
-    let blockhash = rpc_client
-        .get_latest_blockhash()
-        .await
-        .map_err(|err| format!("error: unable to get latest blockhash: {err}"))?;
-    let mut signers = vec![payer];
-    if payer.pubkey() != mint_authority.pubkey() {
-        signers.push(mint_authority);
+    let mut transaction = build_transaction_with_rent_transfer(
+        rpc_client,
+        payer,
+        &extra_account_metas_address,
+        &extra_account_metas,
+        instruction,
+    )
+    .await?;
+
+    sign_and_send_transaction(&mut transaction, rpc_client, payer, mint_authority).await
+}
+
+async fn process_update_extra_account_metas(
+    rpc_client: &RpcClient,
+    program_id: &Pubkey,
+    token: &Pubkey,
+    transfer_hook_accounts: Vec<AccountMeta>,
+    mint_authority: &dyn Signer,
+    payer: &dyn Signer,
+) -> Result<Signature, Box<dyn std::error::Error>> {
+    let extra_account_metas_address = get_extra_account_metas_address(token, program_id);
+
+    // Check if the extra meta account has been initialized first
+    let extra_account_metas_account = rpc_client.get_account(&extra_account_metas_address).await;
+    if extra_account_metas_account.is_err() {
+        return Err(format!(
+            "error: extra account metas for mint {token} and program {program_id} does not exist"
+        )
+        .into());
     }
-    transaction
-        .try_sign(&signers, blockhash)
-        .map_err(|err| format!("error: failed to sign transaction: {err}"))?;
 
-    rpc_client
-        .send_and_confirm_transaction_with_spinner(&transaction)
-        .await
-        .map_err(|err| format!("error: send transaction: {err}").into())
+    let extra_account_metas = transfer_hook_accounts
+        .into_iter()
+        .map(|v| v.into())
+        .collect::<Vec<_>>();
+
+    let instruction = update_extra_account_meta_list(
+        program_id,
+        &extra_account_metas_address,
+        token,
+        &mint_authority.pubkey(),
+        &extra_account_metas,
+    );
+
+    let mut transaction = build_transaction_with_rent_transfer(
+        rpc_client,
+        payer,
+        &extra_account_metas_address,
+        &extra_account_metas,
+        instruction,
+    )
+    .await?;
+
+    sign_and_send_transaction(&mut transaction, rpc_client, payer, mint_authority).await
 }
 
 #[tokio::main]
@@ -217,6 +309,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
                         .global(true)
                         .help("Filepath or URL to mint-authority keypair [default: client keypair]"),
                 )
+        )
+        .subcommand(
+            Command::new("update-extra-metas")
+                .about("Update the extra account metas account for a transfer hook program")
+                .arg(
+                    Arg::with_name("program_id")
+                        .validator(clap_is_valid_pubkey)
+                        .value_name("TRANSFER_HOOK_PROGRAM")
+                        .takes_value(true)
+                        .index(1)
+                        .required(true)
+                        .help("The transfer hook program id"),
+                )
+                .arg(
+                    Arg::with_name("token")
+                        .validator(clap_is_valid_pubkey)
+                        .value_name("TOKEN_MINT_ADDRESS")
+                        .takes_value(true)
+                        .index(2)
+                        .required(true)
+                        .help("The token mint address for the transfer hook"),
+                )
+                .arg(
+                    Arg::with_name("transfer_hook_account")
+                        .value_parser(parse_transfer_hook_account)
+                        .value_name("PUBKEY:ROLE")
+                        .takes_value(true)
+                        .multiple(true)
+                        .min_values(0)
+                        .index(3)
+                        .help("Additional pubkey(s) required for a transfer hook and their \
+                            role, in the format \"<PUBKEY>:<ROLE>\". The role must be \
+                            \"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".")
+                )
+                .arg(
+                    Arg::new("mint_authority")
+                        .long("mint-authority")
+                        .value_name("KEYPAIR")
+                        .validator(|s| is_valid_signer(s))
+                        .takes_value(true)
+                        .global(true)
+                        .help("Filepath or URL to mint-authority keypair [default: client keypair]"),
+                )
         ).get_matches();
 
     let (command, matches) = app_matches.subcommand().unwrap();
@@ -303,6 +438,45 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
             });
             println!("Signature: {signature}");
         }
+        ("update-extra-metas", arg_matches) => {
+            let program_id = pubkey_of_signer(arg_matches, "program_id", &mut wallet_manager)
+                .unwrap()
+                .unwrap();
+            let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
+                .unwrap()
+                .unwrap();
+            let transfer_hook_accounts = arg_matches
+                .get_many::<AccountMeta>("transfer_hook_account")
+                .unwrap_or_default()
+                .cloned()
+                .collect();
+            let mint_authority = DefaultSigner::new(
+                "mint_authority",
+                matches
+                    .value_of("mint_authority")
+                    .map(|s| s.to_string())
+                    .unwrap_or_else(|| cli_config.keypair_path.clone()),
+            )
+            .signer_from_path(matches, &mut wallet_manager)
+            .unwrap_or_else(|err| {
+                eprintln!("error: {err}");
+                exit(1);
+            });
+            let signature = process_update_extra_account_metas(
+                &rpc_client,
+                &program_id,
+                &token,
+                transfer_hook_accounts,
+                mint_authority.as_ref(),
+                config.default_signer.as_ref(),
+            )
+            .await
+            .unwrap_or_else(|err| {
+                eprintln!("error: send transaction: {err}");
+                exit(1);
+            });
+            println!("Signature: {signature}");
+        }
         _ => unreachable!(),
     };
 

+ 64 - 2
interface/src/instruction.rs

@@ -32,8 +32,9 @@ pub enum TransferHookInstruction {
         /// Amount of tokens to transfer
         amount: u64,
     },
-    /// Initializes the extra account metas on an account, writing into
-    /// the first open TLV space.
+
+    /// Initializes the extra account metas on an account, writing into the
+    /// first open TLV space.
     ///
     /// Accounts expected by this instruction:
     ///
@@ -45,6 +46,19 @@ pub enum TransferHookInstruction {
         /// List of `ExtraAccountMeta`s to write into the account
         extra_account_metas: Vec<ExtraAccountMeta>,
     },
+    /// Updates the extra account metas on an account by overwriting the
+    /// existing list.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   0. `[w]` Account with extra account metas
+    ///   1. `[]` Mint
+    ///   2. `[s]` Mint authority
+    UpdateExtraAccountMetaList {
+        /// The new list of `ExtraAccountMetas` to overwrite the existing entry
+        /// in the account.
+        extra_account_metas: Vec<ExtraAccountMeta>,
+    },
 }
 /// TLV instruction type only used to define the discriminator. The actual data
 /// is entirely managed by `ExtraAccountMetaList`, and it is the only data
@@ -59,6 +73,12 @@ pub struct ExecuteInstruction;
 #[discriminator_hash_input("spl-transfer-hook-interface:initialize-extra-account-metas")]
 pub struct InitializeExtraAccountMetaListInstruction;
 
+/// TLV instruction type used to update extra account metas
+/// for the transfer hook
+#[derive(SplDiscriminate)]
+#[discriminator_hash_input("spl-transfer-hook-interface:update-extra-account-metas")]
+pub struct UpdateExtraAccountMetaListInstruction;
+
 impl TransferHookInstruction {
     /// Unpacks a byte buffer into a
     /// [TransferHookInstruction](enum.TransferHookInstruction.html).
@@ -83,6 +103,13 @@ impl TransferHookInstruction {
                     extra_account_metas,
                 }
             }
+            UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => {
+                let pod_slice = PodSlice::<ExtraAccountMeta>::unpack(rest)?;
+                let extra_account_metas = pod_slice.data().to_vec();
+                Self::UpdateExtraAccountMetaList {
+                    extra_account_metas,
+                }
+            }
             _ => return Err(ProgramError::InvalidInstructionData),
         })
     }
@@ -105,6 +132,15 @@ impl TransferHookInstruction {
                 buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
                 buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
             }
+            Self::UpdateExtraAccountMetaList {
+                extra_account_metas,
+            } => {
+                buf.extend_from_slice(
+                    UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE,
+                );
+                buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
+                buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
+            }
         };
         buf
     }
@@ -189,6 +225,32 @@ pub fn initialize_extra_account_meta_list(
     }
 }
 
+/// Creates a `UpdateExtraAccountMetaList` instruction.
+pub fn update_extra_account_meta_list(
+    program_id: &Pubkey,
+    extra_account_metas_pubkey: &Pubkey,
+    mint_pubkey: &Pubkey,
+    authority_pubkey: &Pubkey,
+    extra_account_metas: &[ExtraAccountMeta],
+) -> Instruction {
+    let data = TransferHookInstruction::UpdateExtraAccountMetaList {
+        extra_account_metas: extra_account_metas.to_vec(),
+    }
+    .pack();
+
+    let accounts = vec![
+        AccountMeta::new(*extra_account_metas_pubkey, false),
+        AccountMeta::new_readonly(*mint_pubkey, false),
+        AccountMeta::new_readonly(*authority_pubkey, true),
+    ];
+
+    Instruction {
+        program_id: *program_id,
+        accounts,
+        data,
+    }
+}
+
 #[cfg(test)]
 mod test {
     use {super::*, crate::NAMESPACE, solana_program::hash, spl_pod::bytemuck::pod_from_bytes};

+ 68 - 0
program/src/processor.rs

@@ -134,6 +134,68 @@ pub fn process_initialize_extra_account_meta_list(
     Ok(())
 }
 
+/// Processes a
+/// [UpdateExtraAccountMetaList](enum.TransferHookInstruction.html)
+/// instruction.
+pub fn process_update_extra_account_meta_list(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    extra_account_metas: &[ExtraAccountMeta],
+) -> ProgramResult {
+    let account_info_iter = &mut accounts.iter();
+
+    let extra_account_metas_info = next_account_info(account_info_iter)?;
+    let mint_info = next_account_info(account_info_iter)?;
+    let authority_info = next_account_info(account_info_iter)?;
+
+    // check that the mint authority is valid without fully deserializing
+    let mint_data = mint_info.try_borrow_data()?;
+    let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
+    let mint_authority = mint
+        .base
+        .mint_authority
+        .ok_or(TransferHookError::MintHasNoMintAuthority)?;
+
+    // Check signers
+    if !authority_info.is_signer {
+        return Err(ProgramError::MissingRequiredSignature);
+    }
+    if *authority_info.key != mint_authority {
+        return Err(TransferHookError::IncorrectMintAuthority.into());
+    }
+
+    // Check validation account
+    let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
+    if expected_validation_address != *extra_account_metas_info.key {
+        return Err(ProgramError::InvalidSeeds);
+    }
+
+    // Check if the extra metas have been initialized
+    let min_account_size = ExtraAccountMetaList::size_of(0)?;
+    let original_account_size = extra_account_metas_info.data_len();
+    if program_id != extra_account_metas_info.owner || original_account_size < min_account_size {
+        return Err(ProgramError::UninitializedAccount);
+    }
+
+    // If the new extra_account_metas length is different, resize the account and
+    // update
+    let length = extra_account_metas.len();
+    let account_size = ExtraAccountMetaList::size_of(length)?;
+    if account_size >= original_account_size {
+        extra_account_metas_info.realloc(account_size, false)?;
+        let mut data = extra_account_metas_info.try_borrow_mut_data()?;
+        ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, extra_account_metas)?;
+    } else {
+        {
+            let mut data = extra_account_metas_info.try_borrow_mut_data()?;
+            ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, extra_account_metas)?;
+        }
+        extra_account_metas_info.realloc(account_size, false)?;
+    }
+
+    Ok(())
+}
+
 /// Processes an [Instruction](enum.Instruction.html).
 pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
     let instruction = TransferHookInstruction::unpack(input)?;
@@ -149,5 +211,11 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P
             msg!("Instruction: InitializeExtraAccountMetaList");
             process_initialize_extra_account_meta_list(program_id, accounts, &extra_account_metas)
         }
+        TransferHookInstruction::UpdateExtraAccountMetaList {
+            extra_account_metas,
+        } => {
+            msg!("Instruction: UpdateExtraAccountMetaList");
+            process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas)
+        }
     }
 }

+ 650 - 1
program/tests/functional.rs

@@ -28,7 +28,10 @@ use {
     spl_transfer_hook_interface::{
         error::TransferHookError,
         get_extra_account_metas_address,
-        instruction::{execute_with_extra_account_metas, initialize_extra_account_meta_list},
+        instruction::{
+            execute_with_extra_account_metas, initialize_extra_account_meta_list,
+            update_extra_account_meta_list,
+        },
         onchain,
     },
 };
@@ -747,3 +750,649 @@ async fn fail_without_transferring_flag() {
         )
     );
 }
+
+#[tokio::test]
+async fn success_on_chain_invoke_with_updated_extra_account_metas() {
+    let hook_program_id = Pubkey::new_unique();
+    let mut program_test = setup(&hook_program_id);
+    let program_id = Pubkey::new_unique();
+    program_test.add_program(
+        "test_cpi_program",
+        program_id,
+        processor!(process_instruction),
+    );
+
+    let token_program_id = spl_token_2022::id();
+    let wallet = Keypair::new();
+    let mint_address = Pubkey::new_unique();
+    let mint_authority = Keypair::new();
+    let mint_authority_pubkey = mint_authority.pubkey();
+    let source = Pubkey::new_unique();
+    let destination = Pubkey::new_unique();
+    let decimals = 2;
+    let amount = 0u64;
+
+    setup_token_accounts(
+        &mut program_test,
+        &token_program_id,
+        &mint_address,
+        &mint_authority_pubkey,
+        &source,
+        &destination,
+        &wallet.pubkey(),
+        decimals,
+        true,
+    );
+
+    let extra_account_metas_address =
+        get_extra_account_metas_address(&mint_address, &hook_program_id);
+    let writable_pubkey = Pubkey::new_unique();
+
+    // Create an initial account metas list
+    let init_extra_account_metas = [
+        ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: b"init-seed-prefix".to_vec(),
+                },
+                Seed::AccountKey { index: 0 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::InstructionData {
+                    index: 8,  // After instruction discriminator
+                    length: 8, // `u64` (amount)
+                },
+                Seed::AccountKey { index: 2 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(),
+    ];
+
+    let mut context = program_test.start_with_context().await;
+    let rent = context.banks_client.get_rent().await.unwrap();
+    let rent_lamports = rent
+        .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap());
+    let init_transaction = Transaction::new_signed_with_payer(
+        &[
+            system_instruction::transfer(
+                &context.payer.pubkey(),
+                &extra_account_metas_address,
+                rent_lamports,
+            ),
+            initialize_extra_account_meta_list(
+                &hook_program_id,
+                &extra_account_metas_address,
+                &mint_address,
+                &mint_authority_pubkey,
+                &init_extra_account_metas,
+            ),
+        ],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+
+    context
+        .banks_client
+        .process_transaction(init_transaction)
+        .await
+        .unwrap();
+
+    // Create an updated account metas list
+    let updated_extra_account_metas = [
+        ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: b"updated-seed-prefix".to_vec(),
+                },
+                Seed::AccountKey { index: 0 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::InstructionData {
+                    index: 8,  // After instruction discriminator
+                    length: 8, // `u64` (amount)
+                },
+                Seed::AccountKey { index: 2 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(),
+    ];
+
+    let rent = context.banks_client.get_rent().await.unwrap();
+    let rent_lamports = rent
+        .minimum_balance(ExtraAccountMetaList::size_of(updated_extra_account_metas.len()).unwrap());
+    let update_transaction = Transaction::new_signed_with_payer(
+        &[
+            system_instruction::transfer(
+                &context.payer.pubkey(),
+                &extra_account_metas_address,
+                rent_lamports,
+            ),
+            update_extra_account_meta_list(
+                &hook_program_id,
+                &extra_account_metas_address,
+                &mint_address,
+                &mint_authority_pubkey,
+                &updated_extra_account_metas,
+            ),
+        ],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+
+    context
+        .banks_client
+        .process_transaction(update_transaction)
+        .await
+        .unwrap();
+
+    let updated_extra_pda_1 = Pubkey::find_program_address(
+        &[
+            b"updated-seed-prefix", // Literal prefix
+            source.as_ref(),        // Account at index 0
+        ],
+        &hook_program_id,
+    )
+    .0;
+    let extra_pda_2 = Pubkey::find_program_address(
+        &[
+            &amount.to_le_bytes(), // Instruction data bytes 8 to 16
+            destination.as_ref(),  // Account at index 2
+        ],
+        &hook_program_id,
+    )
+    .0;
+
+    let test_updated_extra_account_metas = [
+        AccountMeta::new_readonly(sysvar::instructions::id(), false),
+        AccountMeta::new_readonly(mint_authority_pubkey, true),
+        AccountMeta::new(updated_extra_pda_1, false),
+        AccountMeta::new(extra_pda_2, false),
+        AccountMeta::new(writable_pubkey, false),
+    ];
+
+    // Use updated account metas list
+    let mut test_instruction = execute_with_extra_account_metas(
+        &program_id,
+        &source,
+        &mint_address,
+        &destination,
+        &wallet.pubkey(),
+        &extra_account_metas_address,
+        &test_updated_extra_account_metas,
+        amount,
+    );
+    test_instruction
+        .accounts
+        .insert(0, AccountMeta::new_readonly(hook_program_id, false));
+    let transaction = Transaction::new_signed_with_payer(
+        &[test_instruction],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+
+    context
+        .banks_client
+        .process_transaction(transaction)
+        .await
+        .unwrap();
+}
+
+#[tokio::test]
+async fn success_execute_with_updated_extra_account_metas() {
+    let program_id = Pubkey::new_unique();
+    let mut program_test = setup(&program_id);
+
+    let token_program_id = spl_token_2022::id();
+    let wallet = Keypair::new();
+    let mint_address = Pubkey::new_unique();
+    let mint_authority = Keypair::new();
+    let mint_authority_pubkey = mint_authority.pubkey();
+    let source = Pubkey::new_unique();
+    let destination = Pubkey::new_unique();
+    let decimals = 2;
+    let amount = 0u64;
+
+    setup_token_accounts(
+        &mut program_test,
+        &token_program_id,
+        &mint_address,
+        &mint_authority_pubkey,
+        &source,
+        &destination,
+        &wallet.pubkey(),
+        decimals,
+        true,
+    );
+
+    let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id);
+
+    let writable_pubkey = Pubkey::new_unique();
+
+    let init_extra_account_metas = [
+        ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: b"seed-prefix".to_vec(),
+                },
+                Seed::AccountKey { index: 0 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::InstructionData {
+                    index: 8,  // After instruction discriminator
+                    length: 8, // `u64` (amount)
+                },
+                Seed::AccountKey { index: 2 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(),
+    ];
+
+    let extra_pda_1 = Pubkey::find_program_address(
+        &[
+            b"seed-prefix",  // Literal prefix
+            source.as_ref(), // Account at index 0
+        ],
+        &program_id,
+    )
+    .0;
+
+    let extra_pda_2 = Pubkey::find_program_address(
+        &[
+            &amount.to_le_bytes(), // Instruction data bytes 8 to 16
+            destination.as_ref(),  // Account at index 2
+        ],
+        &program_id,
+    )
+    .0;
+
+    let init_account_metas = [
+        AccountMeta::new_readonly(sysvar::instructions::id(), false),
+        AccountMeta::new_readonly(mint_authority_pubkey, true),
+        AccountMeta::new(extra_pda_1, false),
+        AccountMeta::new(extra_pda_2, false),
+        AccountMeta::new(writable_pubkey, false),
+    ];
+
+    let mut context = program_test.start_with_context().await;
+    let rent = context.banks_client.get_rent().await.unwrap();
+    let rent_lamports = rent
+        .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap());
+    let transaction = Transaction::new_signed_with_payer(
+        &[
+            system_instruction::transfer(
+                &context.payer.pubkey(),
+                &extra_account_metas_address,
+                rent_lamports,
+            ),
+            initialize_extra_account_meta_list(
+                &program_id,
+                &extra_account_metas_address,
+                &mint_address,
+                &mint_authority_pubkey,
+                &init_extra_account_metas,
+            ),
+        ],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+
+    context
+        .banks_client
+        .process_transaction(transaction)
+        .await
+        .unwrap();
+
+    let updated_amount = 1u64;
+    let updated_writable_pubkey = Pubkey::new_unique();
+
+    // Create updated extra account metas
+    let updated_extra_account_metas = [
+        ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: b"updated-seed-prefix".to_vec(),
+                },
+                Seed::AccountKey { index: 0 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::InstructionData {
+                    index: 8,  // After instruction discriminator
+                    length: 8, // `u64` (amount)
+                },
+                Seed::AccountKey { index: 2 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+        ExtraAccountMeta::new_with_pubkey(&updated_writable_pubkey, false, true).unwrap(),
+        ExtraAccountMeta::new_with_seeds(
+            &[
+                Seed::Literal {
+                    bytes: b"new-seed-prefix".to_vec(),
+                },
+                Seed::AccountKey { index: 0 },
+            ],
+            false,
+            true,
+        )
+        .unwrap(),
+    ];
+
+    let updated_extra_pda_1 = Pubkey::find_program_address(
+        &[
+            b"updated-seed-prefix", // Literal prefix
+            source.as_ref(),        // Account at index 0
+        ],
+        &program_id,
+    )
+    .0;
+
+    let updated_extra_pda_2 = Pubkey::find_program_address(
+        &[
+            &updated_amount.to_le_bytes(), // Instruction data bytes 8 to 16
+            destination.as_ref(),          // Account at index 2
+        ],
+        &program_id,
+    )
+    .0;
+
+    // add another PDA
+    let new_extra_pda = Pubkey::find_program_address(
+        &[
+            b"new-seed-prefix", // Literal prefix
+            source.as_ref(),    // Account at index 0
+        ],
+        &program_id,
+    )
+    .0;
+
+    let updated_account_metas = [
+        AccountMeta::new_readonly(sysvar::instructions::id(), false),
+        AccountMeta::new_readonly(mint_authority_pubkey, true),
+        AccountMeta::new(updated_extra_pda_1, false),
+        AccountMeta::new(updated_extra_pda_2, false),
+        AccountMeta::new(updated_writable_pubkey, false),
+        AccountMeta::new(new_extra_pda, false),
+    ];
+
+    let update_transaction = Transaction::new_signed_with_payer(
+        &[
+            system_instruction::transfer(
+                &context.payer.pubkey(),
+                &extra_account_metas_address,
+                rent_lamports,
+            ),
+            update_extra_account_meta_list(
+                &program_id,
+                &extra_account_metas_address,
+                &mint_address,
+                &mint_authority_pubkey,
+                &updated_extra_account_metas,
+            ),
+        ],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+
+    context
+        .banks_client
+        .process_transaction(update_transaction)
+        .await
+        .unwrap();
+
+    // fail with initial account metas list
+    {
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &init_account_metas,
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer, &mint_authority],
+            context.last_blockhash,
+        );
+        let error = context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err()
+            .unwrap();
+        assert_eq!(
+            error,
+            TransactionError::InstructionError(
+                0,
+                InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32),
+            )
+        );
+    }
+
+    // fail with missing account
+    {
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &updated_account_metas[..2],
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer, &mint_authority],
+            context.last_blockhash,
+        );
+        let error = context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err()
+            .unwrap();
+        assert_eq!(
+            error,
+            TransactionError::InstructionError(
+                0,
+                InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32),
+            )
+        );
+    }
+
+    // fail with wrong account
+    {
+        let extra_account_metas = [
+            AccountMeta::new_readonly(sysvar::instructions::id(), false),
+            AccountMeta::new_readonly(mint_authority_pubkey, true),
+            AccountMeta::new(updated_extra_pda_1, false),
+            AccountMeta::new(updated_extra_pda_2, false),
+            AccountMeta::new(Pubkey::new_unique(), false),
+        ];
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &extra_account_metas,
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer, &mint_authority],
+            context.last_blockhash,
+        );
+        let error = context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err()
+            .unwrap();
+        assert_eq!(
+            error,
+            TransactionError::InstructionError(
+                0,
+                InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32),
+            )
+        );
+    }
+
+    // fail with wrong PDA
+    let wrong_pda_2 = Pubkey::find_program_address(
+        &[
+            &99u64.to_le_bytes(), // Wrong data
+            destination.as_ref(),
+        ],
+        &program_id,
+    )
+    .0;
+    {
+        let extra_account_metas = [
+            AccountMeta::new_readonly(sysvar::instructions::id(), false),
+            AccountMeta::new_readonly(mint_authority_pubkey, true),
+            AccountMeta::new(updated_extra_pda_1, false),
+            AccountMeta::new(wrong_pda_2, false),
+            AccountMeta::new(writable_pubkey, false),
+        ];
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &extra_account_metas,
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer, &mint_authority],
+            context.last_blockhash,
+        );
+        let error = context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err()
+            .unwrap();
+        assert_eq!(
+            error,
+            TransactionError::InstructionError(
+                0,
+                InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32),
+            )
+        );
+    }
+
+    // fail with not signer
+    {
+        let extra_account_metas = [
+            AccountMeta::new_readonly(sysvar::instructions::id(), false),
+            AccountMeta::new_readonly(mint_authority_pubkey, false),
+            AccountMeta::new(updated_extra_pda_1, false),
+            AccountMeta::new(updated_extra_pda_2, false),
+            AccountMeta::new(writable_pubkey, false),
+        ];
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &extra_account_metas,
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer],
+            context.last_blockhash,
+        );
+        let error = context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err()
+            .unwrap();
+        assert_eq!(
+            error,
+            TransactionError::InstructionError(
+                0,
+                InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32),
+            )
+        );
+    }
+
+    // success with correct params
+    {
+        let transaction = Transaction::new_signed_with_payer(
+            &[execute_with_extra_account_metas(
+                &program_id,
+                &source,
+                &mint_address,
+                &destination,
+                &wallet.pubkey(),
+                &extra_account_metas_address,
+                &updated_account_metas,
+                updated_amount,
+            )],
+            Some(&context.payer.pubkey()),
+            &[&context.payer, &mint_authority],
+            context.last_blockhash,
+        );
+        context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap();
+    }
+}