Solana Smart Contract Best Practices
This is a beta version of the Solana Toolkit, and is still a WIP. Please post all feedback as a GitHub issue here.
Optimize Compute Usage #
To prevent abuse of computational resources, each transaction is allocated a "compute budget". This budget specifies details about compute units and includes:
- the compute costs associated with different types of operations the transaction may perform (compute units consumed per operation),
- the maximum number of compute units that a transaction can consume (compute unit limit),
- and the operational bounds the transaction must adhere to (like account data size limits)
When the transaction consumes its entire compute budget (compute budget exhaustion), or exceeds a bound such as attempting to exceed the max call stack depth or max loaded account data size limit, the runtime halts the transaction processing and returns an error. Resulting in a failed transaction and no state changes (aside from the transaction fee being collected).
Additional References #
Saving Bumps #
Program Derived Address (PDAs) are addresses that PDAs are addresses that are deterministically derived and look like standard public keys, but have no associated private keys. These PDAs are derived using a numerical value, called a "bump", to guarantee that the PDA is off-curve and cannot have an associated private key. It "bumps" the address off the cryptographic curve.
Saving the bump to your Solana smart contract account state ensures deterministic address generation, efficiency in address reconstruction, reduced transaction failure, and consistency across invocations.
Additional References #
Payer-Authority Pattern #
The Payer-Authority pattern is an elegant way to handle situations where the account’s funder (payer) differs from the account’s owner or manager (authority). By requiring separate signers and validating them in your onchain logic, you can maintain clear, robust, and flexible ownership semantics in your program.
Shank Example #
// Create a new account.
#[account(0, writable, signer, name="account", desc = "The address of the new account")]
#[account(1, writable, signer, name="payer", desc = "The account paying for the storage fees")]
#[account(2, optional, signer, name="authority", desc = "The authority signing for the account creation")]
#[account(3, name="system_program", desc = "The system program")]
CreateAccountV1(CreateAccountV1Args),
Anchor Example #
#[derive(Accounts)]
pub struct CreateAccount<'info> {
/// The address of the new account
#[account(init, payer = payer_one, space = 8 + NewAccount::MAXIMUM_SIZE)]
pub account: Account<'info, NewAccount>,
/// The account paying for the storage fees
#[account(mut)]
pub payer: Signer<'info>,
/// The authority signing for the account creation
pub authority: Option<Signer<'info>>,
// The system program
pub system_program: Program<'info, System>
}
Additional References #
Invariants #
Implement invariants, which are functions that you can call at the end of your instruction to assert specific properties because they help ensure the correctness and reliability of programs.
require!(amount > 0, ErrorCode::InvalidAmount);
Additional References #
Optimize Indexing #
You can make indexing easier by placing static size fields at the beginning and variable size fields at the end of your onchain structures.