How to Audit Solana Smart Contracts Part 4: The Anchor Framework
Following Part 3: penetration testing, this article introduces the internals of Anchor, a popular framework for writing and testing Solana smart contracts.
Like Truffle for Ethereum, Anchor provides a range of functionalities for developing a complete application on Solana, in particular:
-
Rust crates (macros) for writing Solana programs
-
IDL (interface description language) specification
-
CLI and workspace management
We will elaborate on the internals of Anchor and some caveats from an auditor’s perspective.
Anchor programming model and codegen
Anchor exposes a declarative programming model where a user can annotate methods or data types (structs and their fields) using macros, which then automatically generate code wrappers to execute on Solana.
The generated code wrappers can do a variety of things, such as decoding the input data, creating and initializing accounts, and more importantly, ensuring additional constraints over the input data, e.g., enforcing an input account is signer and relationships among multiple input accounts.
There are three commonly used Anchor macros:
#\[program\]— global instructions declared inside of #[program]#\[derive(Accounts)\]— structs deserialized as input accounts vector#\[account(…)\]— constraints associated with each struct field, i.e., each input account
Next, we will use an example (basic-2 provided in Anchor) to illustrate each of the three macros above and their generated code in detail.
There are also a number of other Anchor macros, such as `#\[state\]` (state methods), `#\[interface\]` (interface methods), etc. A list of them can be found in the Anchor CHANGELOG.
#[program]
mod basic_2 {
use super::*;
pub fn create(ctx: Context<Create>, authority: Pubkey) -> ProgramResult {
let counter = &mut ctx.accounts.counter;
counter.authority = authority;
counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> ProgramResult {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
Figure 1. #[program] macro in basic-2
Figure 1. #[program] macro in basic-2
1. #[program] — global instructions
In a standard Solana program w/o Anchor, there will be an entry point (defined by entrypoint! macro), and there are three parameters passed to the entry point: program_id, accounts, and instruction_data. To invoke a corresponding instruction, the program must parse the instruction_data.
However, w/ Anchor, there is no need to specify an entry point or parse the instruction_data. All is handled by the `#\[program\]` macro.
Figure 1 shows the two functions (create on line 9 and increment line 16): these are contract instructions that will be invoked by transactions. With the `#\[program\]` macro (line 5), Anchor will generate the following code to call these instructions (use cargo expand to show result of macro expansion):
Figure 2. entrypoint generated by Anchor
use basic_2::*;
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
let (program_id, accounts, instruction_data) =
unsafe { ::solana_program::entrypoint::deserialize(input) };
match entry(&program_id, &accounts, &instruction_data) {
Ok(()) => ::solana_program::entrypoint::SUCCESS,
Err(error) => error.into(),
}
}
Figure 2. entrypoint generated by Anchor
The entrypoint function will use solana_program::entrypoint::deserializ to decode the input into a tuple (program_id, accounts, instruction_data), and call another function “entry”, which then calls function “dispatch” taking the tuple as parameters (similar to process_instruction):
Figure 3. entry function generated by Anchor
#[cfg(not(feature = "no-entrypoint"))]
pub fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
if data.len() < 8 {
return Err(anchor_lang::__private::ErrorCode::InstructionMissing.into());
}
dispatch(program_id, accounts, data).map_err(|e| {
::solana_program::log::sol_log(&e.to_string());
e
})
}
Figure 3. entry function generated by Anchor
Figure 4. dispatch function generated by Anchor
fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let mut ix_data: &[u8] = data;
let sighash: [u8; 8] = {
let mut sighash: [u8; 8] = [0; 8];
sighash.copy_from_slice(&ix_data[..8]);
ix_data = &ix_data[8..];
sighash
};
if true {
if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
return __private::__idl::__idl_dispatch(program_id, accounts, &ix_data);
}
}
match sighash {
[24, 30, 200, 40, 5, 28, 7, 119] => {
__private::__global::create(program_id, accounts, ix_data)
}
[11, 18, 104, 9, 104, 174, 59, 33] => {
__private::__global::increment(program_id, accounts, ix_data)
}
_ => Err(anchor_lang::__private::ErrorCode::InstructionFallbackNotFound.into()),
}
}
Figure 4. dispatch function generated by Anchor
In Figure 4, the dispatch function uses the first 8 bytes of the instruction data (called “sighash”) to identify the called instruction. If sighash matches a user defined instruction, then the corresponding method handler wrapper will be invoked. E.g., \[24, 30, 200, 40, 5, 28, 7, 119\] corresponds to “__global::create” and \[11, 18, 104, 9, 104, 174, 59, 33\] corresponds to “__global::increment”.
The method handler wrappers in our example are defined below:
Figure 5. method handler wrappers generated by Anchor
/// __global mod defines wrapped handlers for global instructions.
pub mod __global {
use super::*;
#[inline(never)]
pub fn create(
program_id: &Pubkey,
accounts: &[AccountInfo],
ix_data: &[u8],
) -> ProgramResult {
let ix = instruction::Create::deserialize(&mut &ix_data[..])
.map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
let instruction::Create { authority } = ix;
let mut remaining_accounts: &[AccountInfo] = accounts;
let mut accounts = Create::try_accounts(program_id, &mut remaining_accounts, ix_data)?;
basic_2::create(
Context::new(program_id, &mut accounts, remaining_accounts),
authority,
)?;
accounts.exit(program_id)
}
#[inline(never)]
pub fn increment(
program_id: &Pubkey,
accounts: &[AccountInfo],
ix_data: &[u8],
) -> ProgramResult {
let ix = instruction::Increment::deserialize(&mut &ix_data[..])
.map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
let instruction::Increment = ix;
let mut remaining_accounts: &[AccountInfo] = accounts;
let mut accounts =
Increment::try_accounts(program_id, &mut remaining_accounts, ix_data)?;
basic_2::increment(Context::new(program_id, &mut accounts, remaining_accounts))?;
accounts.exit(program_id)
}
}
Figure 5. method handler wrappers generated by Anchor
The wrappers (__global::create and __global::increment) wrap the corresponding instructions (basic_2:create and basic_2:increment), deserializing the accounts, constructing the context, invoking the user’s code, and finally running the exit routine, which typically persists account changes.
#[derive(Accounts)]
pub struct Create<'info> {
#[account(init, payer = user, space = 8 + 40)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
Figure 6. #[derive(Accounts)] and #[account(...)] macros in basic-2
Figure 6. #[derive(Accounts)] and #[account(…)] macros in basic-2
2. #[derive(Accounts)] — accounts deserialization
For every struct T marked with #\[derive(Accounts)\] , Anchor will generate a corresponding function T::try_accounts that deserializes the input accounts and adds validation checks.
For example, in Figure 6, #\[derive(Accounts)\] is declared on top of the two structs Create and Increment. This tells Anchor to generate two try_accounts functions (Create::try_accounts and Increment::try_accounts), which will deserialize the input accounts into ctx.accounts, as shown in Figure 5 .
Note that the first parameters of the contract instructions are all `ctx*, of a parametric type *Context<T>`. In Figure 1 (the basic_2 example ), `Context<Create>` and `Context<Increment>` respectively .
Figure 7 shows the definition of `Context**, a struct that encapsulates three fields: **program_id, accounts, and remaining_accounts_._ **Note_:_** while accounts` are deserialized and validated according to the macros, remaining_accounts are not, so its use must be very careful.
/// Provides non-argument inputs to the program.
pub struct Context<'a, 'b, 'c, 'info, T> {
/// Currently executing program id.
pub program_id: &'a Pubkey,
/// Deserialized accounts.
pub accounts: &'b mut T,
/// Remaining accounts given but not deserialized or validated.
/// Be very careful when using this directly.
pub remaining_accounts: &'c [AccountInfo<'info>],
}
Figure 7. the “Context” struct defined in Anchor
Figure 7. the “Context” struct defined in Anchor
3. #[account(…)] — the deserialization logics and constraints
The deserialization logics for the struct’s fields are specified by #\[account(…)\], where … denotes a list of attributes, such as mut, init, owner=…, has_one=…, payer=… etc.
Each attribute denotes a certain constraint for the corresponding account, and checks for the constraints are automatically added in try_accounts. For example:
- mut adds a check for is_writable
- init creates an account and initializes it
payer=usersets the user account to be payer for the init account
Figure 8. the Increment::try_accounts function generated by Anchor
pub struct Increment<'info> {
# [account (mut , has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[automatically_derived]
impl<'info> anchor_lang::Accounts<'info> for Increment<'info>
where
'info: 'info,
{
#[inline(never)]
fn try_accounts(
program_id: &anchor_lang::solana_program::pubkey::Pubkey,
accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>],
ix_data: &[u8],
) -> std::result::Result<Self, anchor_lang::solana_program::program_error::ProgramError> {
let counter: anchor_lang::Account<Counter> =
anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?;
let authority: Signer = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?;
if !counter.to_account_info().is_writable {
return Err(anchor_lang::__private::ErrorCode::ConstraintMut.into());
}
if &counter.authority != authority.to_account_info().key {
return Err(anchor_lang::__private::ErrorCode::ConstraintHasOne.into());
}
Ok(Increment { counter, authority })
}
}
Figure 8. the Increment::try_accounts function generated by Anchor
Figure 8 shows the Increment.try_accounts function. Consider the #``\[account(mut, has_one = authority)\] macro declared on the counter account:
For attribute mut, it generates the check:
if !counter.to_account_info().is_writable {
return Err(anchor_lang::__private::ErrorCode::ConstraintMut.into());
}
For attribute has_one=authority, it generates the check:
if &counter.authority != authority.to_account_info().key {
return Err(anchor_lang::__private::ErrorCode::ConstraintHasOne.into());
}
has_one=authority: enforces the constraint thatIncrement.counter.authority == Increment.authority.key.
There are also built-in Account types such as Signer (check is_signer for the account) and Program (the system_program).
let counter = &accounts[0];
*accounts = &accounts[1..];
let user: Signer = anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?;
let system_program: anchor_lang::Program<System> =
anchor_lang::Accounts::try_accounts(program_id, accounts, ix_data)?;
let __anchor_rent = Rent::get()?;
In addition, Anchor also generates rent exemption checks for all accounts marked with #\[account(init)\] by default (unless rent_exempt = skip):
if !__anchor_rent.is_exempt(
counter.to_account_info().lamports(),
counter.to_account_info().try_data_len()?,
) {
return Err(anchor_lang::__private::ErrorCode::ConstraintRentExempt.into());
}
Anchor caveats
Overall, through a declarative programming model, Anchor makes it much easier to write Solana smart contracts compared to native Rust programs. However, there are a few caveats to note:
-
Anchor is still under active development, so features and semantics of certain macros may subject to change.
-
Anchor has not been audited, any bugs in Anchor codegen may lead to subtle vulnerabilities unnoticed.
-
The declarative constraints in
#\[account(...)\]must be taken special care of to ensure sufficient validation and correct access control of every contract instruction. -
Be very careful when using ctx.remaining_accounts
directly. The remaining accounts in the Context struct are not deserialized or validated.
sec3 audit
sec3 is founded by leading minds in the fields of blockchain security and software verification.
We are pleased to provide audit services to high-impact Dapps on Solana. Please visit sec3.dev or email contact@sec3.dev
Previous articles
How to audit Solana smart contracts series?
For all blogs by sec3, Please visit https://www.sec3.dev/blog