Timelocks

Explanation: What is a Timelock Contract, How Does It Work, What does it do?

Timelock Smart Contract Mechanism:

A timelock smart contract serves as an effective mechanism to introduce a delay system for executing function calls on other smart contracts. This unique contract structure establishes a predefined minimum time delay requirement before a scheduled operation can proceed.

Functionality of a Timelock Contract:

While the Timelock contract does not fall strictly under the category of MultiSig/Voting Contracts, it closely adheres to the fundamental principles outlined in CW3-spec compliant contracts. Rather than facilitating immediate execution, this contract allows addresses to propose or schedule operations, which subsequently undergo a specified delay period before final execution can take place.

Timelock Contract Instantiation:

Initial Configuration

Upon the deployment of the Timelock contract, the following parameters can be configured:

Minimum Time Delay: Set a predefined time delay before operations can be executed.

Administrators and Proposers: Assign addresses to fulfill these roles. Operations can only be scheduled by proposers if their execution time exceeds the set delay.

Administrators

Administrators are responsible for the initial setup and ensuring compatibility with potential target contracts. In the absence of specified addresses, the contract initiator automatically assumes the role of administrator. Once the configuration is complete, administrators have the authority to freeze the Timelock, rendering it immutable. This action is irreversible and can render the contract non-modifiable.

Proposers

Proposers are tasked with scheduling operations to be executed after the specified delay. It's crucial to ensure that the Timelock contract possesses the necessary permissions on the target contracts. Additionally, propellers should specify executor addresses responsible for the final execution of operations on the target contract. In cases where no executor is specified, any address can carry out the operation once the scheduled time arrives.

It's important to note that scheduling operations with the Timelock does not guarantee their execution. Proposers retain the option to cancel scheduled operations before they are executed. Therefore, the selection of responsible proposers holds significant importance in this context.

InstantiateMsg

pub struct InstantiateMsg {
  pub admins: Option<Vec<String>>,
  pub proposers: Vec<String>,
  pub min_delay: Duration,
}
Query
pub enum QueryMsg {
  GetOperationStatus {
    operation_id: Uint64,
  },

  GetExecutionTime {
    operation_id: Uint64,
  },

  GetAdmins {},
  GetOperations {
    start_after: Option<u64>,
    limit: Option<u32>,
  },

  GetMinDelay {},

  GetProposers {},

  GetExecutors {
    operation_id: Uint64,
  },
}

ExecuteMsg

pub enum ExecuteMsg {
  Schedule {
    target_address: String,
    data: Binary,
    title: String,
    description: String,
    execution_time: Scheduled,
    executors: Option<Vec<String>>,
  },

  Cancel {
    operation_id: Uint64,
  },

  Execute {
    operation_id: Uint64,
  },

  RevokeAdmin {
    admin_address: String,
  },

  AddProposer {
    proposer_address: String,
  },

  RemoveProposer {
    proposer_address: String,
  },

  UpdateMinDelay {
    new_delay: Duration,
  },

  Freeze {},
}

Example

To generate a CosmWasm timelocks contract, you can prepare the following files: lib.rs, contract.rs, msg.rs, error.rs, and state.rs.

lib.rs

pub mod contract;
mod error;
pub mod helpers;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;

contract.rs

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::WasmMsg::Execute;
use cosmwasm_std::{
    to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError,
    StdResult, Uint64,
};
use cw2::set_contract_version;
use cw_storage_plus::Bound;
use cw_utils::{Duration, Scheduled};
use std::ops::Add;

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, OperationListResponse, QueryMsg};
use crate::state::{Operation, OperationStatus, Timelock, CONFIG, OPERATION_LIST, OPERATION_SEQ};

// version info for migration info

const CONTRACT_NAME: &str = "crates.io:timelock";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    let mut admins = vec![];
    match msg.admins {
        None => {
            admins.push(info.sender.clone());
        }
        Some(admin_list) => {
            for admin in admin_list {
                admins.push(deps.api.addr_validate(&admin)?);
            }
        }
    }
    admins.push(env.contract.address);

    let mut proposers = vec![];
    for proposer in msg.proposers {
        proposers.push(deps.api.addr_validate(&proposer)?);
    }

    let timelock = Timelock {
        min_time_delay: msg.min_delay,
        proposers,
        admins,
        frozen: false,
    };

    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    OPERATION_SEQ.save(deps.storage, &Uint64::zero())?;
    CONFIG.save(deps.storage, &timelock)?;

    Ok(Response::new()
        .add_attribute("Method: ", "instantiate")
        .add_attribute("Admin: ", info.sender)
        .add_attribute(
            "Proposers: ",
            timelock
                .proposers
                .into_iter()
                .map(|item| item.to_string())
                .collect::<String>(),
        )
        .add_attribute("minTimeDelay: ", timelock.min_time_delay.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Schedule {
            target_address,
            data,
            title,
            description,
            execution_time,
            executors,
        } => execute_schedule(
            deps,
            _env,
            info,
            target_address,
            data,
            title,
            description,
            execution_time,
            executors,
        ),
        ExecuteMsg::Execute { operation_id } => execute_execute(deps, _env, info, operation_id),

        ExecuteMsg::Cancel { operation_id } => execute_cancel(deps, _env, info, operation_id),

        ExecuteMsg::RevokeAdmin { admin_address } => {
            execute_revoke_admin(deps, _env, info, admin_address)
        }

        ExecuteMsg::AddProposer { proposer_address } => {
            execute_add_proposer(deps, _env, info, proposer_address)
        }

        ExecuteMsg::RemoveProposer { proposer_address } => {
            execute_remove_proposer(deps, _env, info, proposer_address)
        }

        ExecuteMsg::UpdateMinDelay { new_delay } => {
            execute_update_min_delay(deps, _env, info, new_delay)
        }

        ExecuteMsg::Freeze {} => execute_freeze(deps, _env, info),
    }
}

/*eslint too-many-arguments-threshold:9 */
#[allow(clippy::too_many_arguments)]

pub fn execute_schedule(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    target_address: String,
    data: Binary,
    title: String,
    description: String,
    execution_time: Scheduled,
    executor_list: Option<Vec<String>>,
) -> Result<Response, ContractError> {
    let sender = deps.api.addr_validate(&info.sender.to_string())?;
    let target = deps.api.addr_validate(&target_address)?;
    let timelock = CONFIG.load(deps.storage)?;
    if !(timelock.proposers.contains(&sender)) {
        return Err(ContractError::Unauthorized {});
    }
    if Scheduled::AtTime(env.block.time).add(timelock.min_time_delay)? > execution_time {
        return Err(ContractError::MinDelayNotSatisfied {});
    }

    let id = OPERATION_SEQ.update::<_, StdError>(deps.storage, |id| Ok(id.add(Uint64::new(1))))?;

    let mut executors = None;
    match executor_list {
        None => {}
        Some(list) => {
            let mut checked_executors = vec![];
            for executor in list {
                checked_executors.push(deps.api.addr_validate(&executor)?);
            }
            executors = Option::from(checked_executors);
        }
    }

    let new_operation = Operation {
        id,
        status: OperationStatus::Pending,
        proposer: sender,
        executors,
        execution_time,
        target,
        data,
        title,
        description,
    };

    OPERATION_LIST.save(deps.storage, id.u64(), &new_operation)?;

    Ok(Response::new()
        .add_attribute("Schedule ", "success")
        .add_attribute("Operation ID: ", id)
        .add_attribute("Proposer: ", new_operation.proposer)
        .add_attribute("Target Address: ", new_operation.target.to_string())
        .add_attribute("Execution Time: ", new_operation.execution_time.to_string()))
}

pub fn execute_execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    operation_id: Uint64,
) -> Result<Response, ContractError> {
    let mut operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?;

    //is delay ended
    if !operation.execution_time.is_triggered(&env.block) {
        return Err(ContractError::Unexpired {});
    }

    //has executer list if so sender is in it
    if operation.executors.is_some()
        && !operation
            .executors
            .clone()
            .map(|c| c.contains(&info.sender))
            .unwrap()
    {
        return Err(ContractError::Unauthorized {});
    }

    if operation.status == OperationStatus::Done {
        return Err(ContractError::Executed {});
    }

    //change operation status
    operation.status = OperationStatus::Done;
    OPERATION_LIST.save(deps.storage, operation_id.u64(), &operation)?;

    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(Execute {
            contract_addr: operation.target.to_string(),
            msg: operation.data,
            funds: vec![],
        }))
        .add_attribute("executor", &info.sender.to_string()))
}

pub fn execute_cancel(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    operation_id: Uint64,
) -> Result<Response, ContractError> {
    let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?;

    if operation.status == OperationStatus::Done {
        return Err(ContractError::NotDeletable {});
    }

    if operation.proposer != info.sender {
        return Err(ContractError::Unauthorized {});
    }

    OPERATION_LIST.remove(deps.storage, operation_id.u64());

    Ok(Response::new()
        .add_attribute("Method", "cancel")
        .add_attribute("sender", &info.sender.to_string())
        .add_attribute("operation_id", operation_id.to_string())
        .add_attribute("Result", "Success"))
}

pub fn execute_revoke_admin(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    admin_address: String,
) -> Result<Response, ContractError> {
    let mut timelock = CONFIG.load(deps.storage)?;
    if timelock.frozen {
        return Err(ContractError::TimelockFrozen {});
    }
    if !timelock.admins.contains(&info.sender) {
        return Err(ContractError::Unauthorized {});
    }

    let admin_address = deps.api.addr_validate(&admin_address)?;

    let index = timelock
        .admins
        .iter()
        .position(|x| *x == admin_address.clone())
        .ok_or(ContractError::NotFound {
            address: admin_address.clone().to_string(),
        })?;

    timelock.admins.remove(index);
    CONFIG.save(deps.storage, &timelock)?;
    Ok(Response::new()
        .add_attribute("Method", "revoke admin")
        .add_attribute("sender", &info.sender)
        .add_attribute("Admin to revoke", admin_address)
        .add_attribute("Result", "Success"))
}

pub fn execute_add_proposer(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    proposer_address: String,
) -> Result<Response, ContractError> {
    let mut timelock = CONFIG.load(deps.storage)?;

    if timelock.frozen {
        return Err(ContractError::TimelockFrozen {});
    }

    if !timelock.admins.contains(&info.sender) {
        return Err(ContractError::Unauthorized {});
    }

    let proposer_address = deps.api.addr_validate(&proposer_address)?;

    //is in proposers list
    if timelock.proposers.contains(&proposer_address) {
        return Err(ContractError::AlreadyContainsProposerAddress {});
    }

    timelock.proposers.push(proposer_address);
    CONFIG.save(deps.storage, &timelock)?;
    Ok(Response::new()
        .add_attribute("Method", "add_proposer")
        .add_attribute("sender", &info.sender)
        .add_attribute("Result", "Success"))
}

pub fn execute_remove_proposer(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    proposer_address: String,
) -> Result<Response, ContractError> {
    let mut timelock = CONFIG.load(deps.storage)?;

    if timelock.frozen {
        return Err(ContractError::TimelockFrozen {});
    }

    if !timelock.admins.contains(&info.sender) {
        return Err(ContractError::Unauthorized {});
    }

    let proposer_address = deps.api.addr_validate(&proposer_address)?;
    //is in proposers
    let index = timelock
        .proposers
        .iter()
        .position(|x| *x == proposer_address.clone())
        .ok_or(ContractError::NotFound {
            address: proposer_address.clone().to_string(),
        })?;

    timelock.proposers.remove(index);
    CONFIG.save(deps.storage, &timelock)?;
    Ok(Response::new()
        .add_attribute("Method", "remove_proposer")
        .add_attribute("sender", &info.sender)
        .add_attribute("Result", "Success"))
}

pub fn execute_update_min_delay(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    new_delay: Duration,
) -> Result<Response, ContractError> {
    let mut timelock = CONFIG.load(deps.storage)?;

    if timelock.frozen {
        return Err(ContractError::TimelockFrozen {});
    }

    if !timelock.admins.contains(&info.sender) {
        return Err(ContractError::Unauthorized {});
    }
    timelock.min_time_delay = new_delay;
    CONFIG.save(deps.storage, &timelock)?;
    Ok(Response::new()
        .add_attribute("Method", "Update Min Delay")
        .add_attribute("Sender", &info.sender.to_string())
        .add_attribute("New Min Delay", timelock.min_time_delay.to_string())
        .add_attribute("Result", "Success"))
}

pub fn execute_freeze(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    let mut timelock = CONFIG.load(deps.storage)?;

    if timelock.frozen {
        return Err(ContractError::TimelockFrozen {});
    }

    if !timelock.admins.contains(&info.sender) {
        return Err(ContractError::Unauthorized {});
    }

    timelock.frozen = true;

    CONFIG.save(deps.storage, &timelock)?;

    Ok(Response::new()
        .add_attribute("Method", "freeze")
        .add_attribute("sender", &info.sender)
        .add_attribute("Result", "Success"))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetOperationStatus { operation_id } => {
            to_binary(&query_get_operation_status(deps, operation_id)?)
        }
        QueryMsg::GetExecutionTime { operation_id } => {
            to_binary(&query_get_execution_time(deps, operation_id)?)
        }
        QueryMsg::GetAdmins {} => to_binary(&query_get_admins(deps)?),
        QueryMsg::GetOperations { start_after, limit } => {
            to_binary(&query_get_operations(deps, start_after, limit)?)
        }
        QueryMsg::GetMinDelay {} => to_binary(&query_get_min_delay(deps)?),
        QueryMsg::GetProposers {} => to_binary(&query_get_proposers(deps)?),
        QueryMsg::GetExecutors { operation_id } => {
            to_binary(&query_get_executors(deps, operation_id)?)
        }
    }
}

pub fn query_get_operation_status(deps: Deps, operation_id: Uint64) -> StdResult<OperationStatus> {
    let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?;
    Ok(operation.status)
}

pub fn query_get_execution_time(deps: Deps, operation_id: Uint64) -> StdResult<String> {
    let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?;
    Ok(operation.execution_time.to_string())
}

pub fn query_get_admins(deps: Deps) -> StdResult<Vec<Addr>> {
    let timelock = CONFIG.load(deps.storage)?;
    Ok(timelock.admins)
}

// settings for pagination
const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;

pub fn query_get_operations(
    deps: Deps,
    start_after: Option<u64>,
    limit: Option<u32>,
) -> StdResult<OperationListResponse> {
    let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
    let start = start_after.map(Bound::exclusive);
    let operations: StdResult<Vec<_>> = OPERATION_LIST
        .range(deps.storage, start, None, Order::Ascending)
        .take(limit)
        .collect();

    let res = OperationListResponse {
        operationList: operations?.into_iter().map(|l| l.1.into()).collect(),
    };
    Ok(res)
}

pub fn query_get_min_delay(deps: Deps) -> StdResult<String> {
    let timelock = CONFIG.load(deps.storage)?;
    Ok(timelock.min_time_delay.to_string())
}

pub fn query_get_proposers(deps: Deps) -> StdResult<Vec<Addr>> {
    let timelock = CONFIG.load(deps.storage)?;
    Ok(timelock.proposers)
}

pub fn query_get_executors(deps: Deps, operation_id: Uint64) -> StdResult<Vec<Addr>> {
    let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?;
    Ok(operation.executors.unwrap_or_default())
}

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::Timestamp;
    use cw_utils::Scheduled;

    #[test]

    fn test_no_executers() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::Some(vec!["owner".to_string(), "new_one".to_string()]),
            proposers: vec!["prop1".to_string(), "prop2".to_string()],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);
        let description = "test desc".to_string();
        let title = "Title Example ".to_string();
        // instantiate
        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        let data = to_binary(&"data").unwrap();
        // try Schedule() with sender "creator"
        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(10)),
            Option::None,
        )
        .unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});

        //change sender to prop1
        let info = mock_info("prop1", &[]);
        //try Schedule() sender "prop1" execution_time < env.block.time
        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(1)),
            Option::None,
        )
        .unwrap_err();
        assert_eq!(res, ContractError::MinDelayNotSatisfied {});

        //Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time
        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(120)),
            Option::None,
        )
        .unwrap();
        println!("{:?}", res);

        let res = query_get_execution_time(deps.as_ref(), Uint64::new(1));
        println!("{:?}, {}", res, env.block.time);

        //try Execute() sender "prop1" execution_time > env.block.time
        let res =
            execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err();
        assert_eq!(res, ContractError::Unexpired {});

        //time pass
        env.block.time = Timestamp::from_seconds(120);
        //try Execute() sender "prop1" execution_time <= env.block.time executors "none"
        let res =
            execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap();
        println!("{:?}", res);
    }

    #[test]

    fn test_with_executors() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::Some(vec!["owner".to_string(), "newone".to_string()]),
            proposers: vec!["prop1".to_string(), "prop2".to_string()],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);
        let title = "Title Example ".to_string();

        // instantiate

        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        let data = to_binary(&"data").unwrap();
        let description = "test desc".to_string();
        //change sender to prop1
        let info = mock_info("prop1", &[]);

        //Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time

        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(120)),
            Option::Some(vec!["exec1".to_string(), "exec2".to_string()]),
        )
        .unwrap();
        println!("{:?}", res);

        let res =
            query_get_operations(deps.as_ref(), Option::Some(0u64), Option::Some(1u32)).unwrap();
        println!("{:?}", res);
        //time pass
        env.block.time = Timestamp::from_seconds(120);

        //try Execute() sender "prop1" execution_time <= env.block.time executors "exec1, exec2"
        let res =
            execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});

        let info = mock_info("exec1", &[]);
        //Execute() sender "exec1" execution_time <= env.block.time executors "exec1, exec2"
        let res =
            execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap();
        println!("{:?}", res);
    }


    #[test]

    fn test_cancel() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::Some(vec!["owner".to_string(), "newone".to_string()]),
            proposers: vec!["prop1".to_string(), "prop2".to_string()],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);
        let title = "Title Example ".to_string();

        // instantiate
        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        let data = to_binary(&"data").unwrap();
        let description = "test desc".to_string();

 //change sender to prop1
        let info = mock_info("prop1", &[]);

//Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time
        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(120)),
            Option::None,
        )
        .unwrap();
        println!("{:?}", res);

        //time pass
        env.block.time = Timestamp::from_seconds(120);

        //Execute() sender "prop1" executors ""
        let res =
            execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap();
        println!("{:?}", res);

        //try Cancel() sender "prop1" operation_id "1" status "OperationStatus::Done"

        let res =
            execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err();
        assert_eq!(res, ContractError::NotDeletable {});

        //Schedule() sender "prop1"

        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(140)),
            Option::None,
        )
        .unwrap();
        println!("{:?}", res);

        //Cancel() sender "prop1" operation_id "2" status "OperationStatus::Pending"
        let res = execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(2)).unwrap();
        println!("{:?}", res);

        //try Cancel() sender "nobody" operation_id "2" admin "creator" proposers "prop1, prop2"
        let res =
            execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(2)).unwrap_err();
        println!("{:?}", res);


        //Schedule() sender "prop1"
        let res = execute_schedule(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "target".to_string(),
            data.clone(),
            title.clone(),
            description.clone(),
            Scheduled::AtTime(Timestamp::from_seconds(140)),
            Option::None,
        )
        .unwrap();
        println!("{:?}", res);

        let info = mock_info("nobody", &[]);

        //try Cancel() sender "nobody" operation_id "3" admin "creator" proposers "prop1, prop2"

        let res =
            execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(3)).unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});
    }

    #[test]

    fn test_add_remove_proposer() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::None,
            proposers: vec![],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);

        // instantiate

        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        //try remove_proposer sender "creator" proposer_address "prop1" proposers ""

        let res = execute_remove_proposer(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "prop1".to_string(),
        )
        .unwrap_err();
        assert_eq!(
            res,
            ContractError::NotFound {
                address: "prop1".to_string()
            }
        );
        let info = mock_info("no_admin", &[]);

        //try remove_proposer sender "no_admin" proposer_address "prop1" proposers ""

        let res = execute_remove_proposer(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "prop1".to_string(),
        )
        .unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});

        //try add_proposer sender "no_admin" proposer_address "prop1" proposers ""

        let res = execute_add_proposer(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "prop1".to_string(),
        )
        .unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});
        let info = mock_info("creator", &[]);

        //add_proposer sender "creator" proposer_address "prop1" proposers ""

        let res = execute_add_proposer(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "prop1".to_string(),
        )
        .unwrap();
        println!("{:?}", res);

        //remove_proposer sender "no_admin" proposer_address "prop1" proposers "prop1"

        let res = execute_remove_proposer(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "prop1".to_string(),
        )
        .unwrap();
        println!("{:?}", res);
    }

    #[test]

    fn test_update_min_delay() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::None,
            proposers: vec![],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);

        // instantiate

        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        //update_min_delay() sender "creator"

        let res = execute_update_min_delay(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            Duration::Time(100),
        )
        .unwrap();
        println!("{:?}", res);

        let info = mock_info("no_admin", &[]);
        //try update_min_delay() sender "no_admin"
        let res = execute_update_min_delay(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            Duration::Time(100),
        )
        .unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});
    }

    #[test]

    fn test_revoke_admin() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.time = Timestamp::from_seconds(100);
        let msg = InstantiateMsg {
            admins: Option::None,
            proposers: vec![],
            min_delay: Duration::Time(10),
        };
        let info = mock_info("creator", &[]);

        // instantiate

        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
        println!("{:?}", res);

        //try revoke_admin() sender "creator" admin_address "not_in_it" admin "creator"

        let res = execute_revoke_admin(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "not_in_it".to_string(),
        )
        .unwrap_err();
        assert_eq!(
            res,
            ContractError::NotFound {
                address: "not_in_it".to_string()
            }
        );

        //revoke_admin() sender "creator" admin_address "creator" admin "creator"

        let res = execute_revoke_admin(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "creator".to_string(),
        )
        .unwrap();
        println!("{:?}", res);

        //try revoke_admin() sender "creator" admin_address "creator" admin ""

        let res = execute_revoke_admin(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            "creator".to_string(),
        )
        .unwrap_err();
        assert_eq!(res, ContractError::Unauthorized {});
    }
}

msg.rs

use crate::state::{Operation, OperationStatus};
use cosmwasm_std::{Addr, Binary, Uint64};
use cw_utils::{Duration, Scheduled};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub admins: Option<Vec<String>>,
    pub proposers: Vec<String>,
    pub min_delay: Duration,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Schedule {
        target_address: String,
        data: Binary,
        title: String,
        description: String,
        execution_time: Scheduled,
        executors: Option<Vec<String>>,
    },

    Cancel {
        operation_id: Uint64,
    },

    Execute {
        operation_id: Uint64,
    },

    RevokeAdmin {
        admin_address: String,
    },

    AddProposer {
        proposer_address: String,
    },

    RemoveProposer {
        proposer_address: String,
    },

    UpdateMinDelay {
        new_delay: Duration,
    },

    Freeze {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetOperationStatus {
        operation_id: Uint64,
    },

    GetExecutionTime {
        operation_id: Uint64,
    },

    GetAdmins {},
    GetOperations {
        start_after: Option<u64>,
        limit: Option<u32>,
    },

    GetMinDelay {},
    GetProposers {},
    GetExecutors {
        operation_id: Uint64,
    },

}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OperationResponse {
    pub id: Uint64,
    pub status: OperationStatus,
    pub proposer: Addr,
    pub executors: Option<Vec<Addr>>,
    pub execution_time: Scheduled,
    pub target: Addr,
    pub data: Binary,
    pub title: String,
    pub description: String,
}

//impl Into<OperationResponse> for Operation changed to from due to lint warning

impl From<Operation> for OperationResponse {
    fn from(operation: Operation) -> OperationResponse {
        OperationResponse {
            id: operation.id,
            status: operation.status,
            proposer: operation.proposer,
            executors: operation.executors,
            execution_time: operation.execution_time,
            target: operation.target,
            data: operation.data,
            title: operation.title,
            description: operation.description,
        }
    }
}

// We define a custom struct for each query response

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[allow(non_snake_case)]
pub struct OperationListResponse {
    pub operationList: Vec<OperationResponse>,
}


Error.rs

use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Delay time not ended")]
    Unexpired {},

    #[error("Address {address:?} not found in Proposers")]
    NotFound { address: String },

    #[error("Executed operations can not be cancelled.")]
    NotDeletable {},

    #[error("Proposers list already contains this proposer address")]
    AlreadyContainsProposerAddress {},

    #[error("Minimum Delay condition not satisfied.")]
    MinDelayNotSatisfied {},

    #[error("This operation already executed.")]
    Executed {},

    #[error("Changes can not be made on a frozen Timelock contract.")]
    TimelockFrozen {},

}

state.rs

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use cosmwasm_std::{Addr, Binary, Uint64};
use cw_storage_plus::{Item, Map};
use cw_utils::{Duration, Scheduled};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Timelock {
    pub admins: Vec<Addr>,
    pub proposers: Vec<Addr>,
    pub min_time_delay: Duration,
    pub frozen: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Operation {
    pub id: Uint64,
    pub status: OperationStatus,
    pub proposer: Addr,
    pub executors: Option<Vec<Addr>>,
    pub execution_time: Scheduled,
    pub target: Addr,
    pub data: Binary,
    pub title: String,
    pub description: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum OperationStatus {
    Pending,
    Ready,
    Done,
}

pub const CONFIG: Item<Timelock> = Item::new("timelock");
pub const OPERATION_LIST: Map<u64, Operation> = Map::new("operation_list");
pub const OPERATION_SEQ: Item<Uint64> = Item::new("operation_seq");

Attribution: 'CosmWasm by Example' provides access to the code on GitHub.

Last updated