Remaining Accounts
Accessing dynamically-sized remaining accounts in instructions.
Some instructions need a variable number of accounts -- a list of signers for a multisig, a set of token accounts for a batch transfer, or extra accounts forwarded to a CPI. Remaining accounts are accounts passed after the declared (typed) accounts in the instruction.
Ctx vs CtxWithRemaining
Ctx<T>-- parsed and validated accounts only. For instructions with a fixed set of accounts.CtxWithRemaining<T>-- also captures remaining accounts. For instructions that need additional accounts.
Choose at the instruction signature:
// Fixed accounts only
#[instruction(discriminator = 0)]
pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {
ctx.accounts.handler(&ctx.bumps)
}
// With remaining accounts
#[instruction(discriminator = 1)]
pub fn create_multisig(
ctx: CtxWithRemaining<Create>,
threshold: u8,
) -> Result<(), ProgramError> {
ctx.accounts.create_multisig(threshold, &ctx.bumps, ctx.remaining_accounts())
}The #[program] macro detects which context type you use and generates the appropriate dispatch code.
The RemainingAccounts struct
RemainingAccounts is a zero-allocation accessor over the SVM input buffer. It provides bounds-checked access and automatic duplicate resolution against the declared accounts.
Checking for remaining accounts
let remaining = ctx.remaining_accounts();
if remaining.is_empty() {
return Err(ProgramError::NotEnoughAccountKeys);
}Random access
get(index) walks from the buffer start to the requested index (O(n) per call):
let first = remaining.get(0); // Option<AccountView>
let third = remaining.get(2); // walks past 0, 1, returns 2Iteration
iter() returns a RemainingIter that yields accounts lazily:
for account in remaining.iter() {
let account = account?; // Result<AccountView, ProgramError>
// use account...
}The iterator caches accounts as it walks, enabling O(1) duplicate resolution. Prefer it over get().
Real example: multisig creation
#[derive(Accounts)]
pub struct Create<'info> {
pub creator: &'info mut Signer,
#[account(init, mut, payer = creator, seeds = [b"multisig", creator], bump)]
pub config: Account<MultisigConfig<'info>>,
pub rent: &'info Sysvar<Rent>,
pub system_program: &'info Program<System>,
}
impl<'info> Create<'info> {
pub fn create_multisig(
&mut self,
threshold: u8,
bumps: &CreateBumps,
remaining: RemainingAccounts,
) -> Result<(), ProgramError> {
let mut addrs = core::mem::MaybeUninit::<[Address; 10]>::uninit();
let addrs_ptr = addrs.as_mut_ptr() as *mut Address;
let mut count = 0usize;
for account in remaining.iter() {
let account = account?;
if count >= 10 {
return Err(ProgramError::InvalidArgument);
}
if !account.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
unsafe { core::ptr::write(addrs_ptr.add(count), *account.address()) };
count = count.wrapping_add(1);
}
if threshold == 0 || threshold as usize > count {
return Err(ProgramError::InvalidArgument);
}
let signers = unsafe { core::slice::from_raw_parts(addrs_ptr, count) };
self.config.set_inner(
*self.creator.address(),
threshold,
bumps.config,
"",
signers,
self.creator.to_account_view(),
Some(&**self.rent),
)
}
}Remaining accounts are unchecked by default -- the handler explicitly verifies is_signer() and bounds the count.
Real example: multisig execution
#[derive(Accounts)]
pub struct ExecuteTransfer<'info> {
#[account(
has_one = creator,
seeds = [b"multisig", creator],
bump = config.bump
)]
pub config: Account<MultisigConfig<'info>>,
pub creator: &'info UncheckedAccount,
#[account(mut, seeds = [b"vault", config], bump)]
pub vault: &'info mut UncheckedAccount,
pub recipient: &'info mut UncheckedAccount,
pub system_program: &'info Program<System>,
}
impl<'info> ExecuteTransfer<'info> {
pub fn verify_and_transfer(
&self,
amount: u64,
bumps: &ExecuteTransferBumps,
remaining: RemainingAccounts,
) -> Result<(), ProgramError> {
let stored_signers = self.config.signers();
let threshold = self.config.threshold;
let mut approvals = 0u32;
for account in remaining.iter() {
let account = account?;
if !account.is_signer() {
continue;
}
let addr = account.address();
for stored in stored_signers {
if addr == stored {
approvals = approvals.wrapping_add(1);
break;
}
}
}
if approvals < threshold as u32 {
return Err(ProgramError::MissingRequiredSignature);
}
let seeds = bumps.vault_seeds();
self.system_program
.transfer(self.vault, self.recipient, amount)
.invoke_signed(&seeds)
}
}Non-signer accounts are silently skipped (with continue), allowing clients to pass additional accounts without breaking the instruction.
Duplicate account handling
The SVM uses a compact encoding for duplicate accounts -- a duplicate entry references the original by index instead of repeating the full data. The iterator handles this transparently: duplicates resolve to the same AccountView as the original, whether it was a declared account or a previously yielded remaining account.
The iterator cache is bounded at 64 entries. Exceeding this returns Err(QuasarError::RemainingAccountsOverflow).
Performance characteristics
| Operation | Cost |
|---|---|
ctx.remaining_accounts() | O(1) -- returns pre-computed pointers |
remaining.is_empty() | O(1) -- pointer comparison |
remaining.get(n) | O(n) -- walks buffer from start |
remaining.iter().next() | O(1) amortized -- advances one entry |
| Duplicate resolution (iterator) | O(1) -- cache lookup |
| Duplicate resolution (get) | O(n) -- re-walks buffer with 2-hop limit |
Prefer iter() over repeated get() calls when processing all remaining accounts.
Guidelines
-
Use
CtxWithRemaining<T>only when needed. If your instruction does not access remaining accounts, useCtx<T>. -
Prefer
iter()overget(). The iterator caches for O(1) duplicate resolution. Repeatedget()calls re-walk the buffer each time. -
Validate remaining accounts explicitly. They are unchecked by design. Check
is_signer(),is_writable(), owner, and any other constraints in your handler. -
Stay under 64 remaining accounts. The iterator cache is bounded at 64 entries.
-
Use stack-allocated collection buffers. The multisig example demonstrates the pattern: a
MaybeUninitarray on the stack, filled by the iterator, with a bounded count.
