From 58af8c8952c6ed4e8916a7ed0b3da820ae9b90e7 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 11 Mar 2026 13:43:29 +0000 Subject: [PATCH] feat: add fee reimbursement for v2 BatchAppend and BatchUpdateAddressTree Add fee_payer account to BatchAppend and BatchUpdateAddressTree instructions to reimburse foresters for network fees. BatchAppend transfers 2x network_fee from output_queue, BatchUpdateAddressTree transfers 1x network_fee from merkle_tree. Transfers only occur when network_fee >= 5000 lamports. Registry CPI wrappers pass fee_payer through; SDK builders set fee_payer to forester. Also adds create-address-test-program to the programs build. --- .../tests/batched_merkle_tree_test.rs | 7 +++++- .../src/instructions/batch_append.rs | 25 ++++++++++++++++--- .../instructions/batch_update_address_tree.rs | 25 ++++++++++++++++--- programs/justfile | 1 + .../account_compression_cpi/batch_append.rs | 4 +++ .../batch_update_address_tree.rs | 4 +++ .../src/account_compression_cpi/sdk.rs | 2 ++ 7 files changed, 61 insertions(+), 7 deletions(-) diff --git a/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs b/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs index 23207121d6..ba9446c594 100644 --- a/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs +++ b/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs @@ -212,7 +212,10 @@ async fn test_batch_state_merkle_tree() { let payer_pubkey = context.get_payer().pubkey(); let payer = context.get_payer().insecure_clone(); - let params = InitStateTreeAccountsInstructionData::test_default(); + let mut params = InitStateTreeAccountsInstructionData::test_default(); + // Use network_fee below reimbursement threshold since this test + // bypasses the system program (no fees accumulate in the queue). + params.network_fee = Some(1); let queue_account_size = get_output_queue_account_size( params.output_queue_batch_size, params.output_queue_zkp_batch_size, @@ -752,6 +755,7 @@ pub async fn perform_batch_append( log_wrapper: NOOP_PROGRAM_ID, merkle_tree: merkle_tree_pubkey, output_queue: output_queue_pubkey, + fee_payer: payer.pubkey(), }; let instruction = Instruction { @@ -2184,6 +2188,7 @@ pub async fn update_batch_address_tree( registered_program_pda: None, log_wrapper: NOOP_PROGRAM_ID, merkle_tree, + fee_payer: context.get_payer().pubkey(), }; let instructions = if mode == UpdateBatchAddressTreeTestMode::UpdateTwice { vec![ diff --git a/programs/account-compression/src/instructions/batch_append.rs b/programs/account-compression/src/instructions/batch_append.rs index 1338791ba8..841d5f0994 100644 --- a/programs/account-compression/src/instructions/batch_append.rs +++ b/programs/account-compression/src/instructions/batch_append.rs @@ -5,8 +5,11 @@ use light_batched_merkle_tree::merkle_tree::{ use crate::{ emit_indexer_event, - utils::check_signer_is_registered_or_authority::{ - check_signer_is_registered_or_authority, GroupAccounts, + utils::{ + check_signer_is_registered_or_authority::{ + check_signer_is_registered_or_authority, GroupAccounts, + }, + transfer_lamports::transfer_lamports, }, RegisteredProgram, }; @@ -24,6 +27,9 @@ pub struct BatchAppend<'info> { /// CHECK: in update_tree_from_output_queue_account_info. #[account(mut)] pub output_queue: AccountInfo<'info>, + /// CHECK: receives network fee reimbursement. + #[account(mut)] + pub fee_payer: UncheckedAccount<'info>, } impl<'info> GroupAccounts<'info> for BatchAppend<'info> { @@ -62,6 +68,19 @@ pub fn process_batch_append_leaves<'a, 'b, 'c: 'info, 'info>( let event = merkle_tree .update_tree_from_output_queue_account_info(&ctx.accounts.output_queue, instruction_data) .map_err(ProgramError::from)?; - // 4. Emit indexer event. + // 4. Transfer network fee reimbursement to fee payer. + let network_fee = merkle_tree + .get_metadata() + .metadata + .rollover_metadata + .network_fee; + if network_fee >= 5_000 { + transfer_lamports( + &ctx.accounts.output_queue.to_account_info(), + &ctx.accounts.fee_payer.to_account_info(), + network_fee * 2, + )?; + } + // 5. Emit indexer event. emit_indexer_event(event.try_to_vec()?, &ctx.accounts.log_wrapper) } diff --git a/programs/account-compression/src/instructions/batch_update_address_tree.rs b/programs/account-compression/src/instructions/batch_update_address_tree.rs index f4f3d610a4..8f6e8e0104 100644 --- a/programs/account-compression/src/instructions/batch_update_address_tree.rs +++ b/programs/account-compression/src/instructions/batch_update_address_tree.rs @@ -5,8 +5,11 @@ use light_batched_merkle_tree::merkle_tree::{ use crate::{ emit_indexer_event, - utils::check_signer_is_registered_or_authority::{ - check_signer_is_registered_or_authority, GroupAccounts, + utils::{ + check_signer_is_registered_or_authority::{ + check_signer_is_registered_or_authority, GroupAccounts, + }, + transfer_lamports::transfer_lamports, }, RegisteredProgram, }; @@ -21,6 +24,9 @@ pub struct BatchUpdateAddressTree<'info> { /// CHECK: in from_account_info. #[account(mut)] pub merkle_tree: AccountInfo<'info>, + /// CHECK: receives network fee reimbursement. + #[account(mut)] + pub fee_payer: UncheckedAccount<'info>, } impl<'info> GroupAccounts<'info> for BatchUpdateAddressTree<'info> { @@ -55,6 +61,19 @@ pub fn process_batch_update_address_tree<'a, 'b, 'c: 'info, 'info>( let event = merkle_tree .update_tree_from_address_queue(instruction_data) .map_err(ProgramError::from)?; - // 4. Emit indexer event. + // 4. Transfer network fee reimbursement to fee payer. + let network_fee = merkle_tree + .get_metadata() + .metadata + .rollover_metadata + .network_fee; + if network_fee >= 5_000 { + transfer_lamports( + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.fee_payer.to_account_info(), + network_fee, + )?; + } + // 5. Emit indexer event. emit_indexer_event(event.try_to_vec()?, &ctx.accounts.log_wrapper) } diff --git a/programs/justfile b/programs/justfile index fb91d072bd..6ad1534cbd 100644 --- a/programs/justfile +++ b/programs/justfile @@ -8,6 +8,7 @@ build: cd account-compression && cargo build-sbf --features 'test, migrate-state' cd registry && cargo build-sbf cd compressed-token/program && cargo build-sbf + cargo build-sbf --manifest-path ../program-tests/create-address-test-program/Cargo.toml build-compressed-token-small: cd compressed-token/program && cargo build-sbf --features cpi-without-program-ids diff --git a/programs/registry/src/account_compression_cpi/batch_append.rs b/programs/registry/src/account_compression_cpi/batch_append.rs index 36ba9f8ac7..112400def4 100644 --- a/programs/registry/src/account_compression_cpi/batch_append.rs +++ b/programs/registry/src/account_compression_cpi/batch_append.rs @@ -23,6 +23,9 @@ pub struct BatchAppend<'info> { /// CHECK: (account compression program). #[account(mut)] pub output_queue: AccountInfo<'info>, + /// CHECK: receives network fee reimbursement. + #[account(mut)] + pub fee_payer: UncheckedAccount<'info>, } pub fn process_batch_append(ctx: &Context, bump: u8, data: Vec) -> Result<()> { @@ -35,6 +38,7 @@ pub fn process_batch_append(ctx: &Context, bump: u8, data: Vec) registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), log_wrapper: ctx.accounts.log_wrapper.to_account_info(), output_queue: ctx.accounts.output_queue.to_account_info(), + fee_payer: ctx.accounts.fee_payer.to_account_info(), }; let cpi_ctx = CpiContext::new_with_signer( diff --git a/programs/registry/src/account_compression_cpi/batch_update_address_tree.rs b/programs/registry/src/account_compression_cpi/batch_update_address_tree.rs index aa916e5032..a5f3a42092 100644 --- a/programs/registry/src/account_compression_cpi/batch_update_address_tree.rs +++ b/programs/registry/src/account_compression_cpi/batch_update_address_tree.rs @@ -20,6 +20,9 @@ pub struct BatchUpdateAddressTree<'info> { /// CHECK: (account compression program). #[account(mut)] pub merkle_tree: AccountInfo<'info>, + /// CHECK: receives network fee reimbursement. + #[account(mut)] + pub fee_payer: UncheckedAccount<'info>, } pub fn process_batch_update_address_tree( @@ -35,6 +38,7 @@ pub fn process_batch_update_address_tree( merkle_tree: ctx.accounts.merkle_tree.to_account_info(), registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), log_wrapper: ctx.accounts.log_wrapper.to_account_info(), + fee_payer: ctx.accounts.fee_payer.to_account_info(), }; let cpi_ctx = CpiContext::new_with_signer( diff --git a/programs/registry/src/account_compression_cpi/sdk.rs b/programs/registry/src/account_compression_cpi/sdk.rs index ce3d1d7717..f002c35499 100644 --- a/programs/registry/src/account_compression_cpi/sdk.rs +++ b/programs/registry/src/account_compression_cpi/sdk.rs @@ -379,6 +379,7 @@ pub fn create_batch_append_instruction( registered_program_pda, account_compression_program: account_compression::ID, log_wrapper: NOOP_PUBKEY.into(), + fee_payer: forester, }; let instruction_data = crate::instruction::BatchAppend { bump, data }; Instruction { @@ -506,6 +507,7 @@ pub fn create_batch_update_address_tree_instruction( registered_program_pda, account_compression_program: account_compression::ID, log_wrapper: NOOP_PUBKEY.into(), + fee_payer: forester, }; let instruction_data = crate::instruction::BatchUpdateAddressTree { bump, data }; Instruction {