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, thevesting_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 thespl_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.