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.