After writing several Anchor programs, these are the things that saved me the most time.
1. Use constraint error codes with messages
Instead of:
#[account(mut, constraint = user.authority == signer.key())]
Write:
#[account(
mut,
constraint = user.authority == signer.key() @ MyError::Unauthorized
)]
Custom error codes make debugging on-chain failures orders of magnitude faster.
2. init_if_needed is dangerous
init_if_needed can be exploited if you’re not careful — an attacker can re-initialize an account that already exists. Only use it if you explicitly handle the already-initialized case in your logic, or use init and handle the case client-side.
3. Prefer seeds + bump in constraints
#[account(
seeds = [b"vault", user.key().as_ref()],
bump = vault.bump,
)]
pub vault: Account<'info, Vault>,
Storing the bump in the account at init time and reusing it avoids the find_program_address overhead on every instruction.
4. Keep instruction logic thin
Your #[program] handler should just validate and then call a function in a separate module. Testing program logic in isolation (without the Anchor context) is much easier this way.
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
instructions::deposit::handle(ctx, amount)
}
5. Emit events for everything important
emit!(DepositEvent {
user: ctx.accounts.user.key(),
amount,
timestamp: Clock::get()?.unix_timestamp,
});
Events are indexed by most RPC providers and are cheap. They’re the primary way clients track what happened in a transaction.
Comments