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 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:
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.
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):
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):
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:
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.
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.
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:
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:
For attribute has_one=authority, it generates the check:
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):
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:
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
How to audit Solana smart contracts series?
For all blogs by sec3, Please visit https://www.sec3.dev/blog