Skip to content

Timelock

Timelock with Scheduler
#![no_std]
 
use soroban_sdk::{
    contract, contracterror, contractimpl, contracttype,
    panic_with_error, xdr::ToXdr, Address, Bytes, BytesN,
    Env, InvokeError, Symbol, TryFromVal, Val, Vec
};
 
 
const DONE_TIMESTAMP: u64 = 1;
pub const MAX_MIN_DELAY: u64 = 30 * 24 * 60 * 60; // 30 days
 
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Scheduler(BytesN<32>),
    MinDelay,
    Initialized,
}
 
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[contracttype]
#[repr(u8)]
enum OperationState {
    Unset = 1,
    Waiting = 2,
    Ready = 3,
    Executed = 4,
}
 
#[derive(Copy, Clone)]
#[contracterror]
#[repr(u32)]
pub enum TimeLockError {
    InvalidParams = 0,
    NotInitialized = 1,
    AlreadyInitialized = 2,
    AlreadyExists = 3,
    InsufficientDelay = 4,
    TimeNotReady = 5,
    PredecessorNotDone = 6,
    InvalidStatus = 8,
    NotPermitted = 9,
    ExecuteFailed = 10,
    InvalidFuncName = 11,
    DelayTooLong = 12,
}
 
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct CallExecutedEvent {
    pub opt_id: BytesN<32>,
    pub target: Address,
    pub fn_name: Symbol,
    pub data: Vec<Val>,
}
 
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct CallScheduledEvent {
    pub opt_id: BytesN<32>,
    pub target: Address,
    pub fn_name: Symbol,
    pub data: Vec<Val>,
    pub predecessor: BytesN<32>,
    pub delay: u64,
}
 
 
 
 
#[contract]
pub struct TimeLockContract;
 
#[contractimpl]
impl TimeLockContract {
    pub fn initialize(
        e: Env,
        min_delay: u64,
    ) {
        if min_delay > MAX_MIN_DELAY {
            panic_with_error!(e, TimeLockError::DelayTooLong);
        }
 
        if !e.storage().instance().has(&DataKey::Initialized) {
            e.storage().instance().set(&DataKey::Initialized, &true);
        } else {
            panic_with_error!(e, TimeLockError::AlreadyInitialized);
        }
 
        e.storage().instance().set(&DataKey::MinDelay, &min_delay);
    }
 
    pub fn schedule(
        e: Env,
        target: Address,
        fn_name: Symbol,
        data: Vec<Val>,
        salt: BytesN<32>,
        predecessor: Option<BytesN<32>>,
        delay: u64,
    ) -> BytesN<32> {
        if delay < e.storage().instance().get(&DataKey::MinDelay).unwrap() {
            panic_with_error!(e, TimeLockError::InsufficientDelay);
        }
 
        let operation_id = Self::hash_call(&e, &target, &fn_name, &data, &salt, &predecessor);
        Self::add_operation(&e, &operation_id, delay);
 
        let actual_predecessor = match predecessor {
            Some(predecessor) => predecessor,
            None => BytesN::from_array(&e, &[0_u8; 32]),
        };
 
        e.events().publish(
            (Symbol::new(&e, "CallScheduled"),),
            CallScheduledEvent {
                opt_id: operation_id.clone(),
                target,
                fn_name,
                data,
                predecessor: actual_predecessor,
                delay,
            },
        );
 
        operation_id
    }
 
    pub fn execute(
        e: Env,
        target: Address,
        fn_name: Symbol,
        data: Vec<Val>,
        salt: BytesN<32>,
        predecessor: Option<BytesN<32>>,
        is_native: bool,
    ) {
        let operation_id = Self::hash_call(&e, &target, &fn_name, &data, &salt, &predecessor);
        Self::check_execute(&e, &operation_id, &predecessor);
 
        if is_native {
            Self::exec_native(&e, &fn_name, &data);
        } else {
            Self::exec_external(&e, &target, &fn_name, &data);
        }
 
        e.storage()
            .persistent()
            .set(&DataKey::Scheduler(operation_id.clone()), &DONE_TIMESTAMP);
 
        e.events().publish(
            (Symbol::new(&e, "CallExecuted"),),
            CallExecutedEvent {
                opt_id: operation_id,
                target,
                fn_name,
                data,
            },
        );
    }
 
    pub fn cancel(e: Env, operation_id: BytesN<32>) {
        let state = Self::get_operation_state(&e, &operation_id);
        if state == OperationState::Ready || state == OperationState::Waiting {
            e.storage()
                .persistent()
                .remove(&DataKey::Scheduler(operation_id.clone()));
        } else {
            panic_with_error!(e, TimeLockError::InvalidStatus);
        }
 
        e.events().publish(
            (Symbol::new(&e, "OperationCancelled"),),
            operation_id,
        );
    }
 
    pub fn update_min_delay(e: Env, delay: u64) {
        e.storage().instance().set(&DataKey::MinDelay, &delay);
        e.events()
            .publish((Symbol::new(&e, "MinDelayUpdated"),), delay);
    }
 
    pub fn get_schedule_lock_time(e: Env, operation_id: BytesN<32>) -> u64 {
        let key = DataKey::Scheduler(operation_id.clone());
        if let Some(schedule) = e.storage().persistent().get::<DataKey, u64>(&key) {
            schedule
        } else {
            0_u64
        }
    }
 
    fn get_operation_state(e: &Env, operation_id: &BytesN<32>) -> OperationState {
        let ledger_time = e.ledger().timestamp();
        let lock_time = Self::get_schedule_lock_time(e.clone(), operation_id.clone());
        if lock_time == 0 {
            OperationState::Unset
        } else if lock_time == DONE_TIMESTAMP {
            OperationState::Executed
        } else if ledger_time < lock_time {
            OperationState::Waiting
        } else {
            OperationState::Ready
        }
    }
 
    fn add_operation(e: &Env, operation_id: &BytesN<32>, delay: u64) {
        let ledger_time = e.ledger().timestamp();
        if Self::get_operation_state(e, operation_id) != OperationState::Unset {
            panic_with_error!(e, TimeLockError::AlreadyExists);
        }
 
        let time = ledger_time + delay;
        e.storage()
            .persistent()
            .set(&DataKey::Scheduler(operation_id.clone()), &time);
    }
 
    fn check_execute(e: &Env, operation_id: &BytesN<32>, predecessor: &Option<BytesN<32>>) {
        if Self::get_operation_state(e, operation_id) != OperationState::Ready {
            panic_with_error!(e, TimeLockError::TimeNotReady);
        }
 
        if let Some(predecessor) = predecessor {
            if Self::get_operation_state(e, predecessor) != OperationState::Executed {
                panic_with_error!(e, TimeLockError::PredecessorNotDone);
            }
        }
    }
 
    fn exec_external(e: &Env, target: &Address, fn_name: &Symbol, data: &Vec<Val>) {
        let result = e.try_invoke_contract::<(), InvokeError>(target, fn_name, data.clone());
 
        match result {
            Ok(_) => {}
            Err(_) => {
                panic_with_error!(e, TimeLockError::ExecuteFailed);
            }
        }
    }
 
    fn exec_native(e: &Env, fn_name: &Symbol, data: &Vec<Val>) {
        let fn_name = fn_name.clone();
        if fn_name == Symbol::new(&e, "update_min_delay") {
            Self::update_min_delay_from_data(&e, data);
        } else {
            panic_with_error!(e, TimeLockError::InvalidFuncName);
        }
    }
 
    fn hash_call(
        e: &Env,
        target: &Address,
        fn_name: &Symbol,
        data: &Vec<Val>,
        salt: &BytesN<32>,
        predecessor: &Option<BytesN<32>>,
    ) -> BytesN<32> {
        let mut calldata = Bytes::new(e);
 
        // Clone the values where necessary to avoid moving out of borrowed values.
        calldata.append(&target.clone().to_xdr(e));
        calldata.append(&fn_name.clone().to_xdr(e));
        calldata.append(&data.clone().to_xdr(e)); // Clone `data` to satisfy ownership rules
 
        // Handle the predecessor value by cloning it if it exists.
        if let Some(predecessor) = predecessor {
            calldata.append(&predecessor.clone().to_xdr(e));
        }
 
        calldata.append(&salt.clone().to_xdr(e)); // Clone `salt` to satisfy ownership rules
 
        // Convert the result of sha256 to BytesN<32>
        e.crypto().sha256(&calldata).into()
    }
 
 
    fn update_min_delay_from_data(e: &Env, data: &Vec<Val>) {
        let delay = data.get(0);
        if let Some(delay) = delay {
            let p = u64::try_from_val(e, &delay);
            if let Ok(delay) = p {
                Self::update_min_delay(e.clone(), delay);
            } else {
                panic_with_error!(e, TimeLockError::InvalidParams);
            }
        } else {
            panic_with_error!(e, TimeLockError::InvalidParams);
        }
    }
}

Run in Playground

Loading playground...