Introduction

As Bonfida devs, we have been writing Solana programs for some time now. As a result, we have come to cultivate a particular code style. This systematic approach to writing Solana programs has enabled us to write programs faster, with no sacrifice to their security. If anything, being systematic about our programs has made them safer and easier to audit.

Our approach is based on several key principles:

  • We don't use frameworks, and this is not quite a framework: critical program logic should never be hidden away behind macros.
  • Security checks should always be obvious.
  • Redundant security and safety checks are better than implicit ones.

This book is intended as a guide to what we have come to define internally as the Bonfida style. As we learn new and better ways to write programs, it will necessarily evolve.

Feedback

If you notice a mistake in this book, or if you would like something to get clarified, please do not hesitate to open an issue on the guide's repo.

Getting Started

Installing the Bonfida tool suite

Installing the Bonfida tool suite is quite straightforward.

cargo install --git https://github.com/Bonfida/bonfida-utils.git cli

This command installs the bonfida CLI tool, which incorporates a few useful development tools. You can run bonfida help to get a list of available commands.

In order to update the tool, just re-run the above command.

Creating a new project

To initialize a new project my-project in the current directory, we use the following command:

bonfida autoproject my-project
cd my-project

Overview of project structure

Each new project is a sort of monorepo containing three folders

├── js
├── program
└── python
  • The js folder is used to build JavaScript bindings for the current project.
  • The program folder contains the on-chain Solana program.
  • The python folder contains Python bindings.

The program

The summary below describes the basic structure of the program's files, as well as their individual purpose.

program                               
├── Cargo.toml                       
├── src                              
│   ├── cpi.rs                       
│   ├── entrypoint.rs                # Boilerplate for the Solana program's entrypoint
|   |
│   ├── error.rs                     # Custom errors for the program. Varied and 
|   |                                  descriptive errors should be preferred!
|   |
│   ├── instruction.rs               # The instruction enum which serves as a registry 
|   |                                  of supported instructions.
│   │                                  It also contains the Rust bindings for every 
|   |                                  instruction.
|   |
│   ├── lib.rs                       # Structural root of the crate. Contains a 
|   |                                  `declare_id` statement which defines the 
|   |                                  program's reference on-chain key.
|   |
│   ├── processor                    # Folder holding the available instructions' logic
|   |   |
│   │   └── example_instr.rs         # Example instruction. Each instruction file 
|   |                                  follows a strict template to optimize ease of 
|   |                                  audit and general readability.
|   |
│   ├── processor.rs                 # The processor itself is a dispatcher for all 
|   |                                  instructions. The program entrypoint directly 
|   |                                  calls the processor.
|   |
│   ├── state                        # Contains one file per type of program state 
|   |   |                              account. This can range from anything to user 
|   |   |                              accounts or central system state
|   |   |
│   │   ├── example_state_borsh.rs   # An example account type which uses Borsh for 
|   |   |                              serialization and deserialization
|   |   |
│   │   └── example_state_cast.rs    # An example account type which uses direct 
|   |                                  casting. Casting is more efficient in terms of 
|   |                                  compute budget compared to conventional 
|   |                                  serialization/deserialization.
|   | 
│   └── state.rs                     # Contains general utilities related to state 
|                                      accounts. Includes the main registry of account 
| types for this program: the Tag enum |
|--------------------------------------|
└── tests                            # Contains integration tests
    |
    ├── common                       # Contains common integration testing utilities
    │   ├── mod.rs                   
    │   └── utils.rs                 
    └── functional.rs                # The main integration test. Should be used as a 
                                      high-level and primitive test of every 
                                      instruction.


Writing a simple program

In this section, we will work through the writing of a simple token vesting program. This will enable us to better understand how to write programs using the bonfida toolkit.

What we are trying to create

Our token vesting program will be a simple solution to a common problem. Given a supply of tokens, how can we use on-chain logic to distribute those tokens to investors while making sure that those tokens cannot be used until a predetermined delay, or schedule? The basic idea is that we use an on-chain program (or smart contract) to hold the funds and gradually unlock those. This allows the whole transaction to be completely trustless: the claimers can trust in the fact that they will receive those tokens on the agreed-upon schedule, and the providers can trust in the fact that the claimers will not bypass the vesting schedule. In this context, the program acts as a trusted third-party.

Any token vesting transactions thus has two types of users: the claimers, and the providers. The core logic requires only two types of operation: vesting contract creation, and claiming. We call those operations the program's instructions.

  • create will initialize a vesting contract for a given quantity of a particular token, with a set schedule.
  • claim will transfer unlocked funds from a particular vesting contract to its receiver.

The last thing we need is a way for the program to hold state. This means that we need a program to remember the active vesting contracts and to hold their associated funds. In order to hold the vested assets, each token vesting contract has an associated vault. A vault is a normal token account which is owned by an associated PDA, more on that later.

Each vesting contract will thus have an associated state account owned by our program. These accounts will hold a serialized version of a VestingContract object:


#![allow(unused)]
fn main() {
pub struct VestingContract {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    /// Describes the token release schedule
    pub schedule: Vec<VestingSchedule>
}

pub struct VestingSchedule {
    /// When to unlock the assets
    pub unlock_timestamp: u64,
    /// What quantity of assets to unlock
    pub quantity: u64
}
}

In reality, we will define the VestingContract object quite differently in order to greatly optimize its on-chain serialization and deserialization. This will allow us to handle arbitrarily complex vesting schedules while never running out of compute budget. Thus, the actual VestingContract object will be defined in the following way:


#![allow(unused)]
fn main() {
pub struct VestingContract<'a> {
    pub header: &'a mut VestingContractHeader,
    pub schedule: &'a mut [VestingSchedule]
}

pub struct VestingContractHeader {
    pub owner: Pubkey,
    pub vault: Pubkey,
    pub current_schedule_index: u64,
    pub signer_nonce: u8,
    pub _padding: [u64; 3],
    pub schedule_len: u32
}

pub struct VestingSchedule {
    pub unlock_timestamp: u64,
    pub quantity: u64
}
}

We will explore where this added complexity comes from, and why it's actually worth it.

Encoding state

In the previous section, we described our program's architecture in rough terms. To begin with, let's look at how we can implement our program's data structures.

How to interact with data on Solana

With Solana programs, any persistent data needs to be encoded into accounts. Within the context of a program, an account is represented by an AccountInfo object. The data itself is a mutable reference to a slice of bytes. There are several available options to make use of this slice of bytes.

Thus, an account can represent quite a few things. For instance, it can be a token account which acts as a certificate of ownership for a particular token.

Borsh

Using the borsh library, an object can be serialized and deserialized from an array of bytes. This approach has several advantages:

  • Packed representation: this representation is quite space-efficient.
  • Easier to write bindings in languages other than Rust.
  • No constraint on memory alignment and padding.

The main disadvantage posed by Borsh is that the serialization and deserialization operations have to iterate through the entire data slice, and perform copy operations. This means that this approach is impractical for larger data structures and can consume a substantial amount of the available compute budget.

While this approach is recommended by Solana for its relative simplicity and good compatibility with existing tooling, for better performance, convenience, and even readability, we represent the following hybrid approach.

Bytemuck on-chain, Borsh off-chain

The bytemuck library allows for safe type casting in Rust. While type casting is a very common concept in the context of C and C++, it has a bad reputation within the Rust community as being synonymous with memory unsafety, which goes against the language's key design principles. However, using bytemuck allows us to get the best of both worlds: the efficiency of type casting without its pitfalls.

Type casting is extremely efficient because it doesn't copy any data. This means that very large data structures can be handled with no compute overhead and optimal readability. When using a serialization approach, it is necessary to commit any changes made to an object to its storage account. This is an extremely error-prone approach.

These advantages come at the cost of some constraints:

  • Certain types have to be aligned to particular offsets.
  • Object sizes must be a multiple of their own alignment constraint.

While working with these kinds of abstract constraints can seem quite daunting, bytemuck provides us with convenient derive macros which are able to detect these issues before compilation. By looking at an example, we'll see how these constraints come into play, and how we can deal with them.

Finally, once our data structure is well-defined to play along with bytemuck, we can write a compatible borsh schema to leverage its implementations across various programming languages.

VestingContract

The core data structure on which we are building our program will be called VestingContract. Since we will be using type-casting through the bytemuck library, we need to iterate and think about the various types of constraints our definition will need to obey.

First iteration: what data do we need?

Our program's logic will need something equivalent to the following data structure.


#![allow(unused)]
fn main() {
pub struct VestingContract {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    /// Describes the token release schedule
    pub schedule: Vec<VestingSchedule>
}

pub struct VestingSchedule {
    pub unlock_timestamp: u64,
    pub quantity: u64
}
}

In our freshly created project, let's start by renaming the src/state/example_state_cast.rs to src/state/vesting_contract.rs. We'll also delete the src/state/example_state_borsh.rs file. Hopefully our IDE will take care of the refactor. Let's then refactor the ExampleStateCast struct to VestingContract and paste in the above definitions.

We should be left with something like this:

#[derive(Clone, Copy, Zeroable, Pod)]
#[allow(missing_docs)]
#[repr(C)]
pub struct VestingContract {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    /// Describes the token release schedule
    pub schedule: Vec<VestingSchedule>,
}

pub struct VestingSchedule {
    pub unlock_timestamp: u64,
    pub quantity: u64,
}

Since VestingContract implements the Clone, Copy, Zeroable and Pod traits we need to derive these traits for VestingSchedule as well. The last two traits are related to bytemuck and enable type casting. Same goes for the repr(C) attribute which is essential for type casting.


#![allow(unused)]
fn main() {
#[derive(Clone, Copy, Zeroable, Pod)]
#[allow(missing_docs)]
#[repr(C)]
pub struct VestingContract {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    /// Describes the token release schedule
    pub schedule: Vec<VestingSchedule>,
}

#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(C)]
pub struct VestingSchedule {
    pub unlock_timestamp: u64,
    pub quantity: u64,
}
}

However, the above still does not compile. To sum up, the core bytemuck trait we are interested in is the Pod trait. This trait requires the Zeroable and Copy traits. However, a Vec does not implement the Copy trait. In general, this is due to the fact that Vec is a pointer which owns a section of heap memory. Copying it would mean allocating a new section of heap memory, whereas the Copy trait is reserved for variables which exist on the stack.

In fact it is impossible to directly cast Vec objects. To work around this limitation, we split our object into a reference to a header and a reference to a slice of VestingSchedule objects. This means that the VestingContract object will hold cast references to the underlying objects, instead of being a direct cast by itself. This layer of indirection is of no consequence in terms of performance, and allows us a lot more flexibility in terms of the kind of data structures we can type cast.

Second Iteration: fixing our definitions

We begin by renaming the VestingContract object into VestingContractHeader, removing the schedules from it:


#![allow(unused)]
fn main() {
#[derive(Clone, Copy, Zeroable, Pod)]
#[allow(missing_docs)]
#[repr(C)]
pub struct VestingContractHeader {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    pub _padding: [u8; 7],
}

#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(C)]
pub struct VestingSchedule {
    pub unlock_timestamp: u64,
    pub quantity: u64,
}
}

Ah, and we also added an extra field called _padding. This is where using directly type-cast data structures can seem daunting at first. Let's take some time to talk about memory alignment.

An aside on padding and memory alignment

Modern CPUs are wonderful things which are able to manipulate all kinds of objects. In practice, every operation which a CPU can execute (also called an assembly instruction), is actual physical wiring on the chip. Depending on the chip, designers can cut down on complexity and conversely increase performance by requiring data to be aligned in a certain way. As a somewhat appropriate analogy, let's take a list of five 8-letter words:

although
boundary
calendar
chemical
diameter

Providing a CPU with non-aligned data is akin to presenting you the same list in this way:

although
       boundary
   calendar
                chemical
 diameter

It's just harder to parse, because our ability to parse lists of words is hard-wired to a certain format. So let's give our poor CPUs a break and look at alignment constraints on the BPF architecture.

For any memory address, we say that it is aligned to n if its address is a multiple of n. On the Solana BPF (the on-chain program runtime) and x86_64 architectures, alignment constraints are as follows:

  • Primitive types must be aligned to their size, up to a maximum of 8.
  • Structs must be aligned to the maximum of their fields' alignment constraints.

For primitive types, this yields the following table:

Primitive TypeSize (in bytes)Alignment constraint
u8, i811
u16, i1622
u32, i3244
u64, i6488
u128, i128168

When the runtime provides you with an account, the address of its first byte can be considered to be 0. The first 8 bytes are going to be used by the account or instruction tag, which is encoded as a u64. We would be tempted to use a u8 here, but this would make the rest of the buffer start aligned to 1, which precludes all but the most basic types.

On the BPF architecture, being 8-aligned is essentially equivalent to being 0-aligned. Fortunately, this is also the case on the common x86_64 architecture. Unfortunately, the Apple ARM architecture (i.e. Apple M1, M2, etc.) can sometimes ask for an alignment of 16. To get around this, we can use a compilation flag to replace every instance of a type-cast u128 by a [u64;2]. Even if you don't own an Apple ARM computer, it is important to think about members of the community that do. We will discuss how to tackle this issue in an annex.

So looking at our VestingContractHeader object, we can see that it contains a u64 field. This means that its size has to be a multiple of 8, which is why we add 7 bytes of padding. In practice, these will be implicitly 0 and won't be used by any of our program logic. As a nice bonus, if you later want to add a new field to the object, you'll be able to do so while maintaining backwards compatibility!

Final definition

We then define our VestingContract object. You should notice that since VestingContract holds references, it has a generic lifetime argument. While this can seem daunting, it will not affect its use. bonfida-utils can help us out here by automatically deriving the WrappedPodMut trait. This trait is specifically designed for objects which hold references to Pod objects which are contiguous in memory.

#[derive(WrappedPodMut)]
pub struct VestingContract<'a> {
    pub header: &'a mut VestingContractHeader,
    pub schedules: &'a mut [VestingSchedule],
}

The impl blocks are refactored in the following manner:


#![allow(unused)]
fn main() {
impl VestingContractHeader {
    pub const LEN: usize = std::mem::size_of::<Self>();
}

impl VestingSchedule {
    pub const LEN: usize = std::mem::size_of::<Self>();
}

/// An example PDA state, serialized using Borsh //TODO
#[allow(missing_docs)]
impl<'contract> VestingContract<'contract> {
...
}
}

What remains is to update VestingContract's initialize and from_buffer method. The first step in doing so is to refactor the super::Tag::ExampleStateCast object to super::Tag::VestingContract. Doing so finalizes the initialize method: its only role is to write the account's tag into the first 8 bytes of the data buffer. Tags are incredibly important to ensure that an account is being interpreted correctly: we wouldn't want an attacker to substitute one type of account for another. This is a way of implementing runtime type checks on all accounts to decrease our program's attack surface.

The initialize method also checks that the account has not been initialized before. This gives us a double guarantee: that we're not attempting to corrupt/overwrite existing data, and that the entire account's data is zeroed out.

Finally, we need to fix the from_buffer method. The first 4 lines of the method do not need to be changed. The correct implementation is as follows:


#![allow(unused)]
fn main() {
    pub fn from_buffer(
        // We use the `contract lifetime here since our VestingContract
        // is a set of cast references to this buffer
        buffer: &'contract mut [u8],
        expected_tag: super::Tag,
        // Since we're using a wrapper of references, we return Self
        // and not &mut Self
    ) -> Result<Self, TokenVestingError> {
        let (tag, buffer) = buffer.split_at_mut(8);
        if *bytemuck::from_bytes_mut::<u64>(tag) != expected_tag as u64 {
            return Err(TokenVestingError::DataTypeMismatch.into());
        }
        // The WrappedPodMut trait does all the heavy lifting here
        Ok(Self::from_bytes(buffer))
    }
}

One important thing to know is that we're casting the entire length of the buffer. This means that the account's allocated length must be of a valid size, otherwise this operation will fail.

To make this less error-prone, we need to write a helper static method called compute_allocation_size which determines the valid size for a VestingContract data account in terms of its desired number of schedules:


#![allow(unused)]
fn main() {
    pub fn compute_allocation_size(number_of_schedules: usize) -> usize {
        8 + VestingContractHeader::LEN + number_of_schedules * VestingSchedule::LEN
    }
}

You should note that we're always adding 8 bytes to account for the type tag.

find_key is the last method we need to take a look at. This method is useful when we want our account's address to be uniquely determined by a set of parameters. For instance, if we wanted to allow for only one vesting contract per recipient, we could write find_key as


#![allow(unused)]
fn main() {
    pub fn find_key(program_id: &Pubkey, recipient: &Pubkey) -> (Pubkey, u8) {
        let seeds: &[&[u8]] = &[Self::SEED, &recipient.to_bytes()];
        Pubkey::find_program_address(seeds, program_id)
    }
}

This means that our vesting contract would be a unique program-derived address (PDA). However, in our case, we do not want to add this constraint since any user can hold several vesting contracts. This is why we won't be using a PDA here, and we can just delete the find_key method and its associated SEED constant.

If you have been following along, your src/state/vesting_contract.rs file should look something like this:

use bonfida_utils::WrappedPodMut;
use bytemuck::{Pod, Zeroable};
use solana_program::pubkey::Pubkey;

use crate::error::TokenVestingError;

#[derive(WrappedPodMut)]
pub struct VestingContract<'a> {
    pub header: &'a mut VestingContractHeader,
    pub schedules: &'a mut [VestingSchedule],
}

#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(C)]
/// Holds vesting contract metadata
pub struct VestingContractHeader {
    /// The eventual token receiver
    pub owner: Pubkey,
    /// The contract escrow vault
    pub vault: Pubkey,
    /// Index in the current schedule vector of the last completed schedule
    pub current_schedule_index: u64,
    /// Used to generate the signing PDA which owns the vault
    pub signer_nonce: u8,
    pub _padding: [u8; 7],
}

#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(C)]
/// An item of the vesting schedule
pub struct VestingSchedule {
    /// When the unlock happens as a UTC timestamp
    pub unlock_timestamp: u64,
    /// The quantity of tokens to unlock from the vault
    pub quantity: u64,
}

impl VestingContractHeader {
    pub const LEN: usize = std::mem::size_of::<Self>();
}

impl VestingSchedule {
    pub const LEN: usize = std::mem::size_of::<Self>();
}

impl<'contract> VestingContract<'contract> {
    /// Initialize a new VestingContract data account
    pub fn initialize(buffer: &mut [u8]) -> Result<(), TokenVestingError> {
        let (tag, _) = buffer.split_at_mut(8);
        let tag: &mut u64 = bytemuck::from_bytes_mut(tag);
        if *tag != super::Tag::Uninitialized as u64 {
            return Err(TokenVestingError::DataTypeMismatch);
        }
        *tag = super::Tag::VestingContract as u64;
        Ok(())
    }

    /// Cast the buffer as a VestingContract reference wrapper
    pub fn from_buffer(
        buffer: &'contract mut [u8],
        expected_tag: super::Tag,
    ) -> Result<Self, TokenVestingError> {
        let (tag, buffer) = buffer.split_at_mut(8);
        if *bytemuck::from_bytes_mut::<u64>(tag) != expected_tag as u64 {
            return Err(TokenVestingError::DataTypeMismatch);
        }
        Ok(Self::from_bytes(buffer))
    }

    /// Compute a valid allocation size for a VestingContract
    pub fn compute_allocation_size(number_of_schedules: usize) -> usize {
        8 + VestingContractHeader::LEN + number_of_schedules * VestingSchedule::LEN
    }
}


Writing an instruction: create

In the previous sections, we have defined our program's main data structure VestingContract. The next step is to write our program's one of two main primitives: create.

We'll start by renaming the src/processor/example_instr.rs file to create.rs. Similarly we'll rename the ExampleInstr variant of the instruction::ProgramInstruction enum to Create, and the instruction::example binding to create. We should also alter the log message in processor.rs from "Instruction: Example Instruction" to "Instruction: Create", and update the instruction comment at the top of create.rs to //! Create a new token vesting contract.

This primitive will perform several operations:

  • Initialize a new VestingContract account/object.
  • Configure the VestingContract with user-provided parameters.
  • Transfer funds into the program vault.

An instruction's specification is defined by its Accounts and Params objects. Let's start by thinking about what kinds of accounts we'll need, and then we'll take a look at the associated parameters.

Required accounts

For our vesting contract, we need to know about:

  • The eventual token receiver, so we'll need a recipient account.
  • The account that will hold the VestingContract data, the vesting_contract account. We'll need this account to be writable and owned by our current program.
  • The escrow vault, the vault account. We'll need this account to be writable since we're going to transfer funds into it. It needs to be owned by the spl_token program as well.
  • The spl_token_program account, since we're going to be performing token transfers.
  • The source_tokens account, which will need to be writable as well.
  • The source_tokens_owner account, which we'll need as a signer to enable us to successfully transfer the tokens into our vault.

The associated Accounts struct thus looks like this:


#![allow(unused)]
fn main() {
#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
    /// SPL token program account
    pub spl_token_program: &'a T,

    /// The account which will store the [`VestingContract`] data structure
    #[cons(writable)]
    pub vesting_contract: &'a T,

    /// The contract's escrow vault
    #[cons(writable)]
    pub vault: &'a T,

    #[cons(writable)]
    /// The account currently holding the tokens to be vested
    pub source_tokens: &'a T,

    #[cons(signer)]
    /// The owner of the account currently holding the tokens to be vested
    pub source_tokens_owner: &'a T,

    /// The eventual recipient of the vested tokens
    pub recipient: &'a T,
}
}

The first thing to notice is that the Accounts struct is generic over what we actually mean by what an account is. The reason why it is generic is to enable the same struct to be used with references to Pubkey objects when writing bindings, or AccountInfo objects when writing the instruction's logic.

The InstructionsAccount trait is useful to auto-generate Rust instruction bindings, which we'll make use of later. This trait's automatic derivation may need annotations. This is where the #[cons(signer)] and #[cons(writable)] field attributes come in. In general, struct fields with the InstructionsAccount auto-derived trait can take a #[cons(...)] attribute. cons stands for constraint: we can require the account to be writable, to be a signer, or both. This gives the valid attributes #[cons(signer)], #[cons(writable)], #[cons(signer, writable)] and #[cons(writable, signer)].

We then need to implement a method to parse the &[AccountInfo] slice into our Accounts struct. We will also use this method to perform rudimentary but essential security checks. You should understand the reason behind each check in order to familiarize yourself with basic security base practices when developing for Solana.

impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
    pub fn parse(
        accounts: &'a [AccountInfo<'b>],
        program_id: &Pubkey,
    ) -> Result<Self, ProgramError> {
        let accounts_iter = &mut accounts.iter();
        let accounts = Accounts {
            spl_token_program: next_account_info(accounts_iter)?,
            vesting_contract: next_account_info(accounts_iter)?,
            vault: next_account_info(accounts_iter)?,
            source_tokens: next_account_info(accounts_iter)?,
            source_tokens_owner: next_account_info(accounts_iter)?,
            recipient: next_account_info(accounts_iter)?,
        };

        // Check keys
        check_account_key(accounts.spl_token_program, &spl_token::ID)?;

        // Check owners
        check_account_owner(accounts.vesting_contract, program_id)?;
        check_account_owner(accounts.vault, &spl_token::ID)?;
        check_account_owner(accounts.source_tokens, &spl_token::ID)?;

        // Check signer
        check_signer(accounts.source_tokens_owner)?;

        Ok(accounts)
    }
}

The first part of this method will always be quite similar. However, the second part is where we can already protect ourselves against quite a few basic attacks (you would be surprised how many programs have been attacked for failing to perform these basic kinds of checks). These checks are also the first thing an auditor will look for, and having them all in the same place facilitates their work. Sometimes even auditors can get confused when these checks are not displayed obviously enough!

Checks

Let's go through these checks one by one and explain why they are all here. You won't need to necessarily think too deeply about these checks when writing your own programs, you should just ask yourself: how can I constrain these accounts as much as possible using the rudimentary check_signer, check_account_owner and check_account_key security primitives? As a general rule of thumb, the tighter the constraint, the smaller the attack surface.

check_account_key(accounts.spl_token_program, &spl_token::ID)?;

This check is essential. We'll be performing token transfers using this program and we need to be sure that we're actually calling the right program. An attacker could substitute their own program here and leverage the source_tokens_owner signature to take ownership of the token account!

check_account_owner(accounts.vesting_contract, program_id)?;

We're going to be editing the vesting_contract account data, and we need to be sure that only our program can alter this data. The Solana runtime constraints ensure that every byte of a program-owned account's data is either:

  • determined by the program's logic.
  • freshly allocated and therefore 0.

This means that, as long as we trust our own program (which isn't a given considering our own program might be buggy), we should be able to trust the data that's held by its owned accounts. Failing to perform this check will expose our programs to all sorts of potential attacks.

In reality our program's logic should already make this check redundant: the VestingContract::initialize method will either attempt to modify the account's data (which is only possible when it is owned by the current program), or just fail. Does this mean we should remove this check? Absolutely not. Our program's logic might change in the future, and we don't want this metaphorical sword of Damocles to hang over our heads. These checks are computationally inexpensive, and extremely valuable. Overuse them.

check_account_owner(accounts.vault, &spl_token::ID)?;
check_account_owner(accounts.source_tokens, &spl_token::ID)?;

and

check_signer(accounts.source_tokens_owner)?;

Technically unnecessary since the call to spl_token_program will take care of these for us. Don't even think about removing those.

Parameters

In addition to the above set of accounts, we need some extra information from the user in order to properly configure the vesting contract. We only have access to a user-provided slice of bytes, called the instruction data. This slice of bytes can be cast into a Params wrapper object. The approach will be very similar to the way we handled the VestingContract object in the previous section.

Let's take a direct look at the Params struct:

#[derive(WrappedPod)]
pub struct Params<'a> {
    // Needs to be a `u64` for the schedules slice to be well-aligned in memory
    signer_nonce: &'a u64,
    schedule: &'a [VestingSchedule]
}

In order to handle Params being a wrapped Pod in our Rust instruction bindings, we need to activate the instruction_params_wrapped feature. In the program Cargo.toml, edit the bonfida-utils dependency to:

bonfida-utils = {version = 0.2, features = ["instruction_params_wrapped"]}

Then in instruction.rs, update the create binding to:

#[allow(missing_docs)]
pub fn create(accounts: create::Accounts<Pubkey>, params: create::Params) -> Instruction {
    accounts.get_instruction_wrapped_pod(crate::ID, ProgramInstruction::Create as u8, params)
}

We will discuss why the signer_nonce parameter is required in a later section. In combination with the accounts defined above, we have all the information we need to begin writing the instruction logic!

Instruction Logic

Our entry point into the instruction is always a process function with the following signature:

pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
    ...
}

The first step is to parse our Accounts struct using the Accounts::parse method we defined above. Remember that this method is also responsible for performing basic security checks. We then unwrap our params object into local variables for convenience.

pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
    let accounts = Accounts::parse(accounts, program_id)?;
    let Params { signer_nonce, schedule } = params;

    // We only want a one-byte signer nonce
    let signer_nonce = *signer_nonce as u8;
}

The first item to take care of is the initialization of the VestingContract object. We can start by checking that the given account is of the correct size:

let expected_vesting_contract_account_size = VestingContract::compute_allocation_size(schedule.len());

    if accounts.vesting_contract.data_len() != expected_vesting_contract_account_size {
        msg!("The vesting contract account is incorrectly sized for the supplied schedule!");
        return Err(ProgramError::InvalidArgument)
    }

A refinement to this program would involve taking care of the allocation ourselves, thus making sure that the supplied account will be of the correct size. However, as long as we supply our users with bindings which can take care of this allocation, this is not really an issue. Allocating an account within a program is only required when allocating Program-Derived Addresses (PDAs). As discussed earlier, this is not a concern here.

Once we have verified that the account is properly sized, we can actually initialize our VestingContract account, and then retrieve it!

// This guard variable is the owned pointer to our actual data
// Once it goes out of scope (or is dropped), all its dependent references are dropped
let mut vesting_contract_guard = accounts.vesting_contract.data.borrow_mut();

VestingContract::initialize(&mut vesting_contract_guard)?;
let vesting_contract = VestingContract::from_buffer(&mut vesting_contract_guard, state::Tag::VestingContract)?;

Now that our VestingContract object is properly initialized, we need to save the user-provided configuration.

*vesting_contract.header = VestingContractHeader { 
    owner: *accounts.recipient.key, 
    vault: *accounts.vault.key,
    current_schedule_index: 0,
    signer_nonce,
    _padding: [0;7] 
};

let mut total_amount = 0u64;
let mut last_timestamp: u64 = 0;
for (schedule, slot) in schedule.iter().zip(vesting_contract.schedules.iter_mut()) {
    if schedule.unlock_timestamp < last_timestamp {
        msg!("The schedules should be provided in order!");
        return Err(ProgramError::InvalidArgument);
    }
    last_timestamp = schedule.unlock_timestamp;
    *slot = *schedule;
    total_amount = total_amount.checked_add(schedule.quantity).unwrap();
}

We keep track of the total amount as it represents what we'll have to transfer into our vault. Notice that we use checked_add to compute our sum. Using checked math is absolutely essential. Sometimes it might seem redundant. Sometimes it might actually be redundant. But it's better to think about it this way: if it can overflow, it will overflow. Don't even think about it!

The only exception to this rule is checked_div when dividing by a constant. If you know it to be non-zero because it says so on the same line of code, then you should favor readability.

We also check that the schedules are given in the right order with last_timestamp. This will simplify our computation of what quantity of assets should be unvested. This is typically the kind of refinement that you can notice later on while implementing other instructions. Always check your assumptions and enforce them if necessary.

Finally, we transfer the funds to our vault using the spl_token transfer instruction:

let instruction = spl_token::instruction::transfer(
    &spl_token::ID, 
    accounts.source_tokens.key, 
    accounts.vault.key, 
    accounts.source_tokens_owner.key, 
    &[], 
    total_amount
)?;

invoke(&instruction, &[
    accounts.spl_token_program.clone(),
    accounts.source_tokens.clone(),
    accounts.vault.clone(),
    accounts.source_tokens_owner.clone()
])?;

Regarding the use of invoke, the best way to know what kind of accounts to provide is to:

  • first add the account for the program we're invoking
  • look at the binding code and add all the accounts in order

We're done... right?

As it stands our program has a major vulnerability which enables a fraudulent vesting contract issuer to dupe their recipient. They can run away with the entirety of their funds! If you can't find this vulnerability on your own, that's completely normal and we'll discuss general practices to analyze your code. Try it anyways, and then let's fix it.

Patching a vulnerability

As mentioned in the previous section, our program in its current state has a major vulnerability. It enables a malicious issuer to dupe a recipient into thinking they trustlessly own vested assets, when in reality they don't. Let's apply a methodical approach to find this vulnerability.

Thinking about attack vectors

Very broadly speaking, a vulnerability exists when there is some way to manipulate a program's inputs to make it behave in unexpected ways. This means that patching vulnerabilities means looking at a program's inputs and making sure that those are safe. Solana programs have two different kinds of input with different security implications:

  • Instruction data
  • Account data
  • System variables

Instruction data

This attack vector is the first we have to secure, and it is also the simplest. Securing the instruction data input means making sure that both the deserialization and business logic can handle all possible inputs. When we use Borsh or bytemuck as deserializers, any bit pattern which does not conform to our schema will result in an error. This is precisely what we want. As a corollary, you should be careful when implementing custom deserialization logic. Borsh and the tools provided by bonfida-utils should be enough for your use-case.

The harder part is thinking about the business logic itself. You should think about what kind of values your program should allow as input. Be as restrictive as possible. If a user reports that their own use-case for your product is not handled due to these restrictions, you can update your program. If your program's state / assets are compromised, you can still patch the vulnerability, but the incident will probably have cost you.

To secure your program's business logic, we recommend a combination of unit and integration testing. It can be very useful to use coverage testing tools to make sure that your business logic is sound for every edge case. We will discuss testing in a later section.

Account data

The hardest attack vector to secure is an instruction's accounts. The first step in securing this attack vector is to follow our account checks recommendations detailed in the previous section. The second and hardest step is to look at account data itself.

Broadly speaking, your program will interact with two kinds of accounts:

  • Accounts which it owns (and are therefore part of its own state).
  • Accounts owned by other programs.

Thinking about your program's state

To secure the first type of accounts, a simple but very effective strategy is to use account tags. In the Bonfida style, the first 8 bytes of any account are reserved as a descriptor of what kind of account we're dealing with. If your program's logic allows for the closing of accounts, a closed account's tag should be set to a Disabled value. This makes sure that those accounts can't be used as an attack vector.

Outside of this notion of account tagging, the safety of an account owned by your own program depends on the safety of your entire business logic. This is due to the fact that a program-owned account's data is either just zeros or the result of your program's previous interactions with it. This means that whenever your program modifies an account's data (or its state in general), you should make sure that the state remains safe in all situations. To deal with these kinds of vulnerabilities, a clear coding style and proper reviews and even audits are your friends.

Finally, we have accounts owned by other programs. More often than not, depending on an other program's accounts means depending on this program's own safety. Always think twice before integrating new on-chain dependencies into your projects:

  • Is the program well reviewed and audited?
  • Is the program widely used?
  • If the program is upgradable, can the team behind it be trusted?
  • Do you have a good understanding of the program and its safety guarantees?

In the case of our token-vesting contract, we depend on the spl-token program. Since this program is part of the official Solana Program Library, and has been around for a while now, we can assume that it's quite safe. This ticks off the first three requirements. However, the final point stands: we need to gain an understanding of this program's security guarantees before we can use it in good conscience. As it just so happens, our program is currently vulnerable because we don't look at what's inside the vault spl-token account.

Understanding your on-chain dependencies: spl-token

Since on-chain programs are security-critical applications, it is essential to take the time to read through the official documentation. It often contains security recommendations and can provide you with a better idea of what you are dealing with. In the case of spl-token, the documentation can be found here. Since we are going to be using the smart contract's bindings, we should take a look at the library documentation as well. It can be found here.

When using an on-chain dependency, we should gain a deep understanding of the interface we are going to be using. A program's interface has two components :

  • Its instruction specification (what is encoded in instruction_data)
  • Its state, and how it is encoded into accounts

Instruction specification

We're using spl-token's transfer instruction. Let's take a look at the associated binding's documentation here. This is what it looks like as of writing this :

/// Creates a Transfer instruction.
pub fn transfer(
    token_program_id: &Pubkey, 
    source_pubkey: &Pubkey, 
    destination_pubkey: &Pubkey, 
    authority_pubkey: &Pubkey, 
    signer_pubkeys: &[&Pubkey], 
    amount: u64
) -> Result<Instruction, ProgramError>

Ah, the documentation is quite lackluster. Looking at the binding's signature already gives us a bit of information. However, we want to know as much as possible about this instruction. We have several options here :

  • Find some documentation elsewhere.
  • Read the instruction code.

As a general rule of thumb, there's a pretty straightforward way to find the documentation we need elsewhere. For some reason, program developers are more often than not given less documentation than frontend ones. Maybe it's because program development is supposed to be harder? Really I don't know, and I would encourage any program developer to always think about both web clients and other programs as interfaces to tailor for. I sincerely hope you're reading this and finding this paragraph outdated. In the meantime, we can get the information we deserve by looking at the JavaScript/TypeScript documentation here. This is what it roughly looks like:

Transfer tokens from one account to another

Parameters

  • connection: Connection

    Connection to use
  • payer: Signer

    Payer of the transaction fees
  • source: PublicKey

    Source account
  • destination: PublicKey

    Destination account
  • owner: PublicKey | Signer

    Owner of the source account
  • amount: number | bigint

    Number of tokens to transfer
  • multiSigners: Signer[] = []

    Signing accounts if owner is a multisig
  • Optional confirmOptions: Signer[] = []

    Options for confirming the transaction
  • programId: PublicKey = TOKEN_PROGRAM_ID

    SPL Token program account

Returns Promise<TransactionSignature>

Signature of the confirmed transaction

We get more information here. Let's not care about any parameter mentioned here which isn't part of our Rust binding's signature. This eliminates connection and confirmOptions from consideration. In general, you should make sure that you understand every option, because it's the only way to determine what's important.

Using this knowledge, we can annotate the Rust binding for our particular use-case: transferring funds to a vault:

/// Creates a Transfer instruction.
pub fn transfer(
    /// This will be spl_token::ID since we're using the common spl_token instance
    token_program_id: &Pubkey, 
    /// This will be the key of the provided account currently holding the tokens to vest
    source_pubkey: &Pubkey, 
    /// Our vesting contract's vault key
    destination_pubkey: &Pubkey, 
    /// The owner of the source token account's key
    authority_pubkey: &Pubkey, 
    /// We won't support vesting tokens from a multisig source account.
    /// This will be an empty array.
    signer_pubkeys: &[&Pubkey], 
    /// The total quantity of tokens to vest
    amount: u64
) -> Result<Instruction, ProgramError>

We're quite lucky here: the existing call to transfer is correct in our create instruction!

Account specification

Our create instruction takes several spl_token-owned program accounts as input. We should therefore understand what kind of accounts we should expect here. Using the library documentation, we find that spl_token's accounts are all defined in a state module here. Out of all the different types of state defined by spl_token, we're only interested in the Account type. This is because we're only using program accounts which hold tokens.

Let's take a look at the definition of spl_token::state::Account:

pub struct Account {
    /// The mint associated with this account
    pub mint: Pubkey,
    /// The owner of this account.
    pub owner: Pubkey,
    /// The amount of tokens this account holds.
    pub amount: u64,
    /// If `delegate` is `Some` then `delegated_amount` represents
    /// the amount authorized by the delegate
    pub delegate: COption<Pubkey>,
    /// The account's state
    pub state: AccountState,
    /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
    /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
    /// wrapped SOL accounts do not drop below this threshold.
    pub is_native: COption<u64>,
    /// The amount delegated
    pub delegated_amount: u64,
    /// Optional authority to close the account.
    pub close_authority: COption<Pubkey>,
}

Here we find that every field is properly documented, which will definitely make our job easier! The next step is to go through each field and think about constraints. How can we use each field to constrain our input accounts as much as possible? I have annotated the same struct definition to see what kind of constraints we could enforce.

pub struct Account {
    /// The mint associated with this account
    // We could check that our vault and destination mints match
    // However, we know that the spl_token program itself will perform this check
    pub mint: Pubkey, 
    /// The owner of this account.
    // Our vault should be owned by an account controlled by our program!
    // Otherwise the transfer we perform to our vault creates a fake contract
    // since our program cannot control the vault's funds!
    pub owner: Pubkey,
    /// The amount of tokens this account holds.
    // We expect our vault to be empty, so let's make sure.
    pub amount: u64,
    /// If `delegate` is `Some` then `delegated_amount` represents
    /// the amount authorized by the delegate
    // Our vault should not have a delegate authority.
    pub delegate: COption<Pubkey>,
    /// The account's state
    // The account should be Initialized
    pub state: AccountState,
    /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
    /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
    /// wrapped SOL accounts do not drop below this threshold.
    // Not caring about this makes our token vesting contract support wrapped SOL.
    pub is_native: COption<u64>,
    /// The amount delegated
    // This should be 0, but we're already enforcing the delegate field to be None
    pub delegated_amount: u64,
    /// Optional authority to close the account.
    // This should absolutely be None for our vault.
    // It should be impossible for a third-party to close our vault account.
    pub close_authority: COption<Pubkey>,
}

Looking at the Account struct, we were able to gain a clearer picture of what we should expect from our vault account. Note that we're mostly focusing on enforcing constraints on the vault account here. The other token account at play in our create instruction only has to emit the tokens. We can trust this simple constraint to be enforced by spl_token itself when we call the transfer instruction.

However, our vault account will remain tied to our vesting contract instance for the entirety of its lifetime. We must therefore make sure that we're getting precisely what we need here. Our vault account can be thought of as a piece of third-party state for our program. This is why we need to understand it as well as we understand our program's own state.

Implementing checks on third-party state

In the above discussion, we have determined that our vault should be owned by an account our program controls and can sign for. This is where program-derived addresses (PDAs) come into play.

Aside: what are Program-Derived Addresses (PDAs)?

PDAs are Solana's solution for a recurring generic problem in program design : how can our program own anything of value? Conventionally, owning an asset on a blockchain means controlling a private key the public key of which is set as its owner. Then, users can sign transactions to authorize operations on assets they own. The key to all of this is that the private key is only known to its owner.

Sometimes, we need a program to own something in the same way that a user does. However, any information a program has access to is available to anyone, so we can't really give a private key to our program. What we need is a public key which isn't tied to a private key, but instead to a particular program. This is precisely what PDAs are.

A PDA is uniquely derived from an array of seeds. To generate a PDA, we can use one of two methods Pubkey::find_program_address, or Pubkey::create_program_address. The difference between the two is that create_program_address needs valid seeds which generate a valid PDA. A valid PDA is a Pubkey which we're sure doesn't have a private key. It lies outside the ed25519 curve used by Solana. find_program_address will take a seed array and find a u8 to add to the seed array such that the resulting array is a valid PDA seed. We call this u8 the signer_nonce.

We can relate the two methods:

let seeds: &[u8] = &[...];
let (k, nonce) = Pubkey::find_program_address(&[seeds], program_id).unwrap();
let k2 = Pubkey::create_program_address(&[seeds, &[signer_nonce]]).unwrap();
// k and k2 will be the same
assert_eq!(k, k2);

In general, find_program_address can be relatively inefficient: it loops over potential nonces until it finds a valid one. The real issue is that the length of this loop is quite unpredictable. In most cases, it will be very short. This means that the entirety of your testing scenarios can only test for short loops, and edge cases can show up in production later down the line. Therefore, as a general rule of thumb, find_program_address should be avoided on-chain because it consumes an unreliable amount of compute budget. There are exceptions to this rule, especially when PDAs are used as a mapping function. This is why we ask the user to provide a signer_nonce: find_program_address is executed off-chain.

A PDA's seeds serve a similar purpose to a private key. When a program needs to sign for a PDA, it uses the PDA's seeds in a call to invoke_signed. The runtime can then make sure that the PDA to sign for corresponds to those seeds and the calling program.

To add a layer of security, we want our vesting contract instances to be as compartmentalized as possible. Therefore, we will use the VestingContract key as seeds. Each instance already has its own vault, and this allows each contract to have its own separate signing authority. The alternative to this is related to the idea of central state, which can be required by some use cases and is supported by bonfida-utils.

Writing the check_vault_account method

Using the constraints we came up with by analyzing the spl_token::Account struct, we can add to create.rs:

fn check_vault_account(
    vault: &AccountInfo,
    program_id: &Pubkey,
    contract_key: Pubkey,
    signer_nonce: u8,
) -> Result<(), ProgramError> {
    // We parse the Account struct using the Solana-provided Pack trait
    let vault_account = spl_token::state::Account::unpack(&vault.data.borrow())?;

    let vault_signer =
        Pubkey::create_program_address(&[&contract_key.to_bytes(), &[signer_nonce]], program_id)?;
    let is_valid = vault_account.owner == vault_signer
        && vault_account.amount == 0
        && vault_account.delegate.is_none()
        && vault_account.state == AccountState::Initialized
        && vault_account.close_authority.is_none();
    if !is_valid {
        return Err(TokenVestingError::InvalidVaultAccount.into());
    }
    Ok(())
}

If the provided vault account is invalid, we return the custom InvalidVaultAccount error. Let's define it in error.rs by modifying the TokenVestingError struct:

#[derive(Clone, Debug, Error, FromPrimitive)]
pub enum TokenVestingError {
    #[error("This account is already initialized")]
    AlreadyInitialized,
    #[error("Data type mismatch")]
    DataTypeMismatch,
    #[error("Wrong account owner")]
    WrongOwner,
    #[error("Account is uninitialized")]
    Uninitialized,
    #[error("The provided vault account is invalid")]
    InvalidVaultAccount,
}

Our project does not compile at this point, and we need to edit TokenVestingError's impl of the PrintProgramError trait in entrypoint.rs:

impl PrintProgramError for TokenVestingError {
    fn print<E>(&self)
    where
        E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
    {
        match self {
            TokenVestingError::AlreadyInitialized => {
                msg!("Error: This account is already initialized")
            }
            TokenVestingError::DataTypeMismatch => msg!("Error: Data type mismatch"),
            TokenVestingError::WrongOwner => msg!("Error: Wrong account owner"),
            TokenVestingError::Uninitialized => msg!("Error: Account is uninitialized"),
            TokenVestingError::InvalidVaultAccount => {
                msg!("Error: The provided vault account is invalid")
            }
        }
    }
}

It might seem a bit redundant to have the same error message appear twice in our project. However, doing it this way prevents our PrintProgramError implementation from having to use string formatting which is inefficient on-chain. The last thing we want is our error messages to be buried under a ComputationalBudgetExceeded error.

Finally we just have to insert a call to check_vault_account in our create instruction's logic:

check_vault_account(
    accounts.vault,
    program_id,
    *accounts.vesting_contract.key,
    signer_nonce,
)?;

Conclusion

If you've been following along, your create.rs file is in its final form and should look like this:

//! Create a new token vesting contract

use bonfida_utils::{checks::check_account_owner, WrappedPod};
use solana_program::{msg, program::invoke, program_pack::Pack};
use spl_token::state::AccountState;

use crate::{
    error::TokenVestingError,
    state::{
        self,
        vesting_contract::{VestingContract, VestingContractHeader, VestingSchedule},
    },
};

use {
    bonfida_utils::{
        checks::{check_account_key, check_signer},
        InstructionsAccount,
    },
    solana_program::{
        account_info::{next_account_info, AccountInfo},
        entrypoint::ProgramResult,
        program_error::ProgramError,
        pubkey::Pubkey,
    },
};

#[derive(WrappedPod)]
pub struct Params<'a> {
    signer_nonce: &'a u64,
    schedule: &'a [VestingSchedule],
}

#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
    /// SPL token program account
    pub spl_token_program: &'a T,

    /// The account which will store the [`VestingContract`] data structure
    #[cons(writable)]
    pub vesting_contract: &'a T,

    /// The contract's escrow vault
    #[cons(writable)]
    pub vault: &'a T,

    #[cons(writable)]
    /// The account currently holding the tokens to be vested
    pub source_tokens: &'a T,

    #[cons(signer)]
    /// The owner of the account currently holding the tokens to be vested
    pub source_tokens_owner: &'a T,

    /// The eventual recipient of the vested tokens
    pub recipient: &'a T,
}

impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
    pub fn parse(
        accounts: &'a [AccountInfo<'b>],
        program_id: &Pubkey,
    ) -> Result<Self, ProgramError> {
        let accounts_iter = &mut accounts.iter();
        let accounts = Accounts {
            spl_token_program: next_account_info(accounts_iter)?,
            vesting_contract: next_account_info(accounts_iter)?,
            vault: next_account_info(accounts_iter)?,
            source_tokens: next_account_info(accounts_iter)?,
            source_tokens_owner: next_account_info(accounts_iter)?,
            recipient: next_account_info(accounts_iter)?,
        };

        // Check keys
        check_account_key(accounts.spl_token_program, &spl_token::ID)?;

        // Check owners
        check_account_owner(accounts.vesting_contract, program_id)?;
        check_account_owner(accounts.vault, &spl_token::ID)?;

        // Check signer
        check_signer(accounts.source_tokens_owner)?;

        Ok(accounts)
    }
}

pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
    let accounts = Accounts::parse(accounts, program_id)?;

    let Params {
        signer_nonce,
        schedule,
    } = params;

    // We only want a one-byte signer nonce
    let signer_nonce = *signer_nonce as u8;

    let expected_vesting_contract_account_size =
        VestingContract::compute_allocation_size(schedule.len());

    if accounts.vesting_contract.data_len() != expected_vesting_contract_account_size {
        msg!("The vesting contract account is incorrectly sized for the supplied schedule!");
        return Err(ProgramError::InvalidArgument);
    }

    check_vault_account(
        accounts.vault,
        program_id,
        *accounts.vesting_contract.key,
        signer_nonce,
    )?;

    let mut vesting_contract_guard = accounts.vesting_contract.data.borrow_mut();

    VestingContract::initialize(&mut vesting_contract_guard)?;
    let vesting_contract =
        VestingContract::from_buffer(&mut vesting_contract_guard, state::Tag::VestingContract)?;

    *vesting_contract.header = VestingContractHeader {
        owner: *accounts.recipient.key,
        vault: *accounts.vault.key,
        current_schedule_index: 0,
        signer_nonce,
        _padding: [0; 7],
    };

    let mut total_amount = 0u64;
    for (schedule, slot) in schedule.iter().zip(vesting_contract.schedules.iter_mut()) {
        *slot = *schedule;
        total_amount = total_amount.checked_add(schedule.quantity).unwrap();
    }

    let instruction = spl_token::instruction::transfer(
        &spl_token::ID,
        accounts.source_tokens.key,
        accounts.vault.key,
        accounts.source_tokens_owner.key,
        &[],
        total_amount,
    )?;

    invoke(
        &instruction,
        &[
            accounts.spl_token_program.clone(),
            accounts.source_tokens.clone(),
            accounts.vault.clone(),
            accounts.source_tokens_owner.clone(),
        ],
    )?;

    Ok(())
}

fn check_vault_account(
    vault: &AccountInfo,
    program_id: &Pubkey,
    contract_key: Pubkey,
    signer_nonce: u8,
) -> Result<(), ProgramError> {
    let vault_account = spl_token::state::Account::unpack(&vault.data.borrow())?;

    let vault_signer =
        Pubkey::create_program_address(&[&contract_key.to_bytes(), &[signer_nonce]], program_id)?;
    let is_valid = vault_account.owner == vault_signer
        && vault_account.amount == 0
        && vault_account.delegate.is_none()
        && vault_account.state == AccountState::Initialized
        && vault_account.close_authority.is_none();
    if !is_valid {
        return Err(TokenVestingError::InvalidVaultAccount.into());
    }
    Ok(())
}

Writing an instruction: claim

The final instruction we need to write is claim. This instruction allows a vesting contract owner to claim their assets when those unvest. The design and implementation process will be mostly similar to the previous section. Therefore we'll go a bit faster here and slow down when there are interesting considerations to ponder.

Adding the skeleton for a new instruction

We begin by duplicating the create.rs file, renaming it to claim.rs, declaring the module in processor.rs and wiping our create.rs business logic. An alternative would have been to keep the example instruction and restart from there, but it doesn't make much of a difference.

We won't be needing parameters here : we'll simply extract as many tokens as possible from the vesting contract. The Params struct will be empty but we'll use the casting logic anyways. I have gone ahead and implemented the instruction. You can read through the annotated code:

//! Claim unvested tokens

use bonfida_utils::checks::{check_account_key, check_account_owner, check_signer};
use bytemuck::{Pod, Zeroable};
use solana_program::{clock::Clock, msg, program::invoke_signed, sysvar::Sysvar};

use crate::state::{self, vesting_contract::VestingContract};

use {
    bonfida_utils::InstructionsAccount,
    solana_program::{
        account_info::{next_account_info, AccountInfo},
        entrypoint::ProgramResult,
        program_error::ProgramError,
        pubkey::Pubkey,
    },
};

#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(C)]
pub struct Params {}

#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
    /// SPL token program account
    pub spl_token_program: &'a T,

    /// The account which will store the [`VestingContract`] data structure
    #[cons(writable)]
    pub vesting_contract: &'a T,

    /// The signing PDA which owns the vault
    pub vesting_contract_signer: &'a T,

    /// The contract's escrow vault
    #[cons(writable)]
    pub vault: &'a T,

    /// The token account to transfer the unvested assets to
    #[cons(writable)]
    pub destination_token_account: &'a T,

    /// The owner of the current vesting contract
    #[cons(signer)]
    pub owner: &'a T,
}

impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
    pub fn parse(
        accounts: &'a [AccountInfo<'b>],
        program_id: &Pubkey,
    ) -> Result<Self, ProgramError> {
        let accounts_iter = &mut accounts.iter();
        let accounts = Accounts {
            spl_token_program: next_account_info(accounts_iter)?,
            vesting_contract: next_account_info(accounts_iter)?,
            vesting_contract_signer: next_account_info(accounts_iter)?,
            vault: next_account_info(accounts_iter)?,
            destination_token_account: next_account_info(accounts_iter)?,
            owner: next_account_info(accounts_iter)?,
        };

        // Check keys
        check_account_key(accounts.spl_token_program, &spl_token::ID)?;

        // Check owners
        check_account_owner(accounts.vesting_contract, program_id)?;
        check_account_owner(accounts.vault, &spl_token::ID)?;
        check_account_owner(accounts.destination_token_account, &spl_token::ID)?;

        // Check signer
        check_signer(accounts.owner)?;

        Ok(accounts)
    }
}

pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], _params: &Params) -> ProgramResult {
    let accounts = Accounts::parse(accounts, program_id)?;

    // We begin by parsing the vesting contract account
    let mut vesting_contract_guard = accounts.vesting_contract.data.borrow_mut();
    let vesting_contract =
        VestingContract::from_buffer(&mut vesting_contract_guard, state::Tag::VestingContract)?;

    // We check that the specified owner actually owns this contract
    if &vesting_contract.header.owner != accounts.owner.key {
        msg!("Invalid vesting contract owner!");
        return Err(ProgramError::InvalidArgument);
    }

    // We also check that the vault is the correct one
    // Since our vesting contract signer is tied to just one vesting contract
    // This isn't strictly necessary and the call to spl_token would fail.
    // This is defense in depth. Also it makes for nicer error messages.
    if &vesting_contract.header.vault != accounts.vault.key {
        msg!("Invalid vault provided!");
        return Err(ProgramError::InvalidArgument);
    }

    // We derive and check that the provided contract signer is correct.
    // In the same way, this isn't strictly necessary.
    // The call to invoke_signed would fail if this wasn't the case.
    let contract_signer_key = Pubkey::create_program_address(
        &[
            &accounts.vesting_contract.key.to_bytes(),
            &[vesting_contract.header.signer_nonce as u8],
        ],
        program_id,
    )?;

    if &contract_signer_key != accounts.vesting_contract_signer.key {
        msg!("Invalid contract signer provided!");
        return Err(ProgramError::InvalidArgument);
    }

    // We get the current timestamp from the Clock sysvar
    let current_timestamp = Clock::get()?.unix_timestamp as u64;

    let mut total_amount_to_transfer: u64 = 0;

    // We saturate the vesting_contract.header.current_schedule_index variable in case we don't break
    // out of our loop. Not doing this would leave the contract empty but in a weird state
    let current_schedule_index = vesting_contract.header.current_schedule_index as usize;
    vesting_contract.header.current_schedule_index = u64::MAX;

    for (idx, s) in vesting_contract.schedules[current_schedule_index..]
        .iter_mut()
        .enumerate()
    {
        if s.unlock_timestamp > current_timestamp {
            // We update the current_schedule_index for the next call to claim
            // This prevents the same quantity from being unlocked twice
            vesting_contract.header.current_schedule_index = idx as u64;
            break;
        }

        total_amount_to_transfer = total_amount_to_transfer.checked_add(s.quantity).unwrap();
        // We zero out the schedule. This isn't strictly necessary as well since we
        // update the current_schedule_index. Defense in depth.
        s.quantity = 0;
    }

    let transfer_instruction = spl_token::instruction::transfer(
        &spl_token::ID,
        accounts.vault.key,
        accounts.destination_token_account.key,
        accounts.vesting_contract_signer.key,
        &[],
        total_amount_to_transfer,
    )?;

    invoke_signed(
        &transfer_instruction,
        &[
            accounts.spl_token_program.clone(),
            accounts.vault.clone(),
            accounts.destination_token_account.clone(),
            accounts.vesting_contract_signer.clone(),
        ],
        &[&[
            &accounts.vesting_contract.key.to_bytes(),
            &[vesting_contract.header.signer_nonce as u8],
        ]],
    )?;

    Ok(())
}

Once this is done, we need to add this instruction and its binding to the instruction.rs enum as well as processor.rs.

In instruction.rs, we add the Claim variant to the ProgramInstruction enum. We also add the following binding:

pub fn claim(accounts: claim::Accounts<Pubkey>, params: claim::Params) -> Instruction {
   accounts.get_instruction_cast(crate::ID, ProgramInstruction::Claim as u8, params)
}

In processor.rs, we add the following match variant:

   ProgramInstruction::Claim => {
   msg!("Instruction: Claim");
   let params = bytemuck::from_bytes(instruction_data);
   claim::process(program_id, accounts, params)?;
}

The bytemuck::from_bytes logic serves as an illustration for the syntax to use when dealing with simple parameters. You can omit it.

With this, the entire logic of our program is done! The next step is to add some basic integration tests.

Setting up integration testing

Let's take a look at how to write integration tests using the solana-program-test framework and bonfida-test-utils.

The project already contains a test template at tests/functional.rs. A functional test is a very simple integration test which tests basic program functionality. In real-world applications, we would require a battery of such tests to cover edge cases. For the sake of brevity and since this program is quite straightforward as it is, let's focus our efforts on a single test.

Test scenario

We want to test for a basic test scenario. Alice wants to vest some tokens for Bob. This means that Alice will create a vesting contract with Bob set as the recipient. The vesting schedule will be composed of three unlock phases.

To set up this scenario, we need to create a token with a given mint authority, and then mint some tokens for Alice. We can then have Alice create the vesting contract. Finally, Bob will attempt to unlock tokens at different simulated times and we will check that the unlocked amounts are valid.

Writing the test

Let's start by clearing the contents of the test_01 function. We can then set up the identities (keypairs) that we will be requiring. Using named indices instead of variables can make things neater.

use bonfida_test_utils::{ProgramTestContextExt, ProgramTestExt};
use solana_program::pubkey::Pubkey;
use token_vesting::{
    entrypoint::process_instruction,
    state::vesting_contract::{VestingContract, VestingSchedule},
};

use {
    solana_program_test::{processor, ProgramTest},
    solana_sdk::signer::{keypair::Keypair, Signer},
};

#[tokio::test]
async fn test_01() {
    // Create program and test environment
    const ALICE: usize = 0;
    const BOB: usize = 1;
    const MINT_AUTHORITY: usize = 2;

    // We'll need this later when writing and testing our schedules
    const SECONDS_IN_HOUR: u64 = 3600;

    let keypairs = [Keypair::new(), Keypair::new(), Keypair::new()];

    let mut program_test = ProgramTest::new(
        "token_vesting",
        token_vesting::ID,
        processor!(process_instruction),
    );
}

Then, we can initialize a new token mint with 6 decimals (this is mostly standard). The add_mint method comes from the ProgramTestExt trait included with bonfida-test-utils.

let (mint_key, _) = program_test.add_mint(None, 6, &keypairs[MINT_AUTHORITY].pubkey());

Once program environment setup is done, we can start the test runtime.

////
// Create test context
////
let mut prg_test_ctx = program_test.start_with_context().await;

We then need to initialize Alice and Bob's token accounts. We'll also mint some tokens into Alice's account, this time thanks to the ProgramTestContextExt trait. The initialize_token_accounts method will initialize associated token accounts tied to the given keys.

// Initialize Alice and Bob's token accounts:
let ata_keys = prg_test_ctx
    .initialize_token_accounts(
        mint_key,
        &keypairs.iter().map(|k| k.pubkey()).collect::<Vec<_>>(),
    )
    .await
    .unwrap();

// Alice gets 100 tokens
prg_test_ctx
    .mint_tokens(
        &keypairs[MINT_AUTHORITY],
        &mint_key,
        &ata_keys[ALICE],
        100_000_000, // This is 100 actual tokens since the token is defined with 6 decimals
    )
    .await
    .unwrap();

We can then define the vesting schedule.

// Alice will vest 16 tokens for Bob
// We first define the schedule we want

let now = prg_test_ctx.get_current_timestamp().await.unwrap() as u64;

let schedule = vec![
    VestingSchedule {
        unlock_timestamp: now + SECONDS_IN_HOUR,
        quantity: 10_000_000,
    },
    VestingSchedule {
        unlock_timestamp: now + 2 * SECONDS_IN_HOUR,
        quantity: 5_000_000,
    },
    VestingSchedule {
        unlock_timestamp: now + 3 * SECONDS_IN_HOUR,
        quantity: 1_000_000,
    },
];

In preparation for the token vesting contract initialization, we need to allocate the contract account. This is where VestingContract's compute_allocation_size method comes in handy.

let allocation_size = VestingContract::compute_allocation_size(schedule.len());
let vesting_contract = prg_test_ctx
    .initialize_new_account(allocation_size, token_vesting::ID)
    .await
    .unwrap();

We then need to initialize the vesting contract's token account. The token account will belong to the vesting contract's vault signer, which we need to derive first.

let (vault_signer, vault_signer_nonce) =
    Pubkey::find_program_address(&[&vesting_contract.to_bytes()], &token_vesting::ID);
let vault = prg_test_ctx
    .initialize_token_accounts(mint_key, &[vault_signer])
    .await
    .unwrap()[0];

We can finally create the vesting contract.

let ix = token_vesting::instruction::create(
    token_vesting::instruction::create::Accounts {
        spl_token_program: &spl_token::ID,
        vesting_contract: &vesting_contract,
        vault: &vault,
        source_tokens: &ata_keys[ALICE],
        source_tokens_owner: &keypairs[ALICE].pubkey(),
        recipient: &keypairs[BOB].pubkey(),
    },
    token_vesting::instruction::create::Params {
        signer_nonce: &(vault_signer_nonce as u64),
        schedule: &schedule,
    },
);

prg_test_ctx
    .sign_send_instructions(&[ix], &[&keypairs[ALICE]])
    .await
    .unwrap();

Once this is done, we can check that the tokens have been successfully debited from Alice's account.

let alice_token_account_balance = prg_test_ctx
    .get_token_account(ata_keys[ALICE])
    .await
    .unwrap()
    .amount;
assert_eq!(alice_token_account_balance, 84_000_000);

Already, we can run this test using cargo test-bpf and check that it successfully passes.

The next step is to make Bob claim the schedules one by one. We're going to be using the warp_to_timestamp method to simulate the passage of time in order to test the scheduling logic.

for v in schedule {
    // We fast-forward to the unlock
    let previous_balance = prg_test_ctx
        .get_token_account(ata_keys[BOB])
        .await
        .unwrap()
        .amount;
    prg_test_ctx
        .warp_to_timestamp(v.unlock_timestamp as i64)
        .await
        .unwrap();
    let ix = token_vesting::instruction::claim(
        token_vesting::instruction::claim::Accounts {
            spl_token_program: &spl_token::ID,
            vesting_contract: &vesting_contract,
            vesting_contract_signer: &vault_signer,
            vault: &vault,
            destination_token_account: &ata_keys[BOB],
            owner: &keypairs[BOB].pubkey(),
        },
        token_vesting::instruction::claim::Params {},
    );

    prg_test_ctx
        .sign_send_instructions(&[ix], &[&keypairs[BOB]])
        .await
        .unwrap();

    // We check that the tokens have been properly unvested
    let bob_token_account_balance = prg_test_ctx
        .get_token_account(ata_keys[BOB])
        .await
        .unwrap()
        .amount;
    assert_eq!(bob_token_account_balance - previous_balance, v.quantity);
}

We can run this test and check that it indeed passes.

Conclusion

With this, we have successfully tested our program's overall functionality! The next step would be to write another test in which Bob attempts to unlock the tokens at the wrong times. We could also add in tests in which the provided accounts for each instruction are actually incorrect. The idea is to think about attack vectors and then write integration tests for those attack vectors. If at anytime in the future, a change opens up a vulnerability, it should ideally be directly caught by your test suite.

Writing tests with good coverage is a very generic and hard problem to solve in software engineering, so it is definitely out of the scope of this tutorial.