时间锁控制器

时间锁控制器提供了一种对交易执行强制执行时间延迟的手段。这被认为是关于治理系统的良好实践,因为它允许用户有机会在决策执行之前退出系统,如果他们不同意该决策。

时间锁合约本身执行交易,而不是用户。因此,时间锁应该持有相关的资金、所有权和访问控制角色。

操作生命周期

操作的状态由 OperationState 枚举表示,并且可以通过调用具有操作标识符的 get_operation_state 函数来检索。

操作的标识符是一个 felt252 值,计算为操作调用或多个调用、其前置操作和盐的 Pedersen 哈希值。它可以通过调用实现合约的 hash_operation 函数来计算单次调用操作,或者通过 hash_operation_batch 来计算多次调用操作。再次提交具有相同调用、前置操作和相同盐值的操作将失败,因为操作标识符必须是唯一的。为了解决这个问题,使用不同的盐值来生成唯一的标识符。

时间锁定的操作遵循一个特定的生命周期:

UnsetWaitingReadyDone

  • Unset:操作尚未安排或已被取消。

  • Waiting:操作已安排并且正在等待安排的延迟。

  • Ready:计时器已过期,操作有资格执行。

  • Done:操作已执行。

时间锁流程

调度

当提议者调用 schedule 时,OperationStateUnset 变为 Waiting。 这将启动一个计时器,该计时器必须大于或等于最小延迟。 计时器在可通过 get_timestamp 访问的时间戳到期。 一旦计时器过期,OperationState 自动移动到 Ready 状态。 此时,它可以被执行。

执行

通过调用 execute,执行者触发操作的底层交易并将其移动到 Done 状态。如果操作有一个前置操作,则前置操作必须处于 Done 状态,此交易才能成功。

取消

cancel 函数允许取消者取消任何待处理的操作。 这会将操作重置为 Unset 状态。 因此,提议者可以重新安排已取消的操作。 在这种情况下,当操作被重新安排时,计时器重新启动。

角色

TimelockControllerComponent 利用了一个 AccessControlComponent 设置,我们需要了解这个设置才能设置角色。

  • PROPOSER_ROLE - 负责将操作排队。

  • CANCELLER_ROLE - 可以取消计划的操作。 在初始化期间,被授予 PROPOSER_ROLE 的帐户也将被授予 CANCELLER_ROLE。 因此,最初的提议者也可以在操作被安排后取消它们。

  • EXECUTOR_ROLE - 负责执行已经可用的操作。

  • DEFAULT_ADMIN_ROLE - 可以授予和撤销前三个角色。

DEFAULT_ADMIN_ROLE 是一个敏感的角色,它将自动授予给时间锁本身,也可以选择性地授予给第二个帐户。 后一种情况可能需要简化合约的初始配置;但是,这个角色应该立即放弃。

此外,时间锁组件支持 EXECUTOR_ROLE 的开放角色的概念。 这允许任何人在操作处于 Ready OperationState 时执行该操作。 要使 EXECUTOR_ROLE 开放,请将零地址授予 EXECUTOR_ROLE

非常小心地启用开放角色,因为_任何人_都可以调用该函数。

最小延迟

时间锁的最小延迟充当了从提议者安排操作到执行者可以执行该操作的最早时间点之间的缓冲区。 这样做的目的是为了让用户,如果他们不同意计划的提案,可以选择退出系统或提出他们的理由让取消者取消该操作。

初始化后,更改时间锁的最小延迟的唯一方法是安排它并使用与任何其他操作相同的流程执行它。

合约的最小延迟可以通过 get_min_delay 访问。

用法

将时间锁集成到合约中需要集成 TimelockControllerComponent 以及 SRC5ComponentAccessControlComponent 作为依赖项。 合约的构造函数应该初始化时间锁,这包括设置:

  • 提议者和执行者。

  • 安排和执行操作之间的最小延迟。

  • 如果需要额外的配置,可以选择管理员。

可选的管理员应在配置完成后放弃其角色。

这是一个简单的时间锁合约的示例:

#[starknet::contract]
mod TimelockControllerContract {
    use openzeppelin_access::accesscontrol::AccessControlComponent;
    use openzeppelin_governance::timelock::TimelockControllerComponent;
    use openzeppelin_introspection::src5::SRC5Component;
    use starknet::ContractAddress;

    component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent);
    component!(path: TimelockControllerComponent, storage: timelock, event: TimelockEvent);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // Timelock Mixin
    #[abi(embed_v0)]
    impl TimelockMixinImpl =
        TimelockControllerComponent::TimelockMixinImpl<ContractState>;
    impl TimelockInternalImpl = TimelockControllerComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        access_control: AccessControlComponent::Storage,
        #[substorage(v0)]
        timelock: TimelockControllerComponent::Storage,
        #[substorage(v0)]
        src5: SRC5Component::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        AccessControlEvent: AccessControlComponent::Event,
        #[flat]
        TimelockEvent: TimelockControllerComponent::Event,
        #[flat]
        SRC5Event: SRC5Component::Event
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        min_delay: u64,
        proposers: Span<ContractAddress>,
        executors: Span<ContractAddress>,
        admin: ContractAddress
    ) {
        self.timelock.initializer(min_delay, proposers, executors, admin);
    }
}

接口

这是 TimelockMixinImpl 实现的完整接口:

#[starknet::interface]
pub trait TimelockABI<TState> {
    // ITimelock
    fn is_operation(self: @TState, id: felt252) -> bool;
    fn is_operation_pending(self: @TState, id: felt252) -> bool;
    fn is_operation_ready(self: @TState, id: felt252) -> bool;
    fn is_operation_done(self: @TState, id: felt252) -> bool;
    fn get_timestamp(self: @TState, id: felt252) -> u64;
    fn get_operation_state(self: @TState, id: felt252) -> OperationState;
    fn get_min_delay(self: @TState) -> u64;
    fn hash_operation(self: @TState, call: Call, predecessor: felt252, salt: felt252) -> felt252;
    fn hash_operation_batch(
        self: @TState, calls: Span<Call>, predecessor: felt252, salt: felt252
    ) -> felt252;
    fn schedule(ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64);
    fn schedule_batch(
        ref self: TState, calls: Span<Call>, predecessor: felt252, salt: felt252, delay: u64
    );
    fn cancel(ref self: TState, id: felt252);
    fn execute(ref self: TState, call: Call, predecessor: felt252, salt: felt252);
    fn execute_batch(ref self: TState, calls: Span<Call>, predecessor: felt252, salt: felt252);
    fn update_delay(ref self: TState, new_delay: u64);

    // ISRC5
    fn supports_interface(self: @TState, interface_id: felt252) -> bool;

    // IAccessControl
    fn has_role(self: @TState, role: felt252, account: ContractAddress) -> bool;
    fn get_role_admin(self: @TState, role: felt252) -> felt252;
    fn grant_role(ref self: TState, role: felt252, account: ContractAddress);
    fn revoke_role(ref self: TState, role: felt252, account: ContractAddress);
    fn renounce_role(ref self: TState, role: felt252, account: ContractAddress);

    // IAccessControlCamel
    fn hasRole(self: @TState, role: felt252, account: ContractAddress) -> bool;
    fn getRoleAdmin(self: @TState, role: felt252) -> felt252;
    fn grantRole(ref self: TState, role: felt252, account: ContractAddress);
    fn revokeRole(ref self: TState, role: felt252, account: ContractAddress);
    fn renounceRole(ref self: TState, role: felt252, account: ContractAddress);
}