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