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
    }
}