Clock and Locked Coin Example

In the second fungible token example, we will introduce how to obtain time on-chain in Sui, and how to utilize that to implement a vesting mechanism for a coin.

Clock

Sui Framework has a native clock module that makes timestamps available in Move smart contracts.

The main method that you will need to access is the following:

public fun timestamp_ms(clock: &clock::Clock): u64

the timestamp_ms function returns the current system timestamp, as a running total of milliseconds since an arbitrary point in the past.

The clock object has a special reserved identifier, 0x6, that needs to be passed into function calls using it as one of the inputs.

Locked Coin

Now that we know how to access time on-chain through clock, implementing a vesting fungible token is relatively straight forward.

Locker Custom Type

locked_coin builds on top of the managed_coin implementation with the addition of one more custom type, Locker:

#![allow(unused)]
fn main() {
    /// Transferrable object for storing the vesting coins
    ///
    public struct Locker has key, store {
        id: UID,
        start_date: u64,
        final_date: u64,
        original_balance: u64,
        current_balance: Balance<LOCKED_COIN>

    }
}

Locker is a transferrable asset that encodes the information related to the vesting schedule and vesting status of tokens issued.

start_date and final_date are timestamps obtained from clock, marking the start and end of the vesting term.

original_balance is the initial balance issued into a Locker, balance is the current and remaining balance taking account any vested portion that's already withdrawn.

Minting

In the locked_mint method, we create and transfer a Locker with the specified amount of tokens and vesting scheduled encoded:

#![allow(unused)]
fn main() {
    /// Mints and transfers a locker object with the input amount of coins and specified vesting schedule
    /// 
    public fun locked_mint(treasury_cap: &mut TreasuryCap<LOCKED_COIN>, recipient: address, amount: u64, lock_up_duration: u64, clock: &Clock, ctx: &mut TxContext){
        
        let coin = coin::mint(treasury_cap, amount, ctx);
        let start_date = clock::timestamp_ms(clock);
        let final_date = start_date + lock_up_duration;

        transfer::public_transfer(Locker {
            id: object::new(ctx),
            start_date: start_date,
            final_date: final_date,
            original_balance: amount,
            current_balance: coin::into_balance(coin)
        }, recipient);
    }
}

Note how clock is used here to get the current timestamp.

Withdrawing

The withdraw_vested method contains the majority of the logic to compute the vested amounts:

#![allow(unused)]
fn main() {
    /// Withdraw the available vested amount assuming linear vesting
    ///
    public fun withdraw_vested(locker: &mut Locker, clock: &Clock, ctx: &mut TxContext){
        let total_duration = locker.final_date - locker.start_date;
        let elapsed_duration = clock::timestamp_ms(clock) - locker.start_date;
        let total_vested_amount = if (elapsed_duration > total_duration) {
            locker.original_balance
        } else {
            locker.original_balance * elapsed_duration / total_duration
        };
        let available_vested_amount = total_vested_amount - (locker.original_balance-balance::value(&locker.current_balance));
        transfer::public_transfer(coin::take(&mut locker.current_balance, available_vested_amount, ctx), sender(ctx))
    }
}

This example assumes a simple linear vesting schedule, but can be modified to accommodate a wide range of vesting logic and schedule.

Full Contract

You can find the full smart contract for our implementation of a locked_coin under the example_projects/locked_coin folder.