PDA and ATA assertions
These assertions check that an account the caller passed is the PDA or ATA the template expects, and this page shows how to keep that cheap. They prove how an address was derived; they don't let Ballista sign for it (see Signing).
Assert an associated token account
assertAta derives the ATA for [owner, tokenProgram, mint] with the canonical bump, and the run fails unless associatedTokenAccount has that address. The Run tabs compute the same address off chain.
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS_BYTES, account, assertAta, defineTemplate } from '@jac0xb/ballista';
/** Require `destinationAta` to be the associated token account of the recipient and mint. */
export const assertRecipientAta = defineTemplate({
accounts: {
associatedTokenProgram: { executable: true, address: ASSOCIATED_TOKEN_PROGRAM_ADDRESS_BYTES },
tokenProgram: {},
recipient: {},
mint: {},
destinationAta: { writable: true },
},
steps: [
assertAta({
associatedTokenAccount: account.fixed('destinationAta'),
owner: account.fixed('recipient'),
mint: account.fixed('mint'),
tokenProgram: account.fixed('tokenProgram'),
associatedTokenProgram: account.fixed('associatedTokenProgram'),
}),
],
});import { address, getAddressEncoder, getProgramDerivedAddress, type Address } from '@solana/kit';
import { compileTemplate } from '@jac0xb/ballista';
import { buildKitRunInstruction } from '@jac0xb/ballista/kit';
const ASSOCIATED_TOKEN_PROGRAM = address('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
/** Derive the ATA as the template does, and pass the accounts in the order it declares them. */
export async function runAssertRecipientAta(run: {
templateAddress: Address;
recipient: Address;
mint: Address;
tokenProgram: Address;
}) {
const encoder = getAddressEncoder();
const [destinationAta] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [encoder.encode(run.recipient), encoder.encode(run.tokenProgram), encoder.encode(run.mint)],
});
return buildKitRunInstruction({
compiled: compileTemplate(assertRecipientAta),
templateAddress: run.templateAddress,
accounts: {
associatedTokenProgram: { address: ASSOCIATED_TOKEN_PROGRAM },
tokenProgram: { address: run.tokenProgram },
recipient: { address: run.recipient },
mint: { address: run.mint },
destinationAta: { address: destinationAta },
},
});
}/// Require `destinationAta` to be the associated token account of the recipient and mint.
pub fn assert_recipient_ata() -> Template {
Template::new()
.account(
"associatedTokenProgram",
account::program(ASSOCIATED_TOKEN_PROGRAM_ID),
)
.account("tokenProgram", account::readonly())
.account("recipient", account::readonly())
.account("mint", account::readonly())
.account("destinationAta", account::writable())
.step(assert_ata(
"destinationAta",
"recipient",
"mint",
"tokenProgram",
"associatedTokenProgram",
))
}/// Derive the ATA as the template does, and name each account the template declares.
pub fn assert_recipient_ata(
template: Pubkey,
recipient: Pubkey,
mint: Pubkey,
token_program: Pubkey,
) -> RunResult {
let (destination_ata, _bump) = Pubkey::find_program_address(
&[recipient.as_ref(), token_program.as_ref(), mint.as_ref()],
&ASSOCIATED_TOKEN_PROGRAM_ID,
);
let instruction = templates::assert_recipient_ata()
.compile()?
.run(template)
.account("associatedTokenProgram", ASSOCIATED_TOKEN_PROGRAM_ID)
.account("tokenProgram", token_program)
.account("recipient", recipient)
.account("mint", mint)
.account("destinationAta", destination_ata)
.instruction()?;
Ok(instruction)
}Assert an arbitrary PDA
assertPda fails the run unless account is the canonical PDA of program, which must have a fixed address, for these seeds. Seeds are encoded by type, and a template can pass at most 15 of up to 32 bytes each.
import { account, assertPda, expression } from '@jac0xb/ballista';
assertPda({
account: account.fixed('position'),
program: account.fixed('protocolProgram'),
seeds: [
expression.bytes(new TextEncoder().encode('position')),
expression.accountField(account.fixed('owner'), 'key'),
expression.input('positionId'), // u64, encoded little-endian
],
});Supply the bump
Searching for the canonical bump costs 1,500 compute units per attempt. The caller can find it off chain for free and pass it to assertPda or assertAta:
import { account, assertPda, expression } from '@jac0xb/ballista';
assertPda({
account: account.fixed('position'),
program: account.fixed('protocolProgram'),
seeds: [
expression.bytes(new TextEncoder().encode('position')),
expression.accountField(account.fixed('owner'), 'key'),
expression.input('positionId'),
],
bump: expression.input('positionBump'), // computed off chain, 0 to 255
});- The trade-off. A supplied bump proves only that the address comes from those seeds with that bump. A caller could pass a lower bump that also works, giving a valid but non-canonical PDA.
- When that's fine. For ATAs: the Associated Token Program only creates canonical ones. Otherwise leave
bumpout, or hard-code the canonical bump when every seed is a constant. - The saving. With one constant seed, the search took
4,852compute units and a supplied bump1,898. The search costs more the further the bump is below255.