Skip to content

program: Add batch instruction#884

Merged
joncinque merged 10 commits intosolana-program:mainfrom
abelmarnk:batch
Mar 25, 2026
Merged

program: Add batch instruction#884
joncinque merged 10 commits intosolana-program:mainfrom
abelmarnk:batch

Conversation

@abelmarnk
Copy link
Copy Markdown
Contributor

@abelmarnk abelmarnk commented Dec 7, 2025

This PR adds the batch instruction to the token-2022 program, implementation drawn from here and originally proposed here.

The purpose is to allow the execution of multiple token instructions in a single program instruction – e.g., it is possible to execute multiple instructions in a single CPI call, which saves at least 1000 CUs (CPI base fee) for each additional instruction.

Scope

  • Update the TokenInstruction instruction variants(regular and POD) and associated tests
  • Add de/ser helper for the TokenInstruction variant
  • Update the processor code
  • Update the processor tests
  • Also adds program owner checks for all instructions

It closes #787.

@abelmarnk
Copy link
Copy Markdown
Contributor Author

@joncinque, I didn't add client code because the main purpose of the instruction seems to be for use with CPI, would it still be better to add it in as an instruction that takes raw bytes as arguments?

@abelmarnk abelmarnk marked this pull request as ready for review December 8, 2025 13:46
@abelmarnk
Copy link
Copy Markdown
Contributor Author

Hey @joncinque, following up on this.
Has there been a chance to review it yet? Thanks!

@joncinque
Copy link
Copy Markdown
Contributor

Sorry for the late response, between Breakpoint and the holidays, we had a few other priorities unfortunately.

@febo can you take a look?

Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slowness here, but looks great overall!

Just a few small points, and one huge point that someone (probably me) will need to go through all instructions to check for owner checks. Once that's done, we should be able to land this

Comment thread interface/src/serialization.rs Outdated
Comment thread interface/src/serialization.rs Outdated
}

/// Deserialize the data for the Batch variant of the `TokenInstruction`
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return type made me go cross-eyed for a bit, since I've never seen a deserialize function that returns bytes, but in the context of TokenInstruction::Batch, it makes sense

Comment thread interface/tests/serialization.rs
Comment thread program/src/processor.rs Outdated
Comment thread program/src/processor.rs
Comment on lines +1799 to +1803
pub fn process_batch(
program_id: &Pubkey,
mut accounts: &[AccountInfo],
mut data: &[u8],
) -> ProgramResult {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks correct, but as we have to do in p-token, we need to explicitly check ownership for any instructions that assume the runtime will do ownership checks at the end of the instruction: https://github.com/solana-program/token/blob/be42014b739dc8fbf7942f597d6ac736857896dd/pinocchio/program/src/processor/batch.rs#L49-L96

This will probably be a very painful process 🙃 We could add new cases for every successful instruction test, in which we hack the owner of all token accounts to another program, and see if the program returns an error, or if the runtime does.

If the runtime returns an error, we need to add an explicit ownership check.

Or we can just go one by one by hand and add check_program_account to all input accounts.

@febo any better ideas for how we can do this?

Copy link
Copy Markdown
Contributor Author

@abelmarnk abelmarnk Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about why those checks were there, the main instructions seem to fail immediately upon write attempt, the ones that reach the end of execution and then fail are Close and Withdraw Excess Lamports.

I assumed that for cases where we are writing into account and the account did not belong to the program then it should at least be in the same state that it was at the start of execution of the batch instruction, though the drawback would be that an instruction would allow for accounts that don't belong to it's program to pass of as if they did in a transaction explorer(though Reallocate and Withdraw Excess Lamports currently seems to allow for this).

The changes i made were to add checks for each non-extension instruction(i.e instructions that don't have full fledged sub processors) that the runtime returns an error for and leave the non-extension instructions that already had checks, since the extensions are not used as frequently and most already had program owner checks i added checks for all the rest(i.e the group extension, metadata extension...), i can make re-adjustments after further review if need be.

The batch checks were added here and the tests here, the other changes are in the different processor files.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can just go one by one by hand and add check_program_account to all input accounts.

This seems a good approach.

Comment thread program/src/processor.rs Outdated
@joncinque
Copy link
Copy Markdown
Contributor

Also, can you rebase your branch when you get the chance?

Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing all the comments, this is really great work! I went over the whole program too, and confirm that all of the extension-related processor changes are good.

For the additional ownership checks, they're required with the batch instruction because it's possible to trick the token program otherwise.

For example, I can do initialize_account + + close_account, and the token program will happily write data to the new account, do things with it, and close it at the end of the batch instruction. With wrapped SOL accounts, this can become dangerous.

So with that in mind, let's add more explicit checks in processor.rs:

  • _process_initialize_mint (mint_info)
  • _process_initialize_account (new_account_info / mint_info)
  • _process_initialize_multisig (multisig_info)
  • process_transfer (mint_info)
  • process_approve (source_account_info / mint_info)
  • process_revoke (source_account_info)
  • process_set_authority (account_info)
  • process_close_account (source_account_info)
  • process_toggle_freeze_account (source_account_info, mint_info)
  • process_initialize_mint_close_authority (mint_account_info)
  • process_get_account_data_size (mint_account_info)
  • process_initialize_immutable_owner (token_account_info)
  • process_create_native_mint (native_mint_info)
  • process_initialize_non_transferable_mint (mint_account_info)
  • process_initialize_permanent_delegate (mint_account_info)
  • process_withdraw_excess_lamports (source_info)
  • process_unwrap_lamports (always check source_account_info)

Comment thread program/src/extension/token_metadata/processor.rs Outdated
Comment thread program/src/processor.rs Outdated
Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last thing, then we can land this! Thanks for the quick turnaround on all these

Comment thread program/src/processor.rs Outdated
Comment on lines 1809 to 1820
source_account.base.amount = remaining_amount.into();
if source_account_info.key != destination_account_info.key {
let source_starting_lamports = source_account_info.lamports();
**source_account_info.lamports.borrow_mut() = source_starting_lamports
.checked_sub(amount)
.ok_or(TokenError::Overflow)?;

let destination_starting_lamports = destination_account_info.lamports();
**destination_account_info.lamports.borrow_mut() = destination_starting_lamports
.checked_add(amount)
.ok_or(TokenError::Overflow)?;
}
Ok(())
let destination_starting_lamports = destination_account_info.lamports();
**destination_account_info.lamports.borrow_mut() = destination_starting_lamports
.checked_add(amount)
.ok_or(TokenError::Overflow)?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can now be put in a if amount !=0 { block

@joncinque joncinque changed the title Add batch instruction program: Add batch instruction Mar 24, 2026
Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to go on my side! The audit failure will be addressed in #1060

@febo can you just give a quick look at the instruction implementation?

Copy link
Copy Markdown
Contributor

@febo febo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good – just have a question about the ordering of the owner checks.

Comment thread program/src/processor.rs
Comment on lines 1769 to 1781
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we ok with returning a different error? For example, a previous instruction that would fail to unpack the account data if an invalid account is passed, will now fail with an invalid owner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing errors shouldn't make a difference, since it's impossible to recover from errors in CPI

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this will change errors for non-cpi calls as well, since the new checks are on the individual instructions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but what impact can that have?

error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
TransactionError::InstructionError(0, InstructionError::IncorrectProgramId)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of the change that I mentioned about the ownership checks. Probably it is ok to return a different error. Alternatively, we could move the owner checks to be the last statement of the processor. We would still get a different error if the account is not owned by the program, but other validation checks will stay the same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, but keeping the source code easier to read is more important in my opinion

@joncinque joncinque merged commit 6ce35b6 into solana-program:main Mar 25, 2026
39 of 40 checks passed
@joncinque
Copy link
Copy Markdown
Contributor

Thanks for your contribution! This will be a huge help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature proposal: batch instruction

3 participants