DEV Community

yuzurush
yuzurush

Posted on • Edited on

Soroban Contracts 101: Timelock

Hi there! Welcome to my fifteenth post of my series called "Soroban Contracts 101", where I'll be explaining the basics of Soroban contracts, such as data storage, authentication, custom types, and more. All the code that we're gonna explain throughout this series will mostly come from soroban-contracts-101 github repository.

In this post, i will explain Soroban Timelock example contract. This contract enables users to make deposits of a specified token amount and allows other individuals to claim it either before or after a designated time point.

The Contract Code

#![no_std]

use soroban_sdk::{contractimpl, contracttype, Address, BytesN, Env, Vec};

mod token {
    soroban_sdk::contractimport!(file = "../soroban_token_spec.wasm");
}

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Init,
    Balance,
}

#[derive(Clone)]
#[contracttype]
pub enum TimeBoundKind {
    Before,
    After,
}

#[derive(Clone)]
#[contracttype]
pub struct TimeBound {
    pub kind: TimeBoundKind,
    pub timestamp: u64,
}

#[derive(Clone)]
#[contracttype]
pub struct ClaimableBalance {
    pub token: BytesN<32>,
    pub amount: i128,
    pub claimants: Vec<Address>,
    pub time_bound: TimeBound,
}

pub struct ClaimableBalanceContract;

// The 'timelock' part: check that provided timestamp is before/after
// the current ledger timestamp.
fn check_time_bound(env: &Env, time_bound: &TimeBound) -> bool {
    let ledger_timestamp = env.ledger().timestamp();

    match time_bound.kind {
        TimeBoundKind::Before => ledger_timestamp <= time_bound.timestamp,
        TimeBoundKind::After => ledger_timestamp >= time_bound.timestamp,
    }
}

#[contractimpl]
impl ClaimableBalanceContract {
    pub fn deposit(
        env: Env,
        from: Address,
        token: BytesN<32>,
        amount: i128,
        claimants: Vec<Address>,
        time_bound: TimeBound,
    ) {
        if claimants.len() > 10 {
            panic!("too many claimants");
        }
        if is_initialized(&env) {
            panic!("contract has been already initialized");
        }
        // Make sure `from` address authorized the deposit call with all the
        // arguments.
        from.require_auth();

        // Transfer token from `from` to this contract address.
        token::Client::new(&env, &token).transfer(&from, &env.current_contract_address(), &amount);
        // Store all the necessary info to allow one of the claimants to claim it.
        env.storage().set(
            &DataKey::Balance,
            &ClaimableBalance {
                token,
                amount,
                time_bound,
                claimants,
            },
        );
        // Mark contract as initialized to prevent double-usage.
        // Note, that this is just one way to approach initialization - it may
        // be viable to allow one contract to manage several claimable balances.
        env.storage().set(&DataKey::Init, &());
    }

    pub fn claim(env: Env, claimant: Address) {
        // Make sure claimant has authorized this call, which ensures their
        // identity.
        claimant.require_auth();

        let claimable_balance: ClaimableBalance =
            env.storage().get_unchecked(&DataKey::Balance).unwrap();

        if !check_time_bound(&env, &claimable_balance.time_bound) {
            panic!("time predicate is not fulfilled");
        }

        let claimants = &claimable_balance.claimants;
        if !claimants.contains(&claimant) {
            panic!("claimant is not allowed to claim this balance");
        }

        // Transfer the stored amount of token to claimant after passing
        // all the checks.
        token::Client::new(&env, &claimable_balance.token).transfer(
            &env.current_contract_address(),
            &claimant,
            &claimable_balance.amount,
        );
        // Remove the balance entry to prevent any further claims.
        env.storage().remove(&DataKey::Balance);
    }
}

fn is_initialized(env: &Env) -> bool {
    env.storage().has(&DataKey::Init)
}

mod test;
Enter fullscreen mode Exit fullscreen mode

This contract implements a claimable balance contract, which allows depositing tokens into the contract under certain conditions (time bounds and a list of claimants), and then allowing the claimants to claim the tokens if the time conditions are met.

Here's a brief explanation of the ClaimableBalanceContract contract code:

  • Defines a ClaimableBalance struct to represent the claimable balance details (token, amount, time bound, list of claimants)
  • Defines a TimeBound enum and struct to represent time bound options (before/after a timestamp)
  • It implements a ClaimableBalanceContract with:

deposit - Allows depositing tokens under given conditions. Checks that the number of claimants is within limits, checks that the contract is not already initialized, requires the depositor to authenticate, transfers the tokens to the contract, and stores the claimable balance details.
claim - Allows a claimant to claim the tokens if the time bound is fulfilled. Requires authentication, checks the time bound, checks that the claimant is in the claimants list, and transfers the tokens to the claimant.

  • is_initialized - A helper function to check if the contract is initialized (has a non-empty Init storage entry)

Contract Usage

Here's the usage flow for the ClaimableBalanceContract contract:

  1. The depositor calls deposit to initialize a claimable balance, providing the token address, amount, list of claimants, and time bound conditions.
  2. Once the time bound conditions are met, one of the claimants calls claim to claim the tokens.

Conclusion

Overall, the contract allows depositing tokens under time-based conditions and a list of claimants, and then when claimants claim the deposit,the contract will distributing the tokens to the claimants once the time conditions are met. This can be useful for conditional or time-locked distributions of funds.Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.

Top comments (0)