Governor

去中心化协议从公开发布的那一刻起就不断发展。通常,最初的团队在第一阶段保留对这种演进的控制权,但最终将其委托给利益相关者的社区。这个社区做出决策的过程被称为链上治理,它已成为去中心化协议的核心组成部分,推动了各种决策,如参数调整、智能合约升级、与其他协议的集成、资金管理、授权等。

这种治理协议通常在一种叫做 "Governor" 的专用合约中实现。在 OpenZeppelin Contracts for Cairo 中,我们着手构建一个 Governor 组件的模块化系统,通过实现特定的 trait 可以满足不同的需求。您将找到开箱即用的最常见需求,但是编写额外的需求很简单,我们将在未来的版本中添加社区要求的新功能。

使用和设置

Token

我们治理设置中每个账户的投票权将由 ERC20 或 ERC721 token 决定。该 token 必须实现 {votes-component} 扩展。该扩展将跟踪历史余额,以便从过去快照中检索投票权,而不是从当前余额中检索,这是一个重要的保护措施,可以防止重复投票。

如果您的项目已经有一个没有包含 Votes 且不可升级的实时 token,您可以使用包装器将其包装在一个治理 token 中。这将允许 token 持有者通过 1 对 1 包装他们的 token 来参与治理。

该库目前不包含 token 的包装器,但它将在未来的版本中添加。
目前,时钟模式固定为区块时间戳,因为 Votes 组件使用区块时间戳来跟踪检查点。我们计划在未来的版本中为 Votes 添加对更灵活时钟模式的支持,例如允许使用区块号。

Governor

我们最初将构建一个没有时间锁的 Governor。核心逻辑由 {governor-component} 给出,但我们仍然需要选择:

1) 如何确定投票权,

2) 达到法定人数需要多少票,

3) 人们在投票时有哪些选择,以及如何计算这些票数,以及

4) 应该使用的执行机制。

通过编写自己的扩展可以自定义这些方面的每一个方面,或者更容易地从库中选择一个。

对于 1) 我们将使用 GovernorVotes 扩展,它连接到一个 {ivotes} 实例,以根据账户在提案激活时持有的 token 余额来确定账户的投票权。该模块需要 token 的地址作为初始化器的参数传递。

对于 2) 我们将使用 GovernorVotesQuorumFraction。它与 {ivotes} 实例一起工作,将法定人数定义为提案投票权检索时总供应量的百分比。除了投票 token 地址外,这还需要一个初始化器参数来设置百分比。现在大多数 Governor 使用 4%。由于法定人数分母是 1000 以提高精度,我们将模块初始化为分子 40,从而产生 4% 的法定人数 (40/1000 = 0.04 或 4%)。

对于 3) 我们将使用 GovernorCountingSimple,该扩展为选民提供 3 个选项:赞成、反对和弃权,并且只有赞成和弃权票计入法定人数。

对于 4) 我们将使用 GovernorCoreExecution,它允许通过 governor 直接执行提案。

另一个选项是 GovernorTimelockExecution。可以在下一节中找到一个例子。

除此之外,我们还需要实现 GovernorSettingsTrait,定义投票延迟、投票期和提案阈值。虽然我们可以使用 GovernorSettings 扩展,它允许 governor 本身设置这些参数,但我们将在合约中本地实现该 trait,并将投票延迟、投票期和提案阈值设置为常量值。

voting_delay: 提案创建后多久应固定投票权。较大的投票延迟让用户有时间在必要时取消抵押 token。

voting_period: 提案保持开放投票的时间。

这些参数以 token 时钟中定义的单位指定,目前始终是时间戳。

proposal_threshold: 这将提案创建限制在具有足够投票权的账户。

还需要实现 GovernorComponent::ImmutableConfig。对于下面的示例,我们使用了 DefaultConfig。有关更多详细信息,请查看 不可变的组件配置 指南。

最后缺少的步骤是添加一个 SNIP12Metadata 实现,用于检索 governor 的名称和版本。

#[starknet::contract]
mod MyGovernor {
    use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait;
    use openzeppelin_governance::governor::extensions::GovernorVotesQuorumFractionComponent::InternalTrait;
    use openzeppelin_governance::governor::extensions::{
        GovernorVotesQuorumFractionComponent, GovernorCountingSimpleComponent,
        GovernorCoreExecutionComponent,
    };
    use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig};
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
    use starknet::ContractAddress;

    pub const VOTING_DELAY: u64 = 86400; // 1 day
    pub const VOTING_PERIOD: u64 = 604800; // 1 week
    pub const PROPOSAL_THRESHOLD: u256 = 10;
    pub const QUORUM_NUMERATOR: u256 = 40; // 4%

    component!(path: GovernorComponent, storage: governor, event: GovernorEvent);
    component!(
        path: GovernorVotesQuorumFractionComponent,
        storage: governor_votes,
        event: GovernorVotesEvent
    );
    component!(
        path: GovernorCountingSimpleComponent,
        storage: governor_counting_simple,
        event: GovernorCountingSimpleEvent
    );
    component!(
        path: GovernorCoreExecutionComponent,
        storage: governor_core_execution,
        event: GovernorCoreExecutionEvent
    );
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // Governor
    #[abi(embed_v0)]
    impl GovernorImpl = GovernorComponent::GovernorImpl<ContractState>;

    // Extensions external
    #[abi(embed_v0)]
    impl QuorumFractionImpl =
        GovernorVotesQuorumFractionComponent::QuorumFractionImpl<ContractState>;

    // Extensions internal
    impl GovernorQuorumImpl = GovernorVotesQuorumFractionComponent::GovernorQuorum<ContractState>;
    impl GovernorVotesImpl = GovernorVotesQuorumFractionComponent::GovernorVotes<ContractState>;
    impl GovernorCountingSimpleImpl =
        GovernorCountingSimpleComponent::GovernorCounting<ContractState>;
    impl GovernorCoreExecutionImpl =
        GovernorCoreExecutionComponent::GovernorExecution<ContractState>;

    // SRC5
    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        pub governor: GovernorComponent::Storage,
        #[substorage(v0)]
        pub governor_votes: GovernorVotesQuorumFractionComponent::Storage,
        #[substorage(v0)]
        pub governor_counting_simple: GovernorCountingSimpleComponent::Storage,
        #[substorage(v0)]
        pub governor_core_execution: GovernorCoreExecutionComponent::Storage,
        #[substorage(v0)]
        pub src5: SRC5Component::Storage,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        GovernorEvent: GovernorComponent::Event,
        #[flat]
        GovernorVotesEvent: GovernorVotesQuorumFractionComponent::Event,
        #[flat]
        GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event,
        #[flat]
        GovernorCoreExecutionEvent: GovernorCoreExecutionComponent::Event,
        #[flat]
        SRC5Event: SRC5Component::Event,
    }

    #[constructor]
    fn constructor(ref self: ContractState, votes_token: ContractAddress) {
        self.governor.initializer();
        self.governor_votes.initializer(votes_token, QUORUM_NUMERATOR);
    }

    //
    // SNIP12 Metadata
    //

    pub impl SNIP12MetadataImpl of SNIP12Metadata {
        fn name() -> felt252 {
            'DAPP_NAME'
        }

        fn version() -> felt252 {
            'DAPP_VERSION'
        }
    }

    //
    // Locally implemented extensions
    //

    pub impl GovernorSettings of GovernorComponent::GovernorSettingsTrait<ContractState> {
        /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`.
        fn voting_delay(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
            VOTING_DELAY
        }

        /// See `GovernorComponent::GovernorSettingsTrait::voting_period`.
        fn voting_period(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
            VOTING_PERIOD
        }

        /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`.
        fn proposal_threshold(self: @GovernorComponent::ComponentState<ContractState>) -> u256 {
            PROPOSAL_THRESHOLD
        }
    }
}

Timelock

在治理决策中添加时间锁是一个好习惯。这允许用户在决策执行之前退出系统(如果他们对决策有异议)。我们将使用 OpenZeppelin 的 {timelock-controller} 与 GovernorTimelockExecution 扩展结合使用。

当使用时间锁时,时间锁将执行提案,因此时间锁应持有任何资金、所有权和访问控制角色。

TimelockController 使用一个 {access-control} 设置,我们需要了解它才能设置角色。

Proposer 角色负责将操作排队:这是必须授予 Governor 实例的角色,并且必须是系统中唯一的提议者(和取消者)。

Executor 角色负责执行已可用的操作:我们可以将此角色分配给特殊的零地址,以允许任何人执行(如果操作可能特别时间敏感,则应将 Governor 设置为 Executor)。

Canceller 角色负责取消操作:必须授予 Governor 实例此角色,并且必须是系统中唯一的取消者。

最后,还有 Admin 角色,它可以授予和撤销前两个角色:这是一个非常敏感的角色,它将自动授予给时间锁本身,并且可以选择授予给第二个帐户,该帐户可用于简化设置,但应立即放弃该角色。

以下示例使用 GovernorTimelockExecution 扩展,以及 GovernorSettings,并使用固定的法定人数值而不是百分比:

#[starknet::contract]
pub mod MyTimelockedGovernor {
    use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait;
    use openzeppelin_governance::governor::extensions::GovernorSettingsComponent::InternalTrait as GovernorSettingsInternalTrait;
    use openzeppelin_governance::governor::extensions::GovernorTimelockExecutionComponent::InternalTrait as GovernorTimelockExecutionInternalTrait;
    use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait as GovernorVotesInternalTrait;
    use openzeppelin_governance::governor::extensions::{
        GovernorVotesComponent, GovernorSettingsComponent, GovernorCountingSimpleComponent,
        GovernorTimelockExecutionComponent
    };
    use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig};
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
    use starknet::ContractAddress;

    pub const VOTING_DELAY: u64 = 86400; // 1 day
    pub const VOTING_PERIOD: u64 = 604800; // 1 week
    pub const PROPOSAL_THRESHOLD: u256 = 10;
    pub const QUORUM: u256 = 100_000_000;

    component!(path: GovernorComponent, storage: governor, event: GovernorEvent);
    component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent);
    component!(
        path: GovernorSettingsComponent, storage: governor_settings, event: GovernorSettingsEvent
    );
    component!(
        path: GovernorCountingSimpleComponent,
        storage: governor_counting_simple,
        event: GovernorCountingSimpleEvent
    );
    component!(
        path: GovernorTimelockExecutionComponent,
        storage: governor_timelock_execution,
        event: GovernorTimelockExecutionEvent
    );
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // Governor
    #[abi(embed_v0)]
    impl GovernorImpl = GovernorComponent::GovernorImpl<ContractState>;

    // Extensions external
    #[abi(embed_v0)]
    impl VotesTokenImpl = GovernorVotesComponent::VotesTokenImpl<ContractState>;
    #[abi(embed_v0)]
    impl GovernorSettingsAdminImpl =
        GovernorSettingsComponent::GovernorSettingsAdminImpl<ContractState>;
    #[abi(embed_v0)]
    impl TimelockedImpl =
        GovernorTimelockExecutionComponent::TimelockedImpl<ContractState>;

    // Extensions internal
    impl GovernorVotesImpl = GovernorVotesComponent::GovernorVotes<ContractState>;
    impl GovernorSettingsImpl = GovernorSettingsComponent::GovernorSettings<ContractState>;
    impl GovernorCountingSimpleImpl =
        GovernorCountingSimpleComponent::GovernorCounting<ContractState>;
    impl GovernorTimelockExecutionImpl =
        GovernorTimelockExecutionComponent::GovernorExecution<ContractState>;

    // SRC5
    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        pub governor: GovernorComponent::Storage,
        #[substorage(v0)]
        pub governor_votes: GovernorVotesComponent::Storage,
        #[substorage(v0)]
        pub governor_settings: GovernorSettingsComponent::Storage,
        #[substorage(v0)]
        pub governor_counting_simple: GovernorCountingSimpleComponent::Storage,
        #[substorage(v0)]
        pub governor_timelock_execution: GovernorTimelockExecutionComponent::Storage,
        #[substorage(v0)]
        pub src5: SRC5Component::Storage,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        GovernorEvent: GovernorComponent::Event,
        #[flat]
        GovernorVotesEvent: GovernorVotesComponent::Event,
        #[flat]
        GovernorSettingsEvent: GovernorSettingsComponent::Event,
        #[flat]
        GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event,
        #[flat]
        GovernorTimelockExecutionEvent: GovernorTimelockExecutionComponent::Event,
        #[flat]
        SRC5Event: SRC5Component::Event,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState, votes_token: ContractAddress, timelock_controller: ContractAddress
    ) {
        self.governor.initializer();
        self.governor_votes.initializer(votes_token);
        self.governor_settings.initializer(VOTING_DELAY, VOTING_PERIOD, PROPOSAL_THRESHOLD);
        self.governor_timelock_execution.initializer(timelock_controller);
    }

    //
    // SNIP12 Metadata
    //

    pub impl SNIP12MetadataImpl of SNIP12Metadata {
        fn name() -> felt252 {
            'DAPP_NAME'
        }

        fn version() -> felt252 {
            'DAPP_VERSION'
        }
    }

    //
    // Locally implemented extensions
    //

    impl GovernorQuorum of GovernorComponent::GovernorQuorumTrait<ContractState> {
        /// See `GovernorComponent::GovernorQuorumTrait::quorum`.
        fn quorum(self: @GovernorComponent::ComponentState<ContractState>, timepoint: u64) -> u256 {
            QUORUM
        }
    }
}

接口

这是 Governor 实现的完整接口:

#[starknet::interface]
pub trait IGovernor<TState> {
    fn name(self: @TState) -> felt252;
    fn version(self: @TState) -> felt252;
    fn COUNTING_MODE(self: @TState) -> ByteArray;
    fn hash_proposal(self: @TState, calls: Span<Call>, description_hash: felt252) -> felt252;
    fn state(self: @TState, proposal_id: felt252) -> ProposalState;
    fn proposal_threshold(self: @TState) -> u256;
    fn proposal_snapshot(self: @TState, proposal_id: felt252) -> u64;
    fn proposal_deadline(self: @TState, proposal_id: felt252) -> u64;
    fn proposal_proposer(self: @TState, proposal_id: felt252) -> ContractAddress;
    fn proposal_eta(self: @TState, proposal_id: felt252) -> u64;
    fn proposal_needs_queuing(self: @TState, proposal_id: felt252) -> bool;
    fn voting_delay(self: @TState) -> u64;
    fn voting_period(self: @TState) -> u64;
    fn quorum(self: @TState, timepoint: u64) -> u256;
    fn get_votes(self: @TState, account: ContractAddress, timepoint: u64) -> u256;
    fn get_votes_with_params(
        self: @TState, account: ContractAddress, timepoint: u64, params: Span<felt252>
    ) -> u256;
    fn has_voted(self: @TState, proposal_id: felt252, account: ContractAddress) -> bool;
    fn propose(ref self: TState, calls: Span<Call>, description: ByteArray) -> felt252;
    fn queue(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
    fn execute(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
    fn cancel(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
    fn cast_vote(ref self: TState, proposal_id: felt252, support: u8) -> u256;
    fn cast_vote_with_reason(
        ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray
    ) -> u256;
    fn cast_vote_with_reason_and_params(
        ref self: TState,
        proposal_id: felt252,
        support: u8,
        reason: ByteArray,
        params: Span<felt252>
    ) -> u256;
    fn cast_vote_by_sig(
        ref self: TState,
        proposal_id: felt252,
        support: u8,
        voter: ContractAddress,
        signature: Span<felt252>
    ) -> u256;
    fn cast_vote_with_reason_and_params_by_sig(
        ref self: TState,
        proposal_id: felt252,
        support: u8,
        voter: ContractAddress,
        reason: ByteArray,
        params: Span<felt252>,
        signature: Span<felt252>
    ) -> u256;
    fn nonces(self: @TState, voter: ContractAddress) -> felt252;
    fn relay(ref self: TState, call: Call);
}