How to Audit Solana Smart Contracts Part 4: The Anchor Framework

December 16, 2021

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:

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.

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

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
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

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.

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.

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=user sets the user account to be payer for the init account
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:

#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,    
pub authority: Signer<'info>,

For attribute mut, it generates the check:

For attribute has_one=authority, it generates the check:

  • has_one=authority: enforces the constraint that Increment.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).

In addition, Anchor also generates rent exemption checks for all accounts marked with #[account(init)] by default (unless rent_exempt = skip):

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:

  1. Anchor is still under active development, so features and semantics of certain macros may subject to change.
  2. Anchor has not been audited, any bugs in Anchor codegen may lead to subtle vulnerabilities unnoticed.
  3. The declarative constraints in #[account(...)] must be taken special care of to ensure sufficient validation and correct access control of every contract instruction.
  4. Be very careful when using ctx.remaining_accounts directly. The remaining accounts in the Context struct are not deserialized or validated.

Soteria audit

Soteria 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 soteria.dev or email contact@soteria.dev

Previous articles

How to audit Solana smart contracts series?

For all blogs by Soteria, Please visit https://www.soteria.dev/blogs