投票

VotesComponent 提供了一个灵活的系统来跟踪和委托投票权。该系统允许用户将其投票权委托给其他地址,从而实现更积极地参与治理。

默认情况下,token 余额不计入投票权。这使得转账更便宜。缺点是它需要用户委托给自己以激活检查点并跟踪他们的投票权。
投票单位的转移必须由实施合约处理。在 ERC20ERC721 的情况下,这通常通过 hooks 完成。您可以查看 用法 部分,了解如何实现此目的的示例。

主要特性

  1. 委托: 用户可以将其投票权委托给任何地址,包括他们自己。投票权可以直接通过调用 delegate 函数来委托,也可以通过提供一个签名与 delegate_by_sig 一起使用来委托。

  2. 历史查询: 该系统跟踪每个账户的历史快照,这允许在特定时间戳查询账户的投票权。+ 例如,这可以用于确定账户在提案创建时的投票权,而不是使用当前的余额。

用法

当集成 VotesComponent 时,必须实现 VotingUnitsTrait,以作为实现合约的函数获取给定账户的投票单位。+ 为了简单起见,此模块已经为 ERC20ERC721 token 提供了两个实现,如果集成了相应的组件,它们将开箱即用。+ 此外,您必须实现 NoncesComponentSNIP12Metadata trait 才能通过签名进行委托。

以下是如何构建一个简单的 ERC20Votes 合约的示例:

#[starknet::contract]
mod ERC20VotesContract {
    use openzeppelin_governance::votes::VotesComponent;
    use openzeppelin_token::erc20::{ERC20Component, DefaultConfig};
    use openzeppelin_utils::cryptography::nonces::NoncesComponent;
    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
    use starknet::ContractAddress;

    component!(path: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent);
    component!(path: ERC20Component, storage: erc20, event: ERC20Event);
    component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);

    // Votes
    #[abi(embed_v0)]
    impl VotesImpl = VotesComponent::VotesImpl<ContractState>;
    impl VotesInternalImpl = VotesComponent::InternalImpl<ContractState>;

    // ERC20
    #[abi(embed_v0)]
    impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

    // Nonces
    #[abi(embed_v0)]
    impl NoncesImpl = NoncesComponent::NoncesImpl<ContractState>;

    #[storage]
    pub struct Storage {
        #[substorage(v0)]
        pub erc20_votes: VotesComponent::Storage,
        #[substorage(v0)]
        pub erc20: ERC20Component::Storage,
        #[substorage(v0)]
        pub nonces: NoncesComponent::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        ERC20VotesEvent: VotesComponent::Event,
        #[flat]
        ERC20Event: ERC20Component::Event,
        #[flat]
        NoncesEvent: NoncesComponent::Event
    }

    // Required for hash computation.
    // 哈希计算所需。
    pub impl SNIP12MetadataImpl of SNIP12Metadata {
        fn name() -> felt252 {
            'DAPP_NAME'
        }
        fn version() -> felt252 {
            'DAPP_VERSION'
        }
    }

    // We need to call the `transfer_voting_units` function after
    // 我们需要在每次 mint、burn 和 transfer 之后调用 `transfer_voting_units` 函数。
    // every mint, burn and transfer.
    // 为此,我们使用 `ERC20Component::ERC20HooksTrait` 的 `after_update` hook。
    // For this, we use the `after_update` hook of the `ERC20Component::ERC20HooksTrait`.
    impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
        fn after_update(
            ref self: ERC20Component::ComponentState<ContractState>,
            from: ContractAddress,
            recipient: ContractAddress,
            amount: u256
        ) {
            let mut contract_state = self.get_contract_mut();
            contract_state.erc20_votes.transfer_voting_units(from, recipient, amount);
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.erc20.initializer("MyToken", "MTK");
    }
}

以下是如何构建一个简单的 ERC721Votes 合约的示例:

#[starknet::contract]
pub mod ERC721VotesContract {
    use openzeppelin_governance::votes::VotesComponent;
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_token::erc721::ERC721Component;
    use openzeppelin_utils::cryptography::nonces::NoncesComponent;
    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
    use starknet::ContractAddress;

    component!(path: VotesComponent, storage: erc721_votes, event: ERC721VotesEvent);
    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);
    component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);

    // Votes
    #[abi(embed_v0)]
    impl VotesImpl = VotesComponent::VotesImpl<ContractState>;
    impl VotesInternalImpl = VotesComponent::InternalImpl<ContractState>;

    // ERC721
    #[abi(embed_v0)]
    impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;

    // Nonces
    #[abi(embed_v0)]
    impl NoncesImpl = NoncesComponent::NoncesImpl<ContractState>;

    #[storage]
    pub struct Storage {
        #[substorage(v0)]
        pub erc721_votes: VotesComponent::Storage,
        #[substorage(v0)]
        pub erc721: ERC721Component::Storage,
        #[substorage(v0)]
        pub src5: SRC5Component::Storage,
        #[substorage(v0)]
        pub nonces: NoncesComponent::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        ERC721VotesEvent: VotesComponent::Event,
        #[flat]
        ERC721Event: ERC721Component::Event,
        #[flat]
        SRC5Event: SRC5Component::Event,
        #[flat]
        NoncesEvent: NoncesComponent::Event
    }

    /// Required for hash computation.
    // 哈希计算所需。
    pub impl SNIP12MetadataImpl of SNIP12Metadata {
        fn name() -> felt252 {
            'DAPP_NAME'
        }
        fn version() -> felt252 {
            'DAPP_VERSION'
        }
    }

    // We need to call the `transfer_voting_units` function after
    // 我们需要在每次 mint、burn 和 transfer 之后调用 `transfer_voting_units` 函数。
    // every mint, burn and transfer.
    // 为此,我们使用 `ERC721Component::ERC721HooksTrait` 的 `before_update` hook。
    // For this, we use the `before_update` hook of the
    // 这个 hook 在 transfer 执行之前被调用。
    //`ERC721Component::ERC721HooksTrait`.
    // This hook is called before the transfer is executed.
    // 这使我们可以访问先前的所有者。
    // This gives us access to the previous owner.
    impl ERC721VotesHooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
        fn before_update(
            ref self: ERC721Component::ComponentState<ContractState>,
            to: ContractAddress,
            token_id: u256,
            auth: ContractAddress
        ) {
            let mut contract_state = self.get_contract_mut();

            // We use the internal function here since it does not check if the token
            // 我们在这里使用内部函数,因为它不检查 token id 是否存在,这对于 mint 是必要的
            // id exists which is necessary for mints
            let previous_owner = self._owner_of(token_id);
            contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1);
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.erc721.initializer("MyToken", "MTK", "");
    }
}

接口

这是 VotesImpl 实现的完整接口:

#[starknet::interface]
pub trait VotesABI<TState> {
    // IVotes
    fn get_votes(self: @TState, account: ContractAddress) -> u256;
    fn get_past_votes(self: @TState, account: ContractAddress, timepoint: u64) -> u256;
    fn get_past_total_supply(self: @TState, timepoint: u64) -> u256;
    fn delegates(self: @TState, account: ContractAddress) -> ContractAddress;
    fn delegate(ref self: TState, delegatee: ContractAddress);
    fn delegate_by_sig(ref self: TState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Span<felt252>);

    // INonces
    fn nonces(self: @TState, owner: ContractAddress) -> felt252;
}