Timelock

Explicación

Un contrato inteligente de bloqueo de tiempo introduce un mecanismo de retraso para ejecutar llamadas a funciones en otros contratos inteligentes. Establece un retraso de tiempo mínimo predefinido antes de que se pueda ejecutar una operación programada.

Si bien Timelock no es estrictamente un contrato MultiSig/Voting, se alinea estrechamente con los principios de los contratos que cumplen con las especificaciones CW3. En lugar de una ejecución inmediata, las direcciones solo pueden proponer o programar operaciones, que luego sufren un retraso antes de que se permita la ejecución final.

Creación de instancias

Al implementar el contrato Timelock, puede configurar: Plazo mínimo y direcciones para actuar como Administradores y Proponentes. Los proponentes sólo podrán programar operaciones si su tiempo de ejecución excede el retraso establecido.

Administradores:

Los administradores manejan la configuración inicial y garantizan la compatibilidad con posibles contratos de destino. De forma predeterminada, el iniciador del contrato se convierte en administrador si no se proporcionan otras direcciones. Después de la configuración, los administradores pueden congelar el Timelock, haciéndolo inmutable. Esta acción es irrevocable y puede inutilizar el contrato.

Proponentes

Los proponentes programan operaciones que se ejecutarán después del retraso. Asegúrese de que el contrato Timelock tenga los permisos necesarios en los contratos de destino. Especifique las direcciones de los ejecutores responsables de la ejecución de la operación final en el contrato de destino. Si no se especifica ningún ejecutor, cualquier dirección se puede ejecutar una vez que llegue el momento.

Info

La programación no garantiza la ejecución. Las operaciones programadas pueden ser canceladas por el proponente antes de su ejecución. Por tanto, elegir a los proponentes es crucial.

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 {},
}

Ejemplo

Para crear un contrato de bloqueo de tiempo con CosmWasm, puede crear los siguientes archivos: lib.rs contract.rs msg.rs error.rs 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 {},
    // Add any other custom errors you like here.
    // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
}

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");

Last updated