Skip to content

Vesting Contract

Soroban Vesting Contract
#![no_std]
use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, Env};
 
#[contract]
pub struct VestingContract;
 
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
    pub token: Address,
    pub beneficiary: Address,
    pub admin: Address,
    pub start_time: u64,
    pub end_time: u64,
    pub locked: i128,
    pub paid_out: i128,
}
 
impl State {
    pub fn new(
        token: Address,
        beneficiary: Address,
        admin: Address,
        start_time: u64,
        end_time: u64,
    ) -> State {
        State {
            token,
            beneficiary,
            admin,
            start_time,
            end_time,
            locked: 0,
            paid_out: 0,
        }
    }
}
 
#[contracttype]
pub enum DataKey {
    NextId,
    State(u64),
}
 
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum VestError {
    InvalidDuration = 1,
    ArithmeticError = 2,
    InvalidAmount = 3,
}
 
#[contractimpl]
impl VestingContract {
    pub fn new_vesting(
        env: Env,
        token: Address,
        beneficiary: Address,
        start_time: u64,
        duration: u64,
        admin: Address,
    ) -> Result<u64, VestError> {
        if duration < 1 {
            return Err(VestError::InvalidDuration);
        }
 
        let end_time = start_time
            .checked_add(duration)
            .ok_or(VestError::InvalidDuration)?;
 
        let id: u64 = env
            .storage()
            .persistent()
            .get(&DataKey::NextId)
            .unwrap_or_default();
        let state = State::new(token, beneficiary, admin, start_time, end_time);
 
        env.storage().persistent().set(&DataKey::NextId, &(id + 1));
        VestingContract::save_state(env, id, &state);
 
        Ok(id)
    }
 
    fn save_state(env: Env, id: u64, state: &State) {
        env.storage().persistent().set(&DataKey::State(id), state);
    }
 
    fn get_state(env: &Env, id: u64) -> State {
        env.storage().persistent().get(&DataKey::State(id)).unwrap()
    }
 
    fn time(env: &Env, state: &State) -> u64 {
        let now = env.ledger().timestamp();
        if now <= state.start_time {
            return 0;
        }
        if now >= state.end_time {
            return state.end_time - state.start_time;
        }
        now - state.start_time
    }
 
     fn retrievable_balance_internal(env: &Env, state: &State) -> Result<i128, VestError> {
        let now = VestingContract::time(env, state);
        if now == 0 {
            return Ok(0);
        }
 
        let duration: i128 = (state.end_time - state.start_time).into();
 
        //total is the amount the amount the beneficiary is owed at this time if they never cashed out.
        let total = state
            .locked
            .checked_mul(now.into())
            .and_then(|res| res.checked_div(duration))
            .ok_or(VestError::ArithmeticError)?;state
            .locked
            .checked_mul(now.into())
            .and_then(|res| res.checked_div(duration))
            .ok_or(VestError::ArithmeticError)?;
 
        //Subtract from total the amount that the beneficiary has already cashed
        //out to obtain how much they're owed.
        Ok(total
            .checked_sub(state.paid_out)
            .ok_or(VestError::ArithmeticError)?)
    }
 
    pub fn retrievable_balance(env: Env, id: u64) -> Result<i128, VestError> {
        VestingContract::retrievable_balance_internal(&env, &VestingContract::get_state(&env, id))
    }
 
    pub fn add_vest(
        env: Env,
        id: u64,
        token: Address,
        from: Address,
        amount: i128,
    ) -> Result<i128, VestError> {
        let mut state = VestingContract::get_state(&env, id);
        if token != state.token {
            panic!("token doesn't match!");
        }
 
        state.admin.require_auth();
        state.locked = state
            .locked
            .checked_add(amount)
            .ok_or(VestError::ArithmeticError)?;
 
        from.require_auth();
        token::Client::new(&env, &token).transfer(&from, &env.current_contract_address(), &amount);
 
        VestingContract::save_state(env, id, &state);
 
        Ok(state.locked)
    }
 
    pub fn pay_out(env: Env, id: u64) -> Result<i128, VestError> {
        let mut state = VestingContract::get_state(&env, id);
 
        let available = VestingContract::retrievable_balance_internal(&env, &state)?;
        if available == 0 {
            return Ok(0);
        }
 
        state.paid_out = state
            .paid_out
            .checked_add(available)
            .ok_or(VestError::ArithmeticError)?;
        token::Client::new(&env, &state.token).transfer(
            &env.current_contract_address(),
            &state.beneficiary,
            &available,
        );
 
        VestingContract::save_state(env, id, &state);
 
        Ok(available)
    }
}

Explanation

#![no_std] This attribute prevents linking to the standard library, making the code lighter and more efficient for Soroban contracts. It's big so we save on size.

use soroban_sdk::{contract, contractimpl, Env, log} Imports stuffs from the Soroban SDK. Env is basic Soroban type, we need it because we can't use the Rust standard library.

pub Marks a function as external, meaning it can be invoked outside of the context of the contract code itself.

enum This functionality in Rust allows one to define a type with a fixed set of values. It enables pattern matching, discriminant unions, as well as option and return types.

struct This is a user-defined data type that groups related data fields under a single name.

new_vesting This function creates a new vesting contract, specifying the token, beneficiary, start time, duration, and administrator.

save_state This function saves the provided State object to the contract's storage under the specified ID key.

`get_state: This function retrieves the State object for a given vesting instance ID from storage. time: This function calculates the elapsed vesting time based on the current timestamp and the vesting start time.

retrievable_balance_internal This internal function calculates the amount of tokens currently available for the beneficiary to withdraw, considering elapsed vesting time and already withdrawn amount.

retrievable_balance This function is a public wrapper for retrievable_balance_internal. It retrieves the state for a given ID and then calls the internal function to calculate the retrievable balance.

add_vest This adds more tokens to an existing vesting contract. Only the administrator can call this function.

pay_out This allows the beneficiary to claim the vested tokens that are currently available.

Run in Playground

Loading playground...