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.