From 4324006a50a00f3c6a560cdd18e174953c7b49a8 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 19 Aug 2024 17:30:07 -0300 Subject: [PATCH 01/44] votes first iteration --- packages/governance/src/lib.cairo | 1 + packages/governance/src/votes.cairo | 1 + packages/governance/src/votes/votes.cairo | 243 ++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 packages/governance/src/votes.cairo create mode 100644 packages/governance/src/votes/votes.cairo diff --git a/packages/governance/src/lib.cairo b/packages/governance/src/lib.cairo index 852e24521..05ce4b158 100644 --- a/packages/governance/src/lib.cairo +++ b/packages/governance/src/lib.cairo @@ -2,3 +2,4 @@ mod tests; pub mod timelock; pub mod utils; +pub mod votes; diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo new file mode 100644 index 000000000..d86a74c7d --- /dev/null +++ b/packages/governance/src/votes.cairo @@ -0,0 +1 @@ +pub mod votes; diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo new file mode 100644 index 000000000..bff69d058 --- /dev/null +++ b/packages/governance/src/votes/votes.cairo @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT + +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IVotesInternal { + fn get_voting_units(self: @TState, account: ContractAddress) -> u256; +} + +#[starknet::component] +pub mod VotesComponent { + use core::num::traits::Zero; + use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_governance::utils::interfaces::IVotes; + use openzeppelin_token::erc721::ERC721Component; + use openzeppelin_token::erc721::interface::IERC721; + use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; + use openzeppelin_utils::nonces::NoncesComponent; + use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; + use starknet::ContractAddress; + use starknet::storage::Map; + use super::{Delegation, OffchainMessageHash, SNIP12Metadata, IVotesInternal}; + + #[storage] + struct Storage { + Votes_delegatee: Map::, + Votes_delegate_checkpoints: Map::, + Votes_total_checkpoints: Trace, + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + DelegateChanged: DelegateChanged, + DelegateVotesChanged: DelegateVotesChanged, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct DelegateChanged { + #[key] + pub delegator: ContractAddress, + #[key] + pub from_delegate: ContractAddress, + #[key] + pub to_delegate: ContractAddress + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct DelegateVotesChanged { + #[key] + pub delegate: ContractAddress, + pub previous_votes: u256, + pub new_votes: u256 + } + + pub mod Errors { + pub const FUTURE_LOOKUP: felt252 = 'Votes: future Lookup'; + pub const EXPIRED_SIGNATURE: felt252 = 'Votes: expired signature'; + pub const INVALID_SIGNATURE: felt252 = 'Votes: invalid signature'; + } + + #[embeddable_as(VotesImpl)] + impl Votes< + TContractState, + +HasComponent, + impl Nonces: NoncesComponent::HasComponent, + impl TokenTrait: IVotesInternal>, + +SNIP12Metadata, + +Drop + > of IVotes> { + // Common implementation for both ERC20 and ERC721 + fn get_votes(self: @ComponentState, account: ContractAddress) -> u256 { + self.Votes_delegate_checkpoints.read(account).latest() + } + + fn get_past_votes( + self: @ComponentState, account: ContractAddress, timepoint: u64 + ) -> u256 { + let current_timepoint = starknet::get_block_timestamp(); + assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); + self.Votes_delegate_checkpoints.read(account).upper_lookup_recent(timepoint) + } + + fn get_past_total_supply(self: @ComponentState, timepoint: u64) -> u256 { + let current_timepoint = starknet::get_block_timestamp(); + assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); + self.Votes_total_checkpoints.read().upper_lookup_recent(timepoint) + } + + fn delegates( + self: @ComponentState, account: ContractAddress + ) -> ContractAddress { + self.Votes_delegatee.read(account) + } + + fn delegate(ref self: ComponentState, delegatee: ContractAddress) { + let sender = starknet::get_caller_address(); + self._delegate(sender, delegatee); + } + + fn delegate_by_sig( + ref self: ComponentState, + delegator: ContractAddress, + delegatee: ContractAddress, + nonce: felt252, + expiry: u64, + signature: Array + ) { + assert(starknet::get_block_timestamp() <= expiry, Errors::EXPIRED_SIGNATURE); + + // Check and increase nonce. + let mut nonces_component = get_dep_component_mut!(ref self, Nonces); + nonces_component.use_checked_nonce(delegator, nonce); + + // Build hash for calling `is_valid_signature`. + let delegation = Delegation { delegatee, nonce, expiry }; + let hash = delegation.get_message_hash(delegator); + + let is_valid_signature_felt = DualCaseAccount { contract_address: delegator } + .is_valid_signature(hash, signature); + + // Check either 'VALID' or True for backwards compatibility. + let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED + || is_valid_signature_felt == 1; + + assert(is_valid_signature, Errors::INVALID_SIGNATURE); + + // Delegate votes. + self._delegate(delegator, delegatee); + } + } + + #[embeddable_as(ERC721VotesImpl)] + // Should we also use a trait bound to make sure that the Votes trait is implemented? + impl ERC721Votes< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + +Drop + > of IVotesInternal> { + // ERC721-specific implementation + fn get_voting_units( + self: @ComponentState, account: ContractAddress + ) -> u256 { + let mut erc721_component = get_dep_component!(self, ERC721); + erc721_component.balance_of(account).into() + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl TokenTrait: IVotesInternal>, + +NoncesComponent::HasComponent, + +SNIP12Metadata, + +Drop + > of InternalTrait { + // Common internal functions + fn _delegate( + ref self: ComponentState, + account: ContractAddress, + delegatee: ContractAddress + ) { + let from_delegate = self.delegates(account); + self.Votes_delegatee.write(account, delegatee); + self + .emit( + DelegateChanged { delegator: account, from_delegate, to_delegate: delegatee } + ); + self + .move_delegate_votes( + from_delegate, delegatee, TokenTrait::get_voting_units(@self, account) + ); + } + + fn move_delegate_votes( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + amount: u256 + ) { + let zero_address = Zero::zero(); + let block_timestamp = starknet::get_block_timestamp(); + if (from != to && amount > 0) { + if (from != zero_address) { + let mut trace = self.Votes_delegate_checkpoints.read(from); + let (previous_votes, new_votes) = trace + .push(block_timestamp, trace.latest() - amount); + self.emit(DelegateVotesChanged { delegate: from, previous_votes, new_votes }); + } + if (to != zero_address) { + let mut trace = self.Votes_delegate_checkpoints.read(to); + let (previous_votes, new_votes) = trace + .push(block_timestamp, trace.latest() + amount); + self.emit(DelegateVotesChanged { delegate: to, previous_votes, new_votes }); + } + } + } + + fn transfer_voting_units( + ref self: ComponentState, + from: ContractAddress, + to: ContractAddress, + amount: u256 + ) { + let zero_address = Zero::zero(); + let block_timestamp = starknet::get_block_timestamp(); + if (from == zero_address) { + let mut trace = self.Votes_total_checkpoints.read(); + trace.push(block_timestamp, trace.latest() + amount); + } + if (to == zero_address) { + let mut trace = self.Votes_total_checkpoints.read(); + trace.push(block_timestamp, trace.latest() - amount); + } + self.move_delegate_votes(self.delegates(from), self.delegates(to), amount); + } + } +} + +pub const DELEGATION_TYPE_HASH: felt252 = + 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; + +#[derive(Copy, Drop, Hash)] +pub struct Delegation { + pub delegatee: ContractAddress, + pub nonce: felt252, + pub expiry: u64 +} + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Delegation) -> felt252 { + let hash_state = PoseidonTrait::new(); + hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + } +} From dd0eb3856f77d41f04871d84ace5ad009104eb9e Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 22 Aug 2024 16:37:02 -0300 Subject: [PATCH 02/44] consolidate votes and delegation to governace module --- Scarb.lock | 2 + packages/governance/Scarb.toml | 2 + packages/governance/src/lib.cairo | 1 - packages/governance/src/utils.cairo | 1 - .../governance/src/utils/interfaces.cairo | 3 -- packages/governance/src/votes.cairo | 4 +- .../votes.cairo => votes/interface.cairo} | 6 +++ packages/governance/src/votes/utils.cairo | 29 ++++++++++++ packages/governance/src/votes/votes.cairo | 45 +++++++------------ .../src/erc20/extensions/erc20_votes.cairo | 29 ++---------- .../src/tests/erc20/test_erc20_votes.cairo | 2 +- 11 files changed, 63 insertions(+), 61 deletions(-) delete mode 100644 packages/governance/src/utils.cairo delete mode 100644 packages/governance/src/utils/interfaces.cairo rename packages/governance/src/{utils/interfaces/votes.cairo => votes/interface.cairo} (86%) create mode 100644 packages/governance/src/votes/utils.cairo diff --git a/Scarb.lock b/Scarb.lock index 0fa419a96..48e179b1e 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -46,8 +46,10 @@ name = "openzeppelin_governance" version = "0.15.1" dependencies = [ "openzeppelin_access", + "openzeppelin_account", "openzeppelin_introspection", "openzeppelin_testing", + "openzeppelin_token", "snforge_std", ] diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index 644641f1d..68df693ad 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -19,6 +19,8 @@ fmt.workspace = true starknet.workspace = true openzeppelin_access = { path = "../access" } openzeppelin_introspection = { path = "../introspection" } +openzeppelin_account = { path = "../account"} +openzeppelin_token = {path= "../token"} [dev-dependencies] snforge_std.workspace = true diff --git a/packages/governance/src/lib.cairo b/packages/governance/src/lib.cairo index 05ce4b158..1240e356f 100644 --- a/packages/governance/src/lib.cairo +++ b/packages/governance/src/lib.cairo @@ -1,5 +1,4 @@ mod tests; pub mod timelock; -pub mod utils; pub mod votes; diff --git a/packages/governance/src/utils.cairo b/packages/governance/src/utils.cairo deleted file mode 100644 index 43b15ec08..000000000 --- a/packages/governance/src/utils.cairo +++ /dev/null @@ -1 +0,0 @@ -pub mod interfaces; diff --git a/packages/governance/src/utils/interfaces.cairo b/packages/governance/src/utils/interfaces.cairo deleted file mode 100644 index 7d7abb05b..000000000 --- a/packages/governance/src/utils/interfaces.cairo +++ /dev/null @@ -1,3 +0,0 @@ -pub mod votes; - -pub use votes::IVotes; diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index d86a74c7d..ca7cd7390 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -1 +1,3 @@ -pub mod votes; +pub mod interface; +pub mod utils; +pub mod votes; \ No newline at end of file diff --git a/packages/governance/src/utils/interfaces/votes.cairo b/packages/governance/src/votes/interface.cairo similarity index 86% rename from packages/governance/src/utils/interfaces/votes.cairo rename to packages/governance/src/votes/interface.cairo index d55c481c2..401c9f7d3 100644 --- a/packages/governance/src/utils/interfaces/votes.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -33,3 +33,9 @@ pub trait IVotes { signature: Array ); } + +/// Common interface for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) +#[starknet::interface] +pub trait IVotesToken { + fn get_voting_units(self: @TState, account: ContractAddress) -> u256; +} diff --git a/packages/governance/src/votes/utils.cairo b/packages/governance/src/votes/utils.cairo new file mode 100644 index 000000000..9c1002d96 --- /dev/null +++ b/packages/governance/src/votes/utils.cairo @@ -0,0 +1,29 @@ +// +// Offchain message hash generation helpers. +// + +// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") +// +// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. + +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::{StructHash}; +use starknet::ContractAddress; + +pub const DELEGATION_TYPE_HASH: felt252 = + 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; + +#[derive(Copy, Drop, Hash)] +pub struct Delegation { + pub delegatee: ContractAddress, + pub nonce: felt252, + pub expiry: u64 +} + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Delegation) -> felt252 { + let hash_state = PoseidonTrait::new(); + hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + } +} diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index bff69d058..10435bbdd 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -1,21 +1,20 @@ // SPDX-License-Identifier: MIT - +// OpenZeppelin Contracts for Cairo v0.15.1 (governance/votes/votes.cairo) use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; use starknet::ContractAddress; -#[starknet::interface] -pub trait IVotesInternal { - fn get_voting_units(self: @TState, account: ContractAddress) -> u256; -} #[starknet::component] pub mod VotesComponent { + // We should not use Checkpoints or StorageArray as they are for ERC721Vote + // Instead we can rely on Vec use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_governance::utils::interfaces::IVotes; + use openzeppelin_governance::votes::interface::{IVotes, IVotesToken}; + use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_token::erc721::ERC721Component; use openzeppelin_token::erc721::interface::IERC721; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; @@ -23,7 +22,7 @@ pub mod VotesComponent { use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; use starknet::ContractAddress; use starknet::storage::Map; - use super::{Delegation, OffchainMessageHash, SNIP12Metadata, IVotesInternal}; + use super::{OffchainMessageHash, SNIP12Metadata}; #[storage] struct Storage { @@ -68,7 +67,7 @@ pub mod VotesComponent { TContractState, +HasComponent, impl Nonces: NoncesComponent::HasComponent, - impl TokenTrait: IVotesInternal>, + impl TokenTrait: IVotesToken>, +SNIP12Metadata, +Drop > of IVotes> { @@ -134,7 +133,10 @@ pub mod VotesComponent { } } - #[embeddable_as(ERC721VotesImpl)] + // + // Internal for ERC721Votes + // + // Should we also use a trait bound to make sure that the Votes trait is implemented? impl ERC721Votes< TContractState, @@ -143,7 +145,7 @@ pub mod VotesComponent { impl ERC721: ERC721Component::HasComponent, +ERC721Component::ERC721HooksTrait, +Drop - > of IVotesInternal> { + > of IVotesToken> { // ERC721-specific implementation fn get_voting_units( self: @ComponentState, account: ContractAddress @@ -153,11 +155,15 @@ pub mod VotesComponent { } } + // + // Internal + // + #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, - impl TokenTrait: IVotesInternal>, + impl TokenTrait: IVotesToken>, +NoncesComponent::HasComponent, +SNIP12Metadata, +Drop @@ -224,20 +230,3 @@ pub mod VotesComponent { } } } - -pub const DELEGATION_TYPE_HASH: felt252 = - 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; - -#[derive(Copy, Drop, Hash)] -pub struct Delegation { - pub delegatee: ContractAddress, - pub nonce: felt252, - pub expiry: u64 -} - -impl StructHashImpl of StructHash { - fn hash_struct(self: @Delegation) -> felt252 { - let hash_state = PoseidonTrait::new(); - hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() - } -} diff --git a/packages/token/src/erc20/extensions/erc20_votes.cairo b/packages/token/src/erc20/extensions/erc20_votes.cairo index b13cd160d..32237f84c 100644 --- a/packages/token/src/erc20/extensions/erc20_votes.cairo +++ b/packages/token/src/erc20/extensions/erc20_votes.cairo @@ -18,7 +18,8 @@ use starknet::ContractAddress; pub mod ERC20VotesComponent { use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_governance::utils::interfaces::IVotes; + use openzeppelin_governance::votes::interface::IVotes; + use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_token::erc20::interface::IERC20; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; @@ -26,7 +27,7 @@ pub mod ERC20VotesComponent { use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; use starknet::ContractAddress; use starknet::storage::Map; - use super::{Delegation, OffchainMessageHash, SNIP12Metadata}; + use super::{OffchainMessageHash, SNIP12Metadata}; #[storage] struct Storage { @@ -285,27 +286,3 @@ pub mod ERC20VotesComponent { } } } - -// -// Offchain message hash generation helpers. -// - -// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") -// -// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. -pub const DELEGATION_TYPE_HASH: felt252 = - 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; - -#[derive(Copy, Drop, Hash)] -pub struct Delegation { - pub delegatee: ContractAddress, - pub nonce: felt252, - pub expiry: u64 -} - -impl StructHashImpl of StructHash { - fn hash_struct(self: @Delegation) -> felt252 { - let hash_state = PoseidonTrait::new(); - hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() - } -} diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo index 53961b4a7..3390ba9e4 100644 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ b/packages/token/src/tests/erc20/test_erc20_votes.cairo @@ -9,7 +9,7 @@ use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ }; use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ERC20VotesImpl, InternalImpl}; use openzeppelin_token::erc20::extensions::ERC20VotesComponent; -use openzeppelin_token::erc20::extensions::erc20_votes::Delegation; +use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock::SNIP12MetadataImpl; use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; From 5141fb52c90e149ad6e0c71ae991131b0c100b78 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 22 Aug 2024 17:27:06 -0300 Subject: [PATCH 03/44] fmt --- packages/governance/src/votes.cairo | 2 +- packages/governance/src/votes/votes.cairo | 2 +- packages/token/src/tests/erc20/test_erc20_votes.cairo | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index ca7cd7390..69e5a8028 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -1,3 +1,3 @@ pub mod interface; pub mod utils; -pub mod votes; \ No newline at end of file +pub mod votes; diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 10435bbdd..43c6f2218 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -12,9 +12,9 @@ pub mod VotesComponent { // Instead we can rely on Vec use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_governance::votes::interface::{IVotes, IVotesToken}; use openzeppelin_governance::votes::utils::{Delegation}; + use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc721::ERC721Component; use openzeppelin_token::erc721::interface::IERC721; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo index 3390ba9e4..c11e9bf07 100644 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ b/packages/token/src/tests/erc20/test_erc20_votes.cairo @@ -1,5 +1,6 @@ use core::num::traits::Bounded; use core::num::traits::Zero; +use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; use openzeppelin_testing::events::EventSpyExt; @@ -9,7 +10,6 @@ use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ }; use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ERC20VotesImpl, InternalImpl}; use openzeppelin_token::erc20::extensions::ERC20VotesComponent; -use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock::SNIP12MetadataImpl; use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; From 2a763413347fbf30e122e4505ea652cd5f515579 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 27 Aug 2024 17:41:02 -0400 Subject: [PATCH 04/44] add ERC20Votes impl, code docs and remove interface for internal iml --- packages/governance/src/votes/interface.cairo | 3 +- packages/governance/src/votes/votes.cairo | 102 ++++++++++++++++-- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index 401c9f7d3..707002b3d 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -35,7 +35,6 @@ pub trait IVotes { } /// Common interface for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) -#[starknet::interface] -pub trait IVotesToken { +pub trait TokenVotesTrait { fn get_voting_units(self: @TState, account: ContractAddress) -> u256; } diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 43c6f2218..47d27a927 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -1,20 +1,34 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.15.1 (governance/votes/votes.cairo) + use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; use starknet::ContractAddress; - +/// # Votes Component +/// +/// The Votes component provides a flexible system for tracking voting power and delegation +/// that is currently implemented for ERC20 and ERC721 tokens. It allows accounts to delegate +/// their voting power to a representative, who can then use the pooled voting power in +/// governance decisions. Voting power must be delegated to be counted, and an account can +/// delegate to itself if it wishes to vote directly. +/// +/// This component offers a unified interface for voting mechanisms across ERC20 and ERC721 +/// token standards, with the potential to be extended to other token standards in the future. +/// It's important to note that only one token implementation (either ERC20 or ERC721) should +/// be used at a time to ensure consistent voting power calculations. #[starknet::component] pub mod VotesComponent { // We should not use Checkpoints or StorageArray as they are for ERC721Vote // Instead we can rely on Vec use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_governance::votes::interface::{IVotes, IVotesToken}; + use openzeppelin_governance::votes::interface::{IVotes, TokenVotesTrait}; use openzeppelin_governance::votes::utils::{Delegation}; use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::interface::IERC20; use openzeppelin_token::erc721::ERC721Component; use openzeppelin_token::erc721::interface::IERC721; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; @@ -67,15 +81,20 @@ pub mod VotesComponent { TContractState, +HasComponent, impl Nonces: NoncesComponent::HasComponent, - impl TokenTrait: IVotesToken>, + impl TokenTrait: TokenVotesTrait>, +SNIP12Metadata, +Drop > of IVotes> { - // Common implementation for both ERC20 and ERC721 + /// Returns the current amount of votes that `account` has. fn get_votes(self: @ComponentState, account: ContractAddress) -> u256 { self.Votes_delegate_checkpoints.read(account).latest() } + /// Returns the amount of votes that `account` had at a specific moment in the past. + /// + /// Requirements: + /// + /// - `timepoint` must be in the past. fn get_past_votes( self: @ComponentState, account: ContractAddress, timepoint: u64 ) -> u256 { @@ -84,23 +103,45 @@ pub mod VotesComponent { self.Votes_delegate_checkpoints.read(account).upper_lookup_recent(timepoint) } + /// Returns the total supply of votes available at a specific moment in the past. + /// + /// Requirements: + /// + /// - `timepoint` must be in the past. fn get_past_total_supply(self: @ComponentState, timepoint: u64) -> u256 { let current_timepoint = starknet::get_block_timestamp(); assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); self.Votes_total_checkpoints.read().upper_lookup_recent(timepoint) } + /// Returns the delegate that `account` has chosen. fn delegates( self: @ComponentState, account: ContractAddress ) -> ContractAddress { self.Votes_delegatee.read(account) } + /// Delegates votes from the sender to `delegatee`. + /// + /// Emits a `DelegateChanged` event. + /// May emit one or two `DelegateVotesChanged` events. fn delegate(ref self: ComponentState, delegatee: ContractAddress) { let sender = starknet::get_caller_address(); self._delegate(sender, delegatee); } + /// Delegates votes from the sender to `delegatee` through a SNIP12 message signature + /// validation. + /// + /// Requirements: + /// + /// - `expiry` must not be in the past. + /// - `nonce` must match the account's current nonce. + /// - `delegator` must implement `SRC6::is_valid_signature`. + /// - `signature` should be valid for the message hash. + /// + /// Emits a `DelegateChanged` event. + /// May emit one or two `DelegateVotesChanged` events. fn delegate_by_sig( ref self: ComponentState, delegator: ContractAddress, @@ -137,16 +178,19 @@ pub mod VotesComponent { // Internal for ERC721Votes // - // Should we also use a trait bound to make sure that the Votes trait is implemented? - impl ERC721Votes< + pub impl ERC721VotesImpl< TContractState, +HasComponent, +SRC5Component::HasComponent, impl ERC721: ERC721Component::HasComponent, +ERC721Component::ERC721HooksTrait, +Drop - > of IVotesToken> { - // ERC721-specific implementation + > of TokenVotesTrait> { + /// Returns the number of voting units for a given account. + /// + /// This implementation is specific to ERC721 tokens, where each token + /// represents one voting unit. The function returns the balance of + /// ERC721 tokens for the specified account. fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { @@ -156,19 +200,46 @@ pub mod VotesComponent { } // - // Internal + // Internal for ERC20Votes + // + + pub impl ERC20VotesImpl< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + +Drop + > of TokenVotesTrait> { + /// Returns the number of voting units for a given account. + /// + /// This implementation is specific to ERC20 tokens, where the balance + /// of tokens directly represents the number of voting units. + fn get_voting_units( + self: @ComponentState, account: ContractAddress + ) -> u256 { + let mut erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(account) + } + } + + // + // Common internal for Votes // #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, - impl TokenTrait: IVotesToken>, + impl TokenTrait: TokenVotesTrait>, +NoncesComponent::HasComponent, +SNIP12Metadata, +Drop > of InternalTrait { - // Common internal functions + /// Delegates all of `account`'s voting units to `delegatee`. + /// + /// Emits a `DelegateChanged` event. + /// May emit one or two `DelegateVotesChanged` events. fn _delegate( ref self: ComponentState, account: ContractAddress, @@ -186,6 +257,9 @@ pub mod VotesComponent { ); } + /// Moves delegated votes from one delegate to another. + /// + /// May emit one or two `DelegateVotesChanged` events. fn move_delegate_votes( ref self: ComponentState, from: ContractAddress, @@ -210,6 +284,12 @@ pub mod VotesComponent { } } + /// Transfers, mints, or burns voting units. + /// + /// To register a mint, `from` should be zero. To register a burn, `to` + /// should be zero. Total supply of voting units will be adjusted with mints and burns. + /// + /// May emit one or two `DelegateVotesChanged` events. fn transfer_voting_units( ref self: ComponentState, from: ContractAddress, From 6c02234078fbcf1de3cb948d43804880f92b4617 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Wed, 4 Sep 2024 17:16:56 -0400 Subject: [PATCH 05/44] add basic tests and mocks --- packages/governance/src/tests.cairo | 2 + packages/governance/src/tests/mocks.cairo | 1 + .../src/tests/mocks/votes_mocks.cairo | 189 +++++++++++ .../governance/src/tests/test_votes.cairo | 309 ++++++++++++++++++ 4 files changed, 501 insertions(+) create mode 100644 packages/governance/src/tests/mocks/votes_mocks.cairo create mode 100644 packages/governance/src/tests/test_votes.cairo diff --git a/packages/governance/src/tests.cairo b/packages/governance/src/tests.cairo index 221670eec..9da570e01 100644 --- a/packages/governance/src/tests.cairo +++ b/packages/governance/src/tests.cairo @@ -4,3 +4,5 @@ pub(crate) mod mocks; mod test_timelock; #[cfg(test)] mod test_utils; +#[cfg(test)] +mod test_votes; diff --git a/packages/governance/src/tests/mocks.cairo b/packages/governance/src/tests/mocks.cairo index 6d38a6715..72e9e6b05 100644 --- a/packages/governance/src/tests/mocks.cairo +++ b/packages/governance/src/tests/mocks.cairo @@ -1,2 +1,3 @@ pub(crate) mod non_implementing_mock; pub(crate) mod timelock_mocks; +pub(crate) mod votes_mocks; diff --git a/packages/governance/src/tests/mocks/votes_mocks.cairo b/packages/governance/src/tests/mocks/votes_mocks.cairo new file mode 100644 index 000000000..1d01470ef --- /dev/null +++ b/packages/governance/src/tests/mocks/votes_mocks.cairo @@ -0,0 +1,189 @@ +#[starknet::contract] +pub(crate) mod ERC721VotesMock { + use openzeppelin_governance::votes::votes::VotesComponent; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::ERC721Component; + // This is temporary - we should actually implement the hooks manually + // and transfer the voting units in the hooks. + use openzeppelin_token::erc721::ERC721HooksEmptyImpl; + use openzeppelin_utils::cryptography::nonces::NoncesComponent; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + + 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 and ERC721Votes + #[abi(embed_v0)] + impl VotesImpl = VotesComponent::VotesImpl; + impl InternalImpl = VotesComponent::InternalImpl; + impl ERC721VotesInternalImpl = VotesComponent::ERC721VotesImpl; + + // ERC721 + #[abi(embed_v0)] + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // Nonces + #[abi(embed_v0)] + impl NoncesImpl = NoncesComponent::NoncesImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721_votes: VotesComponent::Storage, + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + 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(crate) impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'DAPP_NAME' + } + fn version() -> felt252 { + 'DAPP_VERSION' + } + } + + // + // Hooks + // + + // impl ERC721VotesHooksImpl< + // TContractState, + // impl Votes: VotesComponent::HasComponent, + // impl HasComponent: VotesComponent::HasComponent, + // +NoncesComponent::HasComponent, + // +Drop + // > of ERC721Component::ERC721HooksTrait { + // fn after_update( + // ref self: ERC721Component::ComponentState, + // to: ContractAddress, + // token_id: u256, + // auth: ContractAddress + // ) { + // let mut erc721_votes_component = get_dep_component_mut!(ref self, Votes); + // erc721_votes_component.transfer_voting_units(auth, to, 1); + // } + // } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc721.initializer("MyToken", "MTK", ""); + } +} + +#[starknet::contract] +pub(crate) mod ERC20VotesMock { + use openzeppelin_governance::votes::votes::VotesComponent; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc20::ERC20Component; + // This is temporary - we should actually implement the hooks manually + // and transfer the voting units in the hooks. + use openzeppelin_token::erc20::ERC20HooksEmptyImpl; + use openzeppelin_utils::cryptography::nonces::NoncesComponent; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + + component!(path: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // Votes and ERC20Votes + #[abi(embed_v0)] + impl VotesImpl = VotesComponent::VotesImpl; + impl InternalImpl = VotesComponent::InternalImpl; + impl ERC20VotesInternalImpl = VotesComponent::ERC20VotesImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + // Nonces + #[abi(embed_v0)] + impl NoncesImpl = NoncesComponent::NoncesImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20_votes: VotesComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + nonces: NoncesComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20VotesEvent: VotesComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + NoncesEvent: NoncesComponent::Event + } + + /// Required for hash computation. + pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'DAPP_NAME' + } + fn version() -> felt252 { + 'DAPP_VERSION' + } + } + + // + // Hooks + // + + // Uncomment and modify this section if you need ERC20 hooks + // impl ERC20VotesHooksImpl< + // TContractState, + // impl Votes: VotesComponent::HasComponent, + // impl HasComponent: VotesComponent::HasComponent, + // +NoncesComponent::HasComponent, + // +Drop + // > of ERC20Component::ERC20HooksTrait { + // fn after_transfer( + // ref self: ERC20Component::ComponentState, + // from: ContractAddress, + // to: ContractAddress, + // amount: u256 + // ) { + // let mut erc20_votes_component = get_dep_component_mut!(ref self, Votes); + // erc20_votes_component.transfer_voting_units(from, to, amount); + // } + // } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc20.initializer("MyToken", "MTK"); + } +} + diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo new file mode 100644 index 000000000..b72477946 --- /dev/null +++ b/packages/governance/src/tests/test_votes.cairo @@ -0,0 +1,309 @@ +use openzeppelin_governance::tests::mocks::votes_mocks::ERC721VotesMock::SNIP12MetadataImpl; +use openzeppelin_governance::tests::mocks::votes_mocks::{ERC721VotesMock, ERC20VotesMock}; +use openzeppelin_governance::votes::interface::TokenVotesTrait; +use openzeppelin_governance::votes::votes::VotesComponent::{ + DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, +}; +use openzeppelin_governance::votes::votes::VotesComponent; +use openzeppelin_testing as utils; +use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; +use openzeppelin_testing::events::EventSpyExt; +use openzeppelin_token::erc20::ERC20Component::InternalTrait; +use openzeppelin_token::erc721::ERC721Component::{ + ERC721MetadataImpl, InternalImpl as ERC721InternalImpl, +}; +use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; +use openzeppelin_utils::structs::checkpoint::TraceTrait; +use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; +use snforge_std::{ + cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address +}; +use snforge_std::{EventSpy}; +use starknet::ContractAddress; + + +// +// Setup +// + +type ComponentState = VotesComponent::ComponentState; +type ERC20ComponentState = VotesComponent::ComponentState; + +fn COMPONENT_STATE() -> ComponentState { + VotesComponent::component_state_for_testing() +} + +fn ERC20_COMPONENT_STATE() -> ERC20ComponentState { + VotesComponent::component_state_for_testing() +} + +fn ERC721VOTES_CONTRACT_STATE() -> ERC721VotesMock::ContractState { + ERC721VotesMock::contract_state_for_testing() +} + +fn ERC20VOTES_CONTRACT_STATE() -> ERC20VotesMock::ContractState { + ERC20VotesMock::contract_state_for_testing() +} + +fn setup_erc721_votes() -> ComponentState { + let mut state = COMPONENT_STATE(); + let mut mock_state = ERC721VOTES_CONTRACT_STATE(); + // Mint 10 NFTs to OWNER + let mut i: u256 = 0; + loop { + if i >= 10 { + break; + } + mock_state.erc721.mint(OWNER(), i); + // We manually transfer voting units here, since this is usually implemented in the hook + state.transfer_voting_units(ZERO(), OWNER(), 1); + i += 1; + }; + state +} + +fn setup_erc20_votes() -> ERC20ComponentState { + let mut state = ERC20_COMPONENT_STATE(); + let mut mock_state = ERC20VOTES_CONTRACT_STATE(); + + // Mint SUPPLY tokens to owner + mock_state.erc20.mint(OWNER(), SUPPLY); + // We manually transfer voting units here, since this is usually implemented in the hook + state.transfer_voting_units(ZERO(), OWNER(), 1); + + state +} + +fn setup_account(public_key: felt252) -> ContractAddress { + let mut calldata = array![public_key]; + utils::declare_and_deploy("DualCaseAccountMock", calldata) +} + +// +// Common tests for Votes +// + +#[test] +fn test_get_votes() { + let mut state = setup_erc721_votes(); + start_cheat_caller_address(test_address(), OWNER()); + // Before delegating, the owner has 0 votes + assert_eq!(state.get_votes(OWNER()), 0); + state.delegate(OWNER()); + + assert_eq!(state.get_votes(OWNER()), 10); +} + +// This test can be improved by using the api of the component +// to add checkpoints and thus verify the internal state of the component +// instead of using the trace directly. +#[test] +fn test_get_past_votes() { + let mut state = setup_erc721_votes(); + let mut trace = state.Votes_delegate_checkpoints.read(OWNER()); + + cheat_block_timestamp_global('ts10'); + + trace.push('ts1', 3); + trace.push('ts2', 5); + trace.push('ts3', 7); + + assert_eq!(state.get_past_votes(OWNER(), 'ts2'), 5); + assert_eq!(state.get_past_votes(OWNER(), 'ts5'), 7); +} + +#[test] +#[should_panic(expected: ('Votes: future Lookup',))] +fn test_get_past_votes_future_lookup() { + let state = setup_erc721_votes(); + cheat_block_timestamp_global('ts1'); + state.get_past_votes(OWNER(), 'ts2'); +} + +#[test] +fn test_get_past_total_supply() { + let mut state = setup_erc721_votes(); + let mut trace = state.Votes_total_checkpoints.read(); + + cheat_block_timestamp_global('ts10'); + trace.push('ts1', 3); + trace.push('ts2', 5); + trace.push('ts3', 7); + + assert_eq!(state.get_past_total_supply('ts2'), 5); + assert_eq!(state.get_past_total_supply('ts5'), 7); +} + +#[test] +#[should_panic(expected: ('Votes: future Lookup',))] +fn test_get_past_total_supply_future_lookup() { + let state = setup_erc721_votes(); + cheat_block_timestamp_global('ts1'); + state.get_past_total_supply('ts2'); +} + +#[test] +fn test_self_delegate() { + let mut state = setup_erc721_votes(); + let contract_address = test_address(); + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, OWNER()); + + state.delegate(OWNER()); + spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), OWNER()); + spy.assert_only_event_delegate_votes_changed(contract_address, OWNER(), 0, 10); + assert_eq!(state.get_votes(OWNER()), 10); +} + +#[test] +fn test_delegate_to_recipient_updates_votes() { + let mut state = setup_erc721_votes(); + let contract_address = test_address(); + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, OWNER()); + + state.delegate(RECIPIENT()); + spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), RECIPIENT()); + spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, 10); + assert_eq!(state.get_votes(RECIPIENT()), 10); + assert_eq!(state.get_votes(OWNER()), 0); +} + +#[test] +fn test_delegate_to_recipient_updates_delegates() { + let mut state = setup_erc721_votes(); + start_cheat_caller_address(test_address(), OWNER()); + state.delegate(OWNER()); + assert_eq!(state.delegates(OWNER()), OWNER()); + state.delegate(RECIPIENT()); + assert_eq!(state.delegates(OWNER()), RECIPIENT()); +} + +// #[test] +// fn test_delegate_by_sig() { +// cheat_chain_id_global('SN_TEST'); +// cheat_block_timestamp_global('ts1'); +// let mut state = setup_erc721_votes(); +// let contract_address = test_address(); +// let key_pair = KeyPairTrait::generate(); +// let account = setup_account(key_pair.public_key); +// let nonce = 0; +// let expiry = 'ts2'; +// let delegator = account; +// let delegatee = RECIPIENT(); +// let delegation = Delegation { delegatee, nonce, expiry }; +// let msg_hash = delegation.get_message_hash(delegator); +// let (r, s) = key_pair.sign(msg_hash).unwrap(); +// let mut spy = spy_events(); +// state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); +// spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); +// assert_eq!(state.delegates(account), delegatee); +// } + +#[test] +#[should_panic(expected: ('Votes: expired signature',))] +fn test_delegate_by_sig_past_expiry() { + cheat_block_timestamp_global('ts5'); + + let mut state = setup_erc721_votes(); + let expiry = 'ts4'; + let signature = array![0, 0]; + + state.delegate_by_sig(OWNER(), RECIPIENT(), 0, expiry, signature); +} + +#[test] +#[should_panic(expected: ('Nonces: invalid nonce',))] +fn test_delegate_by_sig_invalid_nonce() { + let mut state = setup_erc721_votes(); + let signature = array![0, 0]; + + state.delegate_by_sig(OWNER(), RECIPIENT(), 1, 0, signature); +} + +// #[test] +// #[should_panic(expected: ('Votes: invalid signature',))] +// fn test_delegate_by_sig_invalid_signature() { +// let mut state = setup_erc721_votes(); +// // For some reason this is panicking before we get to delegate_by_sig +// let account = setup_account(0x123); +// let signature = array![0, 0]; + +// state.delegate_by_sig(account, RECIPIENT(), 0, 0, signature); +// } + +// +// Tests specific to ERC721Votes and +// + +#[test] +fn test_erc721_get_voting_units() { + let state = setup_erc721_votes(); + + assert_eq!(state.get_voting_units(OWNER()), 10); + assert_eq!(state.get_voting_units(RECIPIENT()), 0); +} + +#[test] +fn test_erc20_get_voting_units() { + let mut state = setup_erc20_votes(); + + assert_eq!(state.get_voting_units(OWNER()), SUPPLY); + assert_eq!(state.get_voting_units(RECIPIENT()), 0); +} + +// +// Helpers +// + +#[generate_trait] +impl VotesSpyHelpersImpl of VotesSpyHelpers { + fn assert_event_delegate_changed( + ref self: EventSpy, + contract: ContractAddress, + delegator: ContractAddress, + from_delegate: ContractAddress, + to_delegate: ContractAddress + ) { + let expected = VotesComponent::Event::DelegateChanged( + DelegateChanged { delegator, from_delegate, to_delegate } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_event_delegate_votes_changed( + ref self: EventSpy, + contract: ContractAddress, + delegate: ContractAddress, + previous_votes: u256, + new_votes: u256 + ) { + let expected = VotesComponent::Event::DelegateVotesChanged( + DelegateVotesChanged { delegate, previous_votes, new_votes } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_delegate_changed( + ref self: EventSpy, + contract: ContractAddress, + delegator: ContractAddress, + from_delegate: ContractAddress, + to_delegate: ContractAddress + ) { + self.assert_event_delegate_changed(contract, delegator, from_delegate, to_delegate); + self.assert_no_events_left_from(contract); + } + + fn assert_only_event_delegate_votes_changed( + ref self: EventSpy, + contract: ContractAddress, + delegate: ContractAddress, + previous_votes: u256, + new_votes: u256 + ) { + self.assert_event_delegate_votes_changed(contract, delegate, previous_votes, new_votes); + self.assert_no_events_left_from(contract); + } +} + From 64461b90f7ddbbbcb300776f5400dd46d2301596 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Wed, 4 Sep 2024 17:37:04 -0400 Subject: [PATCH 06/44] refactor, remove unused usings --- packages/governance/src/votes/votes.cairo | 21 +++++-------------- .../src/erc20/extensions/erc20_votes.cairo | 5 +---- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 47d27a927..3b912fe96 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.15.1 (governance/votes/votes.cairo) -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; -use starknet::ContractAddress; +use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; /// # Votes Component /// @@ -25,7 +22,7 @@ pub mod VotesComponent { use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use openzeppelin_governance::votes::interface::{IVotes, TokenVotesTrait}; - use openzeppelin_governance::votes::utils::{Delegation}; + use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_token::erc20::interface::IERC20; @@ -33,7 +30,7 @@ pub mod VotesComponent { use openzeppelin_token::erc721::interface::IERC721; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; - use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; + use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; use starknet::ContractAddress; use starknet::storage::Map; use super::{OffchainMessageHash, SNIP12Metadata}; @@ -81,7 +78,7 @@ pub mod VotesComponent { TContractState, +HasComponent, impl Nonces: NoncesComponent::HasComponent, - impl TokenTrait: TokenVotesTrait>, + +TokenVotesTrait>, +SNIP12Metadata, +Drop > of IVotes> { @@ -175,7 +172,7 @@ pub mod VotesComponent { } // - // Internal for ERC721Votes + // Internal // pub impl ERC721VotesImpl< @@ -199,10 +196,6 @@ pub mod VotesComponent { } } - // - // Internal for ERC20Votes - // - pub impl ERC20VotesImpl< TContractState, +HasComponent, @@ -223,10 +216,6 @@ pub mod VotesComponent { } } - // - // Common internal for Votes - // - #[generate_trait] pub impl InternalImpl< TContractState, diff --git a/packages/token/src/erc20/extensions/erc20_votes.cairo b/packages/token/src/erc20/extensions/erc20_votes.cairo index 32237f84c..b1fdb633e 100644 --- a/packages/token/src/erc20/extensions/erc20_votes.cairo +++ b/packages/token/src/erc20/extensions/erc20_votes.cairo @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.15.1 (token/erc20/extensions/erc20_votes.cairo) -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, StructHash, SNIP12Metadata}; -use starknet::ContractAddress; +use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; /// # ERC20Votes Component /// From 6f8c2d0174da15fc979070ea19fbc2433b15c30e Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Wed, 4 Sep 2024 22:14:55 -0400 Subject: [PATCH 07/44] cleanup mocks, remove public impl --- .../src/tests/mocks/votes_mocks.cairo | 53 ------------------- packages/governance/src/votes/votes.cairo | 4 +- .../src/tests/erc20/test_erc20_votes.cairo | 2 +- 3 files changed, 3 insertions(+), 56 deletions(-) diff --git a/packages/governance/src/tests/mocks/votes_mocks.cairo b/packages/governance/src/tests/mocks/votes_mocks.cairo index 1d01470ef..7c35bbce4 100644 --- a/packages/governance/src/tests/mocks/votes_mocks.cairo +++ b/packages/governance/src/tests/mocks/votes_mocks.cairo @@ -3,8 +3,6 @@ pub(crate) mod ERC721VotesMock { use openzeppelin_governance::votes::votes::VotesComponent; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc721::ERC721Component; - // This is temporary - we should actually implement the hooks manually - // and transfer the voting units in the hooks. use openzeppelin_token::erc721::ERC721HooksEmptyImpl; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; @@ -17,8 +15,6 @@ pub(crate) mod ERC721VotesMock { //Votes and ERC721Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; - impl InternalImpl = VotesComponent::InternalImpl; - impl ERC721VotesInternalImpl = VotesComponent::ERC721VotesImpl; // ERC721 #[abi(embed_v0)] @@ -64,28 +60,6 @@ pub(crate) mod ERC721VotesMock { } } - // - // Hooks - // - - // impl ERC721VotesHooksImpl< - // TContractState, - // impl Votes: VotesComponent::HasComponent, - // impl HasComponent: VotesComponent::HasComponent, - // +NoncesComponent::HasComponent, - // +Drop - // > of ERC721Component::ERC721HooksTrait { - // fn after_update( - // ref self: ERC721Component::ComponentState, - // to: ContractAddress, - // token_id: u256, - // auth: ContractAddress - // ) { - // let mut erc721_votes_component = get_dep_component_mut!(ref self, Votes); - // erc721_votes_component.transfer_voting_units(auth, to, 1); - // } - // } - #[constructor] fn constructor(ref self: ContractState) { self.erc721.initializer("MyToken", "MTK", ""); @@ -97,8 +71,6 @@ pub(crate) mod ERC20VotesMock { use openzeppelin_governance::votes::votes::VotesComponent; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; - // This is temporary - we should actually implement the hooks manually - // and transfer the voting units in the hooks. use openzeppelin_token::erc20::ERC20HooksEmptyImpl; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; @@ -111,8 +83,6 @@ pub(crate) mod ERC20VotesMock { // Votes and ERC20Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; - impl InternalImpl = VotesComponent::InternalImpl; - impl ERC20VotesInternalImpl = VotesComponent::ERC20VotesImpl; // ERC20 #[abi(embed_v0)] @@ -158,29 +128,6 @@ pub(crate) mod ERC20VotesMock { } } - // - // Hooks - // - - // Uncomment and modify this section if you need ERC20 hooks - // impl ERC20VotesHooksImpl< - // TContractState, - // impl Votes: VotesComponent::HasComponent, - // impl HasComponent: VotesComponent::HasComponent, - // +NoncesComponent::HasComponent, - // +Drop - // > of ERC20Component::ERC20HooksTrait { - // fn after_transfer( - // ref self: ERC20Component::ComponentState, - // from: ContractAddress, - // to: ContractAddress, - // amount: u256 - // ) { - // let mut erc20_votes_component = get_dep_component_mut!(ref self, Votes); - // erc20_votes_component.transfer_voting_units(from, to, amount); - // } - // } - #[constructor] fn constructor(ref self: ContractState) { self.erc20.initializer("MyToken", "MTK"); diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 3b912fe96..79574cab5 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -175,7 +175,7 @@ pub mod VotesComponent { // Internal // - pub impl ERC721VotesImpl< + impl ERC721VotesImpl< TContractState, +HasComponent, +SRC5Component::HasComponent, @@ -196,7 +196,7 @@ pub mod VotesComponent { } } - pub impl ERC20VotesImpl< + impl ERC20VotesImpl< TContractState, +HasComponent, +SRC5Component::HasComponent, diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo index c11e9bf07..16bc80827 100644 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ b/packages/token/src/tests/erc20/test_erc20_votes.cairo @@ -1,6 +1,6 @@ use core::num::traits::Bounded; use core::num::traits::Zero; -use openzeppelin_governance::votes::utils::{Delegation}; +use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; use openzeppelin_testing::events::EventSpyExt; From de076af7da0a803a3059536f3c0b46fa380e8e02 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Fri, 4 Oct 2024 16:41:05 -0400 Subject: [PATCH 08/44] refactor --- Scarb.lock | 2 +- .../src/tests/mocks/votes_mocks.cairo | 6 ---- .../governance/src/tests/test_votes.cairo | 15 ++++------ packages/governance/src/votes/interface.cairo | 4 --- packages/governance/src/votes/votes.cairo | 29 ++++++++++--------- 5 files changed, 23 insertions(+), 33 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index ab2f2fb59..cca249e31 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -62,8 +62,8 @@ dependencies = [ "openzeppelin_account", "openzeppelin_introspection", "openzeppelin_testing", - "openzeppelin_utils", "openzeppelin_token", + "openzeppelin_utils", "snforge_std", ] diff --git a/packages/governance/src/tests/mocks/votes_mocks.cairo b/packages/governance/src/tests/mocks/votes_mocks.cairo index 608280841..89e2d7324 100644 --- a/packages/governance/src/tests/mocks/votes_mocks.cairo +++ b/packages/governance/src/tests/mocks/votes_mocks.cairo @@ -69,7 +69,6 @@ pub(crate) mod ERC721VotesMock { #[starknet::contract] pub(crate) mod ERC20VotesMock { use openzeppelin_governance::votes::votes::VotesComponent; - use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_token::erc20::ERC20HooksEmptyImpl; use openzeppelin_utils::cryptography::nonces::NoncesComponent; @@ -77,7 +76,6 @@ pub(crate) mod ERC20VotesMock { component!(path: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent); component!(path: ERC20Component, storage: erc20, event: ERC20Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); // Votes and ERC20Votes @@ -100,8 +98,6 @@ pub(crate) mod ERC20VotesMock { #[substorage(v0)] pub erc20: ERC20Component::Storage, #[substorage(v0)] - pub src5: SRC5Component::Storage, - #[substorage(v0)] pub nonces: NoncesComponent::Storage } @@ -113,8 +109,6 @@ pub(crate) mod ERC20VotesMock { #[flat] ERC20Event: ERC20Component::Event, #[flat] - SRC5Event: SRC5Component::Event, - #[flat] NoncesEvent: NoncesComponent::Event } diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index ed123778c..b49eee489 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -1,12 +1,12 @@ use openzeppelin_governance::tests::mocks::votes_mocks::ERC721VotesMock::SNIP12MetadataImpl; use openzeppelin_governance::tests::mocks::votes_mocks::{ERC721VotesMock, ERC20VotesMock}; -use openzeppelin_governance::votes::interface::TokenVotesTrait; +use openzeppelin_governance::votes::votes::TokenVotesTrait; use openzeppelin_governance::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; use openzeppelin_governance::votes::votes::VotesComponent; use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; +use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT, OTHER}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_token::erc20::ERC20Component::InternalTrait; use openzeppelin_token::erc721::ERC721Component::{ @@ -51,10 +51,7 @@ fn setup_erc721_votes() -> ComponentState { let mut mock_state = ERC721VOTES_CONTRACT_STATE(); // Mint 10 NFTs to OWNER let mut i: u256 = 0; - loop { - if i >= 10 { - break; - } + while i < 10 { mock_state.erc721.mint(OWNER(), i); // We manually transfer voting units here, since this is usually implemented in the hook state.transfer_voting_units(ZERO(), OWNER(), 1); @@ -70,7 +67,7 @@ fn setup_erc20_votes() -> ERC20ComponentState { // Mint SUPPLY tokens to owner mock_state.erc20.mint(OWNER(), SUPPLY); // We manually transfer voting units here, since this is usually implemented in the hook - state.transfer_voting_units(ZERO(), OWNER(), 1); + state.transfer_voting_units(ZERO(), OWNER(), SUPPLY); state } @@ -242,7 +239,7 @@ fn test_erc721_get_voting_units() { let state = setup_erc721_votes(); assert_eq!(state.get_voting_units(OWNER()), 10); - assert_eq!(state.get_voting_units(RECIPIENT()), 0); + assert_eq!(state.get_voting_units(OTHER()), 0); } #[test] @@ -250,7 +247,7 @@ fn test_erc20_get_voting_units() { let mut state = setup_erc20_votes(); assert_eq!(state.get_voting_units(OWNER()), SUPPLY); - assert_eq!(state.get_voting_units(RECIPIENT()), 0); + assert_eq!(state.get_voting_units(OTHER()), 0); } // diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index 707002b3d..597045720 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -34,7 +34,3 @@ pub trait IVotes { ); } -/// Common interface for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) -pub trait TokenVotesTrait { - fn get_voting_units(self: @TState, account: ContractAddress) -> u256; -} diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 5d6ddba60..0f98eda41 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.15.1 (governance/votes/votes.cairo) -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; +use starknet::ContractAddress; + /// # Votes Component /// @@ -21,21 +22,21 @@ pub mod VotesComponent { // Instead we can rely on Vec use core::num::traits::Zero; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_governance::votes::interface::{IVotes, TokenVotesTrait}; + use openzeppelin_governance::votes::interface::IVotes; use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_token::erc20::interface::IERC20; use openzeppelin_token::erc721::ERC721Component; use openzeppelin_token::erc721::interface::IERC721; + use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; - use starknet::ContractAddress; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess }; - use super::{OffchainMessageHash, SNIP12Metadata}; + use super::{TokenVotesTrait, ContractAddress}; #[storage] pub struct Storage { @@ -193,7 +194,7 @@ pub mod VotesComponent { fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { - let mut erc721_component = get_dep_component!(self, ERC721); + let erc721_component = get_dep_component!(self, ERC721); erc721_component.balance_of(account).into() } } @@ -201,10 +202,8 @@ pub mod VotesComponent { impl ERC20VotesImpl< TContractState, +HasComponent, - +SRC5Component::HasComponent, impl ERC20: ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait, - +Drop + +ERC20Component::ERC20HooksTrait > of TokenVotesTrait> { /// Returns the number of voting units for a given account. /// @@ -213,7 +212,7 @@ pub mod VotesComponent { fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); + let erc20_component = get_dep_component!(self, ERC20); erc20_component.balance_of(account) } } @@ -257,16 +256,15 @@ pub mod VotesComponent { to: ContractAddress, amount: u256 ) { - let zero_address = Zero::zero(); let block_timestamp = starknet::get_block_timestamp(); - if (from != to && amount > 0) { - if (from != zero_address) { + if from != to && amount > 0 { + if from.is_non_zero() { let mut trace = self.Votes_delegate_checkpoints.read(from); let (previous_votes, new_votes) = trace .push(block_timestamp, trace.latest() - amount); self.emit(DelegateVotesChanged { delegate: from, previous_votes, new_votes }); } - if (to != zero_address) { + if to.is_non_zero() { let mut trace = self.Votes_delegate_checkpoints.read(to); let (previous_votes, new_votes) = trace .push(block_timestamp, trace.latest() + amount); @@ -301,3 +299,8 @@ pub mod VotesComponent { } } } + +/// Common trait for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) +pub trait TokenVotesTrait { + fn get_voting_units(self: @TState, account: ContractAddress) -> u256; +} From 6f5a8a677d5c9d0645bd60902c6a8e7cd62b298f Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Fri, 4 Oct 2024 16:49:21 -0400 Subject: [PATCH 09/44] export VotesComponent for easier use --- packages/governance/src/votes.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index 69e5a8028..b66668c46 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -1,3 +1,5 @@ pub mod interface; pub mod utils; pub mod votes; + +pub use votes::VotesComponent; \ No newline at end of file From fefcfeb8b630a1816998d5037fa365a532ee1fab Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Fri, 4 Oct 2024 17:19:54 -0400 Subject: [PATCH 10/44] remove erc20votes from token module --- Scarb.lock | 1 - packages/token/README.md | 1 - packages/token/Scarb.toml | 1 - packages/token/src/erc20.cairo | 1 - packages/token/src/erc20/extensions.cairo | 3 - .../src/erc20/extensions/erc20_votes.cairo | 287 ------------- packages/token/src/erc20/interface.cairo | 40 -- packages/token/src/tests/erc20.cairo | 1 - .../src/tests/erc20/test_erc20_votes.cairo | 406 ------------------ packages/token/src/tests/mocks.cairo | 1 - .../src/tests/mocks/erc20_votes_mocks.cairo | 94 ---- 11 files changed, 836 deletions(-) delete mode 100644 packages/token/src/erc20/extensions.cairo delete mode 100644 packages/token/src/erc20/extensions/erc20_votes.cairo delete mode 100644 packages/token/src/tests/erc20/test_erc20_votes.cairo delete mode 100644 packages/token/src/tests/mocks/erc20_votes_mocks.cairo diff --git a/Scarb.lock b/Scarb.lock index cca249e31..6cf85b600 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -131,7 +131,6 @@ name = "openzeppelin_token" version = "0.17.0" dependencies = [ "openzeppelin_account", - "openzeppelin_governance", "openzeppelin_introspection", "openzeppelin_test_common", "openzeppelin_testing", diff --git a/packages/token/README.md b/packages/token/README.md index 33982f342..920092019 100644 --- a/packages/token/README.md +++ b/packages/token/README.md @@ -15,7 +15,6 @@ standards. #### Components - [`ERC20Component`](https://docs.openzeppelin.com/contracts-cairo/0.17.0/api/erc20#ERC20Component) -- [`ERC20VotesComponent`](https://docs.openzeppelin.com/contracts-cairo/0.17.0/api/erc20#ERC20VotesComponent) ### ERC721 diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 158519166..e83f10a7f 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -26,7 +26,6 @@ fmt.workspace = true starknet.workspace = true openzeppelin_account = { path = "../account" } openzeppelin_introspection = { path = "../introspection" } -openzeppelin_governance = { path = "../governance" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index e40fbb2bb..3f63977d5 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -1,6 +1,5 @@ pub mod dual20; pub mod erc20; -pub mod extensions; pub mod interface; pub use erc20::{ERC20Component, ERC20HooksEmptyImpl}; diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo deleted file mode 100644 index e45cdcbf3..000000000 --- a/packages/token/src/erc20/extensions.cairo +++ /dev/null @@ -1,3 +0,0 @@ -pub mod erc20_votes; - -pub use erc20_votes::ERC20VotesComponent; diff --git a/packages/token/src/erc20/extensions/erc20_votes.cairo b/packages/token/src/erc20/extensions/erc20_votes.cairo deleted file mode 100644 index de52e9814..000000000 --- a/packages/token/src/erc20/extensions/erc20_votes.cairo +++ /dev/null @@ -1,287 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc20_votes.cairo) - -use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; - -/// # ERC20Votes Component -/// -/// The ERC20Votes component tracks voting units from ERC20 balances, which are a measure of voting -/// power that can be transferred, and provides a system of vote delegation, where an account can -/// delegate its voting units to a sort of "representative" that will pool delegated voting units -/// from different accounts and can then use it to vote in decisions. In fact, voting units MUST be -/// delegated in order to count as actual votes, and an account has to delegate those votes to -/// itself if it wishes to participate in decisions and does not have a trusted representative. -#[starknet::component] -pub mod ERC20VotesComponent { - use core::num::traits::Zero; - use crate::erc20::ERC20Component; - use crate::erc20::interface::IERC20; - use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_governance::votes::interface::IVotes; - use openzeppelin_governance::votes::utils::{Delegation}; - use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; - use openzeppelin_utils::nonces::NoncesComponent; - use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; - use starknet::ContractAddress; - use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess - }; - use super::{OffchainMessageHash, SNIP12Metadata}; - - #[storage] - pub struct Storage { - pub ERC20Votes_delegatee: Map, - pub ERC20Votes_delegate_checkpoints: Map, - pub ERC20Votes_total_checkpoints: Trace - } - - #[event] - #[derive(Drop, PartialEq, starknet::Event)] - pub enum Event { - DelegateChanged: DelegateChanged, - DelegateVotesChanged: DelegateVotesChanged, - } - - /// Emitted when `delegator` delegates their votes from `from_delegate` to `to_delegate`. - #[derive(Drop, PartialEq, starknet::Event)] - pub struct DelegateChanged { - #[key] - pub delegator: ContractAddress, - #[key] - pub from_delegate: ContractAddress, - #[key] - pub to_delegate: ContractAddress - } - - /// Emitted when `delegate` votes are updated from `previous_votes` to `new_votes`. - #[derive(Drop, PartialEq, starknet::Event)] - pub struct DelegateVotesChanged { - #[key] - pub delegate: ContractAddress, - pub previous_votes: u256, - pub new_votes: u256 - } - - pub mod Errors { - pub const FUTURE_LOOKUP: felt252 = 'Votes: future Lookup'; - pub const EXPIRED_SIGNATURE: felt252 = 'Votes: expired signature'; - pub const INVALID_SIGNATURE: felt252 = 'Votes: invalid signature'; - } - - #[embeddable_as(ERC20VotesImpl)] - impl ERC20Votes< - TContractState, - +HasComponent, - +ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait, - impl Nonces: NoncesComponent::HasComponent, - +SNIP12Metadata, - +Drop - > of IVotes> { - /// Returns the current amount of votes that `account` has. - fn get_votes(self: @ComponentState, account: ContractAddress) -> u256 { - self.ERC20Votes_delegate_checkpoints.read(account).latest() - } - - /// Returns the amount of votes that `account` had at a specific moment in the past. - /// - /// Requirements: - /// - /// - `timepoint` must be in the past. - fn get_past_votes( - self: @ComponentState, account: ContractAddress, timepoint: u64 - ) -> u256 { - let current_timepoint = starknet::get_block_timestamp(); - assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); - - self.ERC20Votes_delegate_checkpoints.read(account).upper_lookup_recent(timepoint) - } - - /// Returns the total supply of votes available at a specific moment in the past. - /// - /// Requirements: - /// - /// - `timepoint` must be in the past. - /// - /// NOTE: This value is the sum of all available votes, which is not necessarily the sum of - /// all delegated votes. - /// Votes that have not been delegated are still part of total supply, even though they - /// would not participate in a vote. - fn get_past_total_supply(self: @ComponentState, timepoint: u64) -> u256 { - let current_timepoint = starknet::get_block_timestamp(); - assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); - - self.ERC20Votes_total_checkpoints.read().upper_lookup_recent(timepoint) - } - - /// Returns the delegate that `account` has chosen. - fn delegates( - self: @ComponentState, account: ContractAddress - ) -> ContractAddress { - self.ERC20Votes_delegatee.read(account) - } - - /// Delegates votes from the sender to `delegatee`. - /// - /// Emits a `DelegateChanged` event. - /// May emit one or two `DelegateVotesChanged` events. - fn delegate(ref self: ComponentState, delegatee: ContractAddress) { - let sender = starknet::get_caller_address(); - self._delegate(sender, delegatee); - } - - /// Delegates votes from the sender to `delegatee` through a SNIP12 message signature - /// validation. - /// - /// Requirements: - /// - /// - `expiry` must not be in the past. - /// - `nonce` must match the account's current nonce. - /// - `delegator` must implement `SRC6::is_valid_signature`. - /// - `signature` should be valid for the message hash. - /// - /// Emits a `DelegateChanged` event. - /// May emit one or two `DelegateVotesChanged` events. - fn delegate_by_sig( - ref self: ComponentState, - delegator: ContractAddress, - delegatee: ContractAddress, - nonce: felt252, - expiry: u64, - signature: Array - ) { - assert(starknet::get_block_timestamp() <= expiry, Errors::EXPIRED_SIGNATURE); - - // Check and increase nonce. - let mut nonces_component = get_dep_component_mut!(ref self, Nonces); - nonces_component.use_checked_nonce(delegator, nonce); - - // Build hash for calling `is_valid_signature`. - let delegation = Delegation { delegatee, nonce, expiry }; - let hash = delegation.get_message_hash(delegator); - - let is_valid_signature_felt = DualCaseAccount { contract_address: delegator } - .is_valid_signature(hash, signature); - - // Check either 'VALID' or true for backwards compatibility. - let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED - || is_valid_signature_felt == 1; - - assert(is_valid_signature, Errors::INVALID_SIGNATURE); - - // Delegate votes. - self._delegate(delegator, delegatee); - } - } - - // - // Internal - // - - #[generate_trait] - pub impl InternalImpl< - TContractState, - +HasComponent, - impl ERC20: ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait, - +NoncesComponent::HasComponent, - +SNIP12Metadata, - +Drop - > of InternalTrait { - /// Returns the current total supply of votes. - fn get_total_supply(self: @ComponentState) -> u256 { - self.ERC20Votes_total_checkpoints.read().latest() - } - - /// Delegates all of `account`'s voting units to `delegatee`. - /// - /// Emits a `DelegateChanged` event. - /// May emit one or two `DelegateVotesChanged` events. - fn _delegate( - ref self: ComponentState, - account: ContractAddress, - delegatee: ContractAddress - ) { - let from_delegate = self.delegates(account); - self.ERC20Votes_delegatee.write(account, delegatee); - - self - .emit( - DelegateChanged { delegator: account, from_delegate, to_delegate: delegatee } - ); - self.move_delegate_votes(from_delegate, delegatee, self.get_voting_units(account)); - } - - /// Moves delegated votes from one delegate to another. - /// - /// May emit one or two `DelegateVotesChanged` events. - fn move_delegate_votes( - ref self: ComponentState, - from: ContractAddress, - to: ContractAddress, - amount: u256 - ) { - let zero_address = Zero::zero(); - let block_timestamp = starknet::get_block_timestamp(); - if (from != to && amount > 0) { - if (from != zero_address) { - let mut trace = self.ERC20Votes_delegate_checkpoints.read(from); - let (previous_votes, new_votes) = trace - .push(block_timestamp, trace.latest() - amount); - self.emit(DelegateVotesChanged { delegate: from, previous_votes, new_votes }); - } - if (to != zero_address) { - let mut trace = self.ERC20Votes_delegate_checkpoints.read(to); - let (previous_votes, new_votes) = trace - .push(block_timestamp, trace.latest() + amount); - self.emit(DelegateVotesChanged { delegate: to, previous_votes, new_votes }); - } - } - } - - /// Transfers, mints, or burns voting units. - /// - /// To register a mint, `from` should be zero. To register a burn, `to` - /// should be zero. Total supply of voting units will be adjusted with mints and burns. - /// - /// May emit one or two `DelegateVotesChanged` events. - fn transfer_voting_units( - ref self: ComponentState, - from: ContractAddress, - to: ContractAddress, - amount: u256 - ) { - let zero_address = Zero::zero(); - let block_timestamp = starknet::get_block_timestamp(); - if (from == zero_address) { - let mut trace = self.ERC20Votes_total_checkpoints.read(); - trace.push(block_timestamp, trace.latest() + amount); - } - if (to == zero_address) { - let mut trace = self.ERC20Votes_total_checkpoints.read(); - trace.push(block_timestamp, trace.latest() - amount); - } - self.move_delegate_votes(self.delegates(from), self.delegates(to), amount); - } - - /// Returns the number of checkpoints for `account`. - fn num_checkpoints(self: @ComponentState, account: ContractAddress) -> u32 { - self.ERC20Votes_delegate_checkpoints.read(account).length() - } - - /// Returns the `pos`-th checkpoint for `account`. - fn checkpoints( - self: @ComponentState, account: ContractAddress, pos: u32 - ) -> Checkpoint { - self.ERC20Votes_delegate_checkpoints.read(account).at(pos) - } - - /// Returns the voting units of an `account`. - fn get_voting_units( - self: @ComponentState, account: ContractAddress - ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); - erc20_component.balance_of(account) - } - } -} diff --git a/packages/token/src/erc20/interface.cairo b/packages/token/src/erc20/interface.cairo index 811eb62fc..6ad506ad6 100644 --- a/packages/token/src/erc20/interface.cairo +++ b/packages/token/src/erc20/interface.cairo @@ -67,43 +67,3 @@ pub trait ERC20ABI { ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 ) -> bool; } - -#[starknet::interface] -pub trait ERC20VotesABI { - // IERC20 - fn total_supply(self: @TState) -> u256; - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; - - // IERC20Metadata - fn name(self: @TState) -> ByteArray; - fn symbol(self: @TState) -> ByteArray; - fn decimals(self: @TState) -> u8; - - // 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: Array - ); - - // IERC20CamelOnly - fn totalSupply(self: @TState) -> u256; - fn balanceOf(self: @TState, account: ContractAddress) -> u256; - fn transferFrom( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; -} diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index 213861a92..203942da6 100644 --- a/packages/token/src/tests/erc20.cairo +++ b/packages/token/src/tests/erc20.cairo @@ -1,3 +1,2 @@ mod test_dual20; mod test_erc20; -mod test_erc20_votes; diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo deleted file mode 100644 index 775ccc10c..000000000 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ /dev/null @@ -1,406 +0,0 @@ -use core::num::traits::Bounded; -use core::num::traits::Zero; -use openzeppelin_governance::votes::utils::Delegation; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; -use openzeppelin_testing::events::EventSpyExt; -use openzeppelin_token::erc20::ERC20Component::InternalImpl as ERC20Impl; -use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ - DelegateChanged, DelegateVotesChanged -}; -use openzeppelin_token::erc20::extensions::ERC20VotesComponent::{ERC20VotesImpl, InternalImpl}; -use openzeppelin_token::erc20::extensions::ERC20VotesComponent; -use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock::SNIP12MetadataImpl; -use openzeppelin_token::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock; -use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; -use openzeppelin_utils::structs::checkpoint::{Checkpoint, TraceTrait}; -use snforge_std::EventSpy; -use snforge_std::signature::KeyPairTrait; -use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; -use snforge_std::{ - start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, - start_cheat_chain_id_global, test_address -}; -use starknet::storage::{StorageMapReadAccess, StoragePointerReadAccess}; -use starknet::{ContractAddress, contract_address_const}; - -// -// Setup -// - -type ComponentState = ERC20VotesComponent::ComponentState; - -fn CONTRACT_STATE() -> DualCaseERC20VotesMock::ContractState { - DualCaseERC20VotesMock::contract_state_for_testing() -} -fn COMPONENT_STATE() -> ComponentState { - ERC20VotesComponent::component_state_for_testing() -} - -fn setup() -> ComponentState { - let mut state = COMPONENT_STATE(); - let mut mock_state = CONTRACT_STATE(); - - mock_state.erc20.mint(OWNER(), SUPPLY); - state.transfer_voting_units(ZERO(), OWNER(), SUPPLY); - state -} - -fn setup_account(public_key: felt252) -> ContractAddress { - let mut calldata = array![public_key]; - utils::declare_and_deploy("DualCaseAccountMock", calldata) -} - -// Checkpoints unordered insertion - -#[test] -#[should_panic(expected: ('Unordered insertion',))] -fn test__delegate_checkpoints_unordered_insertion() { - let mut state = setup(); - let mut trace = state.ERC20Votes_delegate_checkpoints.read(OWNER()); - - start_cheat_block_timestamp_global('ts10'); - trace.push('ts2', 0x222); - trace.push('ts1', 0x111); -} - -#[test] -#[should_panic(expected: ('Unordered insertion',))] -fn test__total_checkpoints_unordered_insertion() { - let mut state = setup(); - let mut trace = state.ERC20Votes_total_checkpoints.read(); - - start_cheat_block_timestamp_global('ts10'); - trace.push('ts2', 0x222); - trace.push('ts1', 0x111); -} - -// -// get_votes && get_past_votes -// - -#[test] -fn test_get_votes() { - let mut state = setup(); - start_cheat_caller_address(test_address(), OWNER()); - state.delegate(OWNER()); - - assert_eq!(state.get_votes(OWNER()), SUPPLY); -} - -#[test] -fn test_get_past_votes() { - let mut state = setup(); - let mut trace = state.ERC20Votes_delegate_checkpoints.read(OWNER()); - - // Future timestamp. - start_cheat_block_timestamp_global('ts10'); - trace.push('ts1', 0x111); - trace.push('ts2', 0x222); - trace.push('ts3', 0x333); - - // Big numbers (high different from 0x0) - let big1: u256 = Bounded::::MAX.into() + 0x444; - let big2: u256 = Bounded::::MAX.into() + 0x666; - let big3: u256 = Bounded::::MAX.into() + 0x888; - trace.push('ts4', big1); - trace.push('ts6', big2); - trace.push('ts8', big3); - - assert_eq!(state.get_past_votes(OWNER(), 'ts2'), 0x222, "Should eq ts2"); - assert_eq!(state.get_past_votes(OWNER(), 'ts5'), big1, "Should eq ts4"); - assert_eq!(state.get_past_votes(OWNER(), 'ts8'), big3, "Should eq ts8"); -} - -#[test] -#[should_panic(expected: ('Votes: future Lookup',))] -fn test_get_past_votes_future_lookup() { - let state = setup(); - - // Past timestamp. - start_cheat_block_timestamp_global('ts1'); - state.get_past_votes(OWNER(), 'ts2'); -} - -#[test] -fn test_get_past_total_supply() { - let mut state = setup(); - let mut trace = state.ERC20Votes_total_checkpoints.read(); - - // Future timestamp. - start_cheat_block_timestamp_global('ts10'); - trace.push('ts1', 0x111); - trace.push('ts2', 0x222); - trace.push('ts3', 0x333); - - // Big numbers (high different from 0x0) - let big1: u256 = Bounded::::MAX.into() + 0x444; - let big2: u256 = Bounded::::MAX.into() + 0x666; - let big3: u256 = Bounded::::MAX.into() + 0x888; - trace.push('ts4', big1); - trace.push('ts6', big2); - trace.push('ts8', big3); - - assert_eq!(state.get_past_total_supply('ts2'), 0x222, "Should eq ts2"); - assert_eq!(state.get_past_total_supply('ts5'), big1, "Should eq ts4"); - assert_eq!(state.get_past_total_supply('ts8'), big3, "Should eq ts8"); -} - -#[test] -#[should_panic(expected: ('Votes: future Lookup',))] -fn test_get_past_total_supply_future_lookup() { - let state = setup(); - - // Past timestamp. - start_cheat_block_timestamp_global('ts1'); - state.get_past_total_supply('ts2'); -} - -// -// delegate & delegates -// - -#[test] -fn test_delegate() { - let mut state = setup(); - let contract_address = test_address(); - let mut spy = spy_events(); - - start_cheat_caller_address(contract_address, OWNER()); - - // Delegate from zero - state.delegate(OWNER()); - - spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), OWNER()); - spy.assert_only_event_delegate_votes_changed(contract_address, OWNER(), 0, SUPPLY); - assert_eq!(state.get_votes(OWNER()), SUPPLY); - - // Delegate from non-zero to non-zero - state.delegate(RECIPIENT()); - - spy.assert_event_delegate_changed(contract_address, OWNER(), OWNER(), RECIPIENT()); - spy.assert_event_delegate_votes_changed(contract_address, OWNER(), SUPPLY, 0); - spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, SUPPLY); - assert!(state.get_votes(OWNER()).is_zero()); - assert_eq!(state.get_votes(RECIPIENT()), SUPPLY); - - // Delegate to zero - state.delegate(ZERO()); - - spy.assert_event_delegate_changed(contract_address, OWNER(), RECIPIENT(), ZERO()); - spy.assert_event_delegate_votes_changed(contract_address, RECIPIENT(), SUPPLY, 0); - assert!(state.get_votes(RECIPIENT()).is_zero()); - - // Delegate from zero to zero - state.delegate(ZERO()); - - spy.assert_only_event_delegate_changed(contract_address, OWNER(), ZERO(), ZERO()); -} - -#[test] -fn test_delegates() { - let mut state = setup(); - start_cheat_caller_address(test_address(), OWNER()); - - state.delegate(OWNER()); - assert_eq!(state.delegates(OWNER()), OWNER()); - - state.delegate(RECIPIENT()); - assert_eq!(state.delegates(OWNER()), RECIPIENT()); -} - -// delegate_by_sig - -#[test] -fn test_delegate_by_sig_hash_generation() { - start_cheat_chain_id_global('SN_TEST'); - - let nonce = 0; - let expiry = 'ts2'; - let delegator = contract_address_const::< - 0x70b0526a4bfbc9ca717c96aeb5a8afac85181f4585662273668928585a0d628 - >(); - let delegatee = RECIPIENT(); - let delegation = Delegation { delegatee, nonce, expiry }; - - let hash = delegation.get_message_hash(delegator); - - // This hash was computed using starknet js sdk from the following values: - // - name: 'DAPP_NAME' - // - version: 'DAPP_VERSION' - // - chainId: 'SN_TEST' - // - account: 0x70b0526a4bfbc9ca717c96aeb5a8afac85181f4585662273668928585a0d628 - // - delegatee: 'RECIPIENT' - // - nonce: 0 - // - expiry: 'ts2' - // - revision: '1' - let expected_hash = 0x314bd38b22b62d576691d8dafd9f8ea0601329ebe686bc64ca28e4d8821d5a0; - assert_eq!(hash, expected_hash); -} - -#[test] -fn test_delegate_by_sig() { - start_cheat_chain_id_global('SN_TEST'); - start_cheat_block_timestamp_global('ts1'); - - let mut state = setup(); - let contract_address = test_address(); - let key_pair = KeyPairTrait::::generate(); - let account = setup_account(key_pair.public_key); - - let nonce = 0; - let expiry = 'ts2'; - let delegator = account; - let delegatee = RECIPIENT(); - - let delegation = Delegation { delegatee, nonce, expiry }; - let msg_hash = delegation.get_message_hash(delegator); - let (r, s) = key_pair.sign(msg_hash).unwrap(); - - let mut spy = spy_events(); - - state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); - - spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); - assert_eq!(state.delegates(account), delegatee); -} - -#[test] -#[should_panic(expected: ('Votes: expired signature',))] -fn test_delegate_by_sig_past_expiry() { - start_cheat_block_timestamp_global('ts5'); - - let mut state = setup(); - let expiry = 'ts4'; - let signature = array![0, 0]; - - state.delegate_by_sig(OWNER(), RECIPIENT(), 0, expiry, signature); -} - -#[test] -#[should_panic(expected: ('Nonces: invalid nonce',))] -fn test_delegate_by_sig_invalid_nonce() { - let mut state = setup(); - let signature = array![0, 0]; - - state.delegate_by_sig(OWNER(), RECIPIENT(), 1, 0, signature); -} - -#[test] -#[should_panic(expected: ('Votes: invalid signature',))] -fn test_delegate_by_sig_invalid_signature() { - let mut state = setup(); - let account = setup_account(0x123); - let signature = array![0, 0]; - - state.delegate_by_sig(account, RECIPIENT(), 0, 0, signature); -} - -// -// num_checkpoints & checkpoints -// - -#[test] -fn test_num_checkpoints() { - let state = @setup(); - let mut trace = state.ERC20Votes_delegate_checkpoints.read(OWNER()); - - trace.push('ts1', 0x111); - trace.push('ts2', 0x222); - trace.push('ts3', 0x333); - trace.push('ts4', 0x444); - assert_eq!(state.num_checkpoints(OWNER()), 4); - - trace.push('ts5', 0x555); - trace.push('ts6', 0x666); - assert_eq!(state.num_checkpoints(OWNER()), 6); - - assert!(state.num_checkpoints(RECIPIENT()).is_zero()); -} - -#[test] -fn test_checkpoints() { - let state = @setup(); - let mut trace = state.ERC20Votes_delegate_checkpoints.read(OWNER()); - - trace.push('ts1', 0x111); - trace.push('ts2', 0x222); - trace.push('ts3', 0x333); - trace.push('ts4', 0x444); - - let checkpoint: Checkpoint = state.checkpoints(OWNER(), 2); - assert_eq!(checkpoint.key, 'ts3'); - assert_eq!(checkpoint.value, 0x333); -} - -#[test] -#[should_panic(expected: ('Array overflow',))] -fn test__checkpoints_array_overflow() { - let state = setup(); - state.checkpoints(OWNER(), 1); -} - -// -// get_voting_units -// - -#[test] -fn test_get_voting_units() { - let state = setup(); - assert_eq!(state.get_voting_units(OWNER()), SUPPLY); -} - -// -// Helpers -// - -#[generate_trait] -impl VotesSpyHelpersImpl of VotesSpyHelpers { - fn assert_event_delegate_changed( - ref self: EventSpy, - contract: ContractAddress, - delegator: ContractAddress, - from_delegate: ContractAddress, - to_delegate: ContractAddress - ) { - let expected = ERC20VotesComponent::Event::DelegateChanged( - DelegateChanged { delegator, from_delegate, to_delegate } - ); - self.assert_emitted_single(contract, expected); - } - - fn assert_only_event_delegate_changed( - ref self: EventSpy, - contract: ContractAddress, - delegator: ContractAddress, - from_delegate: ContractAddress, - to_delegate: ContractAddress - ) { - self.assert_event_delegate_changed(contract, delegator, from_delegate, to_delegate); - self.assert_no_events_left_from(contract); - } - - fn assert_event_delegate_votes_changed( - ref self: EventSpy, - contract: ContractAddress, - delegate: ContractAddress, - previous_votes: u256, - new_votes: u256 - ) { - let expected = ERC20VotesComponent::Event::DelegateVotesChanged( - DelegateVotesChanged { delegate, previous_votes, new_votes } - ); - self.assert_emitted_single(contract, expected); - } - - fn assert_only_event_delegate_votes_changed( - ref self: EventSpy, - contract: ContractAddress, - delegate: ContractAddress, - previous_votes: u256, - new_votes: u256 - ) { - self.assert_event_delegate_votes_changed(contract, delegate, previous_votes, new_votes); - self.assert_no_events_left_from(contract); - } -} diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 9574a4b7a..849962bbd 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -2,7 +2,6 @@ pub(crate) mod account_mocks; pub(crate) mod erc1155_mocks; pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; -pub(crate) mod erc20_votes_mocks; pub(crate) mod erc2981_mocks; pub(crate) mod erc721_enumerable_mocks; pub(crate) mod erc721_mocks; diff --git a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo b/packages/token/src/tests/mocks/erc20_votes_mocks.cairo deleted file mode 100644 index c777ec62f..000000000 --- a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo +++ /dev/null @@ -1,94 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC20VotesMock { - use crate::erc20::ERC20Component; - use crate::erc20::extensions::ERC20VotesComponent::InternalTrait as ERC20VotesInternalTrait; - use crate::erc20::extensions::ERC20VotesComponent; - use openzeppelin_utils::cryptography::nonces::NoncesComponent; - use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; - use starknet::ContractAddress; - - component!(path: ERC20VotesComponent, storage: erc20_votes, event: ERC20VotesEvent); - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); - - // ERC20Votes - #[abi(embed_v0)] - impl ERC20VotesComponentImpl = - ERC20VotesComponent::ERC20VotesImpl; - - // ERC20Mixin - #[abi(embed_v0)] - impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; - impl InternalImpl = ERC20Component::InternalImpl; - - // Nonces - #[abi(embed_v0)] - impl NoncesImpl = NoncesComponent::NoncesImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20_votes: ERC20VotesComponent::Storage, - #[substorage(v0)] - pub erc20: ERC20Component::Storage, - #[substorage(v0)] - pub nonces: NoncesComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20VotesEvent: ERC20VotesComponent::Event, - #[flat] - ERC20Event: ERC20Component::Event, - #[flat] - NoncesEvent: NoncesComponent::Event - } - - /// Required for hash computation. - pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata { - fn name() -> felt252 { - 'DAPP_NAME' - } - fn version() -> felt252 { - 'DAPP_VERSION' - } - } - - // - // Hooks - // - - impl ERC20VotesHooksImpl< - TContractState, - impl ERC20Votes: ERC20VotesComponent::HasComponent, - impl HasComponent: ERC20Component::HasComponent, - +NoncesComponent::HasComponent, - +Drop - > of ERC20Component::ERC20HooksTrait { - fn after_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) { - let mut erc20_votes_component = get_dep_component_mut!(ref self, ERC20Votes); - erc20_votes_component.transfer_voting_units(from, recipient, amount); - } - } - - /// Sets the token `name` and `symbol`. - /// Mints `fixed_supply` tokens to `recipient`. - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - fixed_supply: u256, - recipient: ContractAddress - ) { - self.erc20.initializer(name, symbol); - self.erc20.mint(recipient, fixed_supply); - } -} From 0116f0385405340a5be7a653d80fc38dce29cc5b Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Fri, 4 Oct 2024 19:44:09 -0400 Subject: [PATCH 11/44] fmt --- packages/governance/src/votes.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index b66668c46..93d7040ab 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -2,4 +2,4 @@ pub mod interface; pub mod utils; pub mod votes; -pub use votes::VotesComponent; \ No newline at end of file +pub use votes::VotesComponent; From 175af4e4034490ecdc3198895ff839a8335c68f0 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sat, 5 Oct 2024 19:59:55 -0400 Subject: [PATCH 12/44] add more tests --- packages/governance/src/tests/mocks.cairo | 1 + .../src/tests/mocks/account_mocks.cairo | 145 ++++++++++++++++++ .../governance/src/tests/test_votes.cairo | 104 ++++++++----- 3 files changed, 209 insertions(+), 41 deletions(-) create mode 100644 packages/governance/src/tests/mocks/account_mocks.cairo diff --git a/packages/governance/src/tests/mocks.cairo b/packages/governance/src/tests/mocks.cairo index 72e9e6b05..46c829072 100644 --- a/packages/governance/src/tests/mocks.cairo +++ b/packages/governance/src/tests/mocks.cairo @@ -1,3 +1,4 @@ pub(crate) mod non_implementing_mock; pub(crate) mod timelock_mocks; pub(crate) mod votes_mocks; +pub(crate) mod account_mocks; diff --git a/packages/governance/src/tests/mocks/account_mocks.cairo b/packages/governance/src/tests/mocks/account_mocks.cairo new file mode 100644 index 000000000..f8f222dad --- /dev/null +++ b/packages/governance/src/tests/mocks/account_mocks.cairo @@ -0,0 +1,145 @@ +#[starknet::contract(account)] +pub(crate) mod DualCaseAccountMock { + use openzeppelin_account::AccountComponent; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Account + #[abi(embed_v0)] + impl SRC6Impl = AccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl SRC6CamelOnlyImpl = AccountComponent::SRC6CamelOnlyImpl; + #[abi(embed_v0)] + impl DeclarerImpl = AccountComponent::DeclarerImpl; + #[abi(embed_v0)] + impl DeployableImpl = AccountComponent::DeployableImpl; + impl AccountInternalImpl = AccountComponent::InternalImpl; + + // SCR5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub account: AccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } +} + +#[starknet::contract(account)] +pub(crate) mod SnakeAccountMock { + use openzeppelin_account::AccountComponent; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Account + #[abi(embed_v0)] + impl SRC6Impl = AccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl PublicKeyImpl = AccountComponent::PublicKeyImpl; + impl AccountInternalImpl = AccountComponent::InternalImpl; + + // SCR5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub account: AccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } +} + +#[starknet::contract(account)] +pub(crate) mod CamelAccountMock { + use openzeppelin_account::AccountComponent; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::account::Call; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Account + #[abi(embed_v0)] + impl SRC6CamelOnlyImpl = AccountComponent::SRC6CamelOnlyImpl; + #[abi(embed_v0)] + impl PublicKeyCamelImpl = AccountComponent::PublicKeyCamelImpl; + impl SRC6Impl = AccountComponent::SRC6Impl; + impl AccountInternalImpl = AccountComponent::InternalImpl; + + // SCR5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub account: AccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, publicKey: felt252) { + self.account.initializer(publicKey); + } + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn __execute__(self: @ContractState, mut calls: Array) -> Array> { + self.account.__execute__(calls) + } + + #[external(v0)] + fn __validate__(self: @ContractState, mut calls: Array) -> felt252 { + self.account.__validate__(calls) + } + } +} diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index b49eee489..5a9fc8c8a 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -5,6 +5,7 @@ use openzeppelin_governance::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; use openzeppelin_governance::votes::votes::VotesComponent; +use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT, OTHER}; use openzeppelin_testing::events::EventSpyExt; @@ -14,6 +15,7 @@ use openzeppelin_token::erc721::ERC721Component::{ }; use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; use openzeppelin_utils::structs::checkpoint::TraceTrait; +use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; use snforge_std::{ start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address @@ -22,6 +24,7 @@ use snforge_std::{EventSpy}; use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StorageMapReadAccess}; +const ERC_721_INITIAL_MINT: u256 = 10; // // Setup @@ -49,9 +52,9 @@ fn ERC20VOTES_CONTRACT_STATE() -> ERC20VotesMock::ContractState { fn setup_erc721_votes() -> ComponentState { let mut state = COMPONENT_STATE(); let mut mock_state = ERC721VOTES_CONTRACT_STATE(); - // Mint 10 NFTs to OWNER + // Mint ERC_721_INITIAL_MINT NFTs to OWNER let mut i: u256 = 0; - while i < 10 { + while i < ERC_721_INITIAL_MINT { mock_state.erc721.mint(OWNER(), i); // We manually transfer voting units here, since this is usually implemented in the hook state.transfer_voting_units(ZERO(), OWNER(), 1); @@ -74,7 +77,7 @@ fn setup_erc20_votes() -> ERC20ComponentState { fn setup_account(public_key: felt252) -> ContractAddress { let mut calldata = array![public_key]; - utils::declare_and_deploy("DualCaseAccountMock", calldata) + utils::declare_and_deploy("SnakeAccountMock", calldata) } // @@ -89,7 +92,7 @@ fn test_get_votes() { assert_eq!(state.get_votes(OWNER()), 0); state.delegate(OWNER()); - assert_eq!(state.get_votes(OWNER()), 10); + assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); } // This test can be improved by using the api of the component @@ -114,6 +117,7 @@ fn test_get_past_votes() { #[should_panic(expected: ('Votes: future Lookup',))] fn test_get_past_votes_future_lookup() { let state = setup_erc721_votes(); + start_cheat_block_timestamp_global('ts1'); state.get_past_votes(OWNER(), 'ts2'); } @@ -149,8 +153,8 @@ fn test_self_delegate() { state.delegate(OWNER()); spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), OWNER()); - spy.assert_only_event_delegate_votes_changed(contract_address, OWNER(), 0, 10); - assert_eq!(state.get_votes(OWNER()), 10); + spy.assert_only_event_delegate_votes_changed(contract_address, OWNER(), 0, ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); } #[test] @@ -162,8 +166,8 @@ fn test_delegate_to_recipient_updates_votes() { state.delegate(RECIPIENT()); spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), RECIPIENT()); - spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, 10); - assert_eq!(state.get_votes(RECIPIENT()), 10); + spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(RECIPIENT()), ERC_721_INITIAL_MINT); assert_eq!(state.get_votes(OWNER()), 0); } @@ -177,26 +181,36 @@ fn test_delegate_to_recipient_updates_delegates() { assert_eq!(state.delegates(OWNER()), RECIPIENT()); } -// #[test] -// fn test_delegate_by_sig() { -// cheat_chain_id_global('SN_TEST'); -// start_cheat_block_timestamp_global('ts1'); -// let mut state = setup_erc721_votes(); -// let contract_address = test_address(); -// let key_pair = KeyPairTrait::generate(); -// let account = setup_account(key_pair.public_key); -// let nonce = 0; -// let expiry = 'ts2'; -// let delegator = account; -// let delegatee = RECIPIENT(); -// let delegation = Delegation { delegatee, nonce, expiry }; -// let msg_hash = delegation.get_message_hash(delegator); -// let (r, s) = key_pair.sign(msg_hash).unwrap(); -// let mut spy = spy_events(); -// state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); -// spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); -// assert_eq!(state.delegates(account), delegatee); -// } +#[test] +fn test_delegate_by_sig() { + // Set up the state + // start_cheat_chain_id_global('SN_TEST'); + let mut state = setup_erc721_votes(); + let contract_address = test_address(); + start_cheat_block_timestamp_global('ts1'); + + // Generate a key pair and set up an account + let key_pair = StarkCurveKeyPairImpl::generate(); + let account = setup_account(key_pair.public_key); + + // Set up delegation parameters + let nonce = 0; + let expiry = 'ts2'; + let delegator = account; + let delegatee = RECIPIENT(); + + // Create and sign the delegation message + let delegation = Delegation { delegatee, nonce, expiry }; + let msg_hash = delegation.get_message_hash(delegator); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + // Set up event spy and execute delegation + let mut spy = spy_events(); + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); + + spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); + assert_eq!(state.delegates(account), delegatee); +} #[test] #[should_panic(expected: ('Votes: expired signature',))] @@ -219,16 +233,25 @@ fn test_delegate_by_sig_invalid_nonce() { state.delegate_by_sig(OWNER(), RECIPIENT(), 1, 0, signature); } -// #[test] -// #[should_panic(expected: ('Votes: invalid signature',))] -// fn test_delegate_by_sig_invalid_signature() { -// let mut state = setup_erc721_votes(); -// // For some reason this is panicking before we get to delegate_by_sig -// let account = setup_account(0x123); -// let signature = array![0, 0]; - -// state.delegate_by_sig(account, RECIPIENT(), 0, 0, signature); -// } +#[test] +#[should_panic(expected: ('Votes: invalid signature',))] +fn test_delegate_by_sig_invalid_signature() { + let mut state = setup_erc721_votes(); + let key_pair = StarkCurveKeyPairImpl::generate(); + let account = setup_account(key_pair.public_key); + + let nonce = 0; + let expiry = 'ts2'; + let delegator = account; + let delegatee = RECIPIENT(); + let delegation = Delegation { delegatee, nonce, expiry }; + let msg_hash = delegation.get_message_hash(delegator); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + start_cheat_block_timestamp_global('ts1'); + // Use an invalid signature + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r + 1, s]); +} // // Tests specific to ERC721Votes and @@ -238,7 +261,7 @@ fn test_delegate_by_sig_invalid_nonce() { fn test_erc721_get_voting_units() { let state = setup_erc721_votes(); - assert_eq!(state.get_voting_units(OWNER()), 10); + assert_eq!(state.get_voting_units(OWNER()), ERC_721_INITIAL_MINT); assert_eq!(state.get_voting_units(OTHER()), 0); } @@ -303,5 +326,4 @@ impl VotesSpyHelpersImpl of VotesSpyHelpers { self.assert_event_delegate_votes_changed(contract, delegate, previous_votes, new_votes); self.assert_no_events_left_from(contract); } -} - +} \ No newline at end of file From ce72985e6990739f13b05659d39908749a050a8c Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sat, 5 Oct 2024 20:00:35 -0400 Subject: [PATCH 13/44] fmt --- packages/governance/src/tests/mocks.cairo | 2 +- .../governance/src/tests/test_votes.cairo | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/governance/src/tests/mocks.cairo b/packages/governance/src/tests/mocks.cairo index 46c829072..6bc6d5875 100644 --- a/packages/governance/src/tests/mocks.cairo +++ b/packages/governance/src/tests/mocks.cairo @@ -1,4 +1,4 @@ +pub(crate) mod account_mocks; pub(crate) mod non_implementing_mock; pub(crate) mod timelock_mocks; pub(crate) mod votes_mocks; -pub(crate) mod account_mocks; diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 5a9fc8c8a..97c3795e3 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -1,11 +1,11 @@ use openzeppelin_governance::tests::mocks::votes_mocks::ERC721VotesMock::SNIP12MetadataImpl; use openzeppelin_governance::tests::mocks::votes_mocks::{ERC721VotesMock, ERC20VotesMock}; +use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_governance::votes::votes::TokenVotesTrait; use openzeppelin_governance::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; use openzeppelin_governance::votes::votes::VotesComponent; -use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT, OTHER}; use openzeppelin_testing::events::EventSpyExt; @@ -14,8 +14,8 @@ use openzeppelin_token::erc721::ERC721Component::{ ERC721MetadataImpl, InternalImpl as ERC721InternalImpl, }; use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; -use openzeppelin_utils::structs::checkpoint::TraceTrait; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; +use openzeppelin_utils::structs::checkpoint::TraceTrait; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; use snforge_std::{ start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address @@ -117,7 +117,7 @@ fn test_get_past_votes() { #[should_panic(expected: ('Votes: future Lookup',))] fn test_get_past_votes_future_lookup() { let state = setup_erc721_votes(); - + start_cheat_block_timestamp_global('ts1'); state.get_past_votes(OWNER(), 'ts2'); } @@ -153,7 +153,10 @@ fn test_self_delegate() { state.delegate(OWNER()); spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), OWNER()); - spy.assert_only_event_delegate_votes_changed(contract_address, OWNER(), 0, ERC_721_INITIAL_MINT); + spy + .assert_only_event_delegate_votes_changed( + contract_address, OWNER(), 0, ERC_721_INITIAL_MINT + ); assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); } @@ -166,7 +169,10 @@ fn test_delegate_to_recipient_updates_votes() { state.delegate(RECIPIENT()); spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), RECIPIENT()); - spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, ERC_721_INITIAL_MINT); + spy + .assert_only_event_delegate_votes_changed( + contract_address, RECIPIENT(), 0, ERC_721_INITIAL_MINT + ); assert_eq!(state.get_votes(RECIPIENT()), ERC_721_INITIAL_MINT); assert_eq!(state.get_votes(OWNER()), 0); } @@ -188,26 +194,26 @@ fn test_delegate_by_sig() { let mut state = setup_erc721_votes(); let contract_address = test_address(); start_cheat_block_timestamp_global('ts1'); - + // Generate a key pair and set up an account let key_pair = StarkCurveKeyPairImpl::generate(); let account = setup_account(key_pair.public_key); - + // Set up delegation parameters let nonce = 0; let expiry = 'ts2'; let delegator = account; let delegatee = RECIPIENT(); - + // Create and sign the delegation message let delegation = Delegation { delegatee, nonce, expiry }; let msg_hash = delegation.get_message_hash(delegator); let (r, s) = key_pair.sign(msg_hash).unwrap(); - + // Set up event spy and execute delegation let mut spy = spy_events(); state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); - + spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); assert_eq!(state.delegates(account), delegatee); } @@ -247,7 +253,7 @@ fn test_delegate_by_sig_invalid_signature() { let delegation = Delegation { delegatee, nonce, expiry }; let msg_hash = delegation.get_message_hash(delegator); let (r, s) = key_pair.sign(msg_hash).unwrap(); - + start_cheat_block_timestamp_global('ts1'); // Use an invalid signature state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r + 1, s]); @@ -326,4 +332,4 @@ impl VotesSpyHelpersImpl of VotesSpyHelpers { self.assert_event_delegate_votes_changed(contract, delegate, previous_votes, new_votes); self.assert_no_events_left_from(contract); } -} \ No newline at end of file +} From 4ca0ae485582c881637adc6f8f8746e7e0b77907 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sat, 5 Oct 2024 20:10:13 -0400 Subject: [PATCH 14/44] add changelog entry --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0312236cf..fc57425e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- `VotesComponent` with implementation for ERC721 and ERC20 tokens (#1114) + +### Changed (Breaking) + +- Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) + ### Changed - Bump scarb to v2.8.3 (#1166) From f866b4c2966da16d89aba8f08d07b994d7679376 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 7 Oct 2024 18:59:54 -0400 Subject: [PATCH 15/44] add burn tests --- .../governance/src/tests/test_votes.cairo | 55 +++++++++++++++++-- packages/governance/src/votes/votes.cairo | 5 +- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 97c3795e3..bfc7e74ee 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -95,9 +95,6 @@ fn test_get_votes() { assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); } -// This test can be improved by using the api of the component -// to add checkpoints and thus verify the internal state of the component -// instead of using the trace directly. #[test] fn test_get_past_votes() { let mut state = setup_erc721_votes(); @@ -260,7 +257,7 @@ fn test_delegate_by_sig_invalid_signature() { } // -// Tests specific to ERC721Votes and +// Tests specific to ERC721Votes and ERC20Votes // #[test] @@ -279,6 +276,55 @@ fn test_erc20_get_voting_units() { assert_eq!(state.get_voting_units(OTHER()), 0); } +#[test] +fn test_erc20_burn_updates_votes() { + let mut state = setup_erc20_votes(); + let mut mock_state = ERC20VOTES_CONTRACT_STATE(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, OWNER()); + start_cheat_block_timestamp_global('ts1'); + + state.delegate(OWNER()); + + // Burnsome tokens + let burn_amount = 1000; + mock_state.erc20.burn(OWNER(), burn_amount); + + // Manually update voting units (this would typically be done in a hook) + state.transfer_voting_units(OWNER(), ZERO(), burn_amount); + + // We need to move the timestamp forward to be able to call get_past_total_supply + start_cheat_block_timestamp_global('ts2'); + assert_eq!(state.get_votes(OWNER()), SUPPLY - burn_amount); + assert_eq!(state.get_past_total_supply('ts1'), SUPPLY - burn_amount); +} + +#[test] +fn test_erc721_burn_updates_votes() { + let mut state = setup_erc721_votes(); + let mut mock_state = ERC721VOTES_CONTRACT_STATE(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, OWNER()); + start_cheat_block_timestamp_global('ts1'); + + state.delegate(OWNER()); + + // Burn some tokens + let burn_amount = 3; + let mut i: u256 = 0; + while i < burn_amount { + mock_state.erc721.burn(i); + // Manually update voting units (this would typically be done in a hook) + state.transfer_voting_units(OWNER(), ZERO(), 1); + i += 1; + }; + + // We need to move the timestamp forward to be able to call get_past_total_supply + start_cheat_block_timestamp_global('ts2'); + assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT - burn_amount); + assert_eq!(state.get_past_total_supply('ts1'), ERC_721_INITIAL_MINT - burn_amount); +} + // // Helpers // @@ -333,3 +379,4 @@ impl VotesSpyHelpersImpl of VotesSpyHelpers { self.assert_no_events_left_from(contract); } } + diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 0f98eda41..4accb677b 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -285,13 +285,12 @@ pub mod VotesComponent { to: ContractAddress, amount: u256 ) { - let zero_address = Zero::zero(); let block_timestamp = starknet::get_block_timestamp(); - if (from == zero_address) { + if from.is_zero() { let mut trace = self.Votes_total_checkpoints.read(); trace.push(block_timestamp, trace.latest() + amount); } - if (to == zero_address) { + if to.is_zero() { let mut trace = self.Votes_total_checkpoints.read(); trace.push(block_timestamp, trace.latest() - amount); } From 43a11b4ccd0280eddaf17950510439d7dc9dd642 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 8 Oct 2024 09:41:53 -0400 Subject: [PATCH 16/44] use hooks in mocks --- .../src/tests/mocks/votes_mocks.cairo | 49 +++++++++++++++++-- .../governance/src/tests/test_votes.cairo | 12 +---- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/governance/src/tests/mocks/votes_mocks.cairo b/packages/governance/src/tests/mocks/votes_mocks.cairo index 89e2d7324..2e5429ef8 100644 --- a/packages/governance/src/tests/mocks/votes_mocks.cairo +++ b/packages/governance/src/tests/mocks/votes_mocks.cairo @@ -3,18 +3,19 @@ pub(crate) mod ERC721VotesMock { use openzeppelin_governance::votes::votes::VotesComponent; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc721::ERC721Component; - use openzeppelin_token::erc721::ERC721HooksEmptyImpl; 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 and ERC721Votes + //Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; + impl VotesInternalImpl = VotesComponent::InternalImpl; // ERC721 #[abi(embed_v0)] @@ -60,6 +61,29 @@ pub(crate) mod ERC721VotesMock { } } + impl ERC721VotesHooksImpl< + TContractState, + impl Votes: VotesComponent::HasComponent, + impl HasComponent: ERC721Component::HasComponent, + +NoncesComponent::HasComponent, + +SRC5Component::HasComponent, + +Drop + > of ERC721Component::ERC721HooksTrait { + fn before_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let mut votes_component = get_dep_component_mut!(ref self, Votes); + + // We use the internal function here since it does not check if the token id exists + // which is necessary for mints + let previous_owner = self._owner_of(token_id); + votes_component.transfer_voting_units(previous_owner, to, 1); + } + } + #[constructor] fn constructor(ref self: ContractState) { self.erc721.initializer("MyToken", "MTK", ""); @@ -70,9 +94,9 @@ pub(crate) mod ERC721VotesMock { pub(crate) mod ERC20VotesMock { use openzeppelin_governance::votes::votes::VotesComponent; use openzeppelin_token::erc20::ERC20Component; - use openzeppelin_token::erc20::ERC20HooksEmptyImpl; 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); @@ -81,6 +105,7 @@ pub(crate) mod ERC20VotesMock { // Votes and ERC20Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; + impl VotesInternalImpl = VotesComponent::InternalImpl; // ERC20 #[abi(embed_v0)] @@ -122,6 +147,24 @@ pub(crate) mod ERC20VotesMock { } } + impl ERC20VotesHooksImpl< + TContractState, + impl Votes: VotesComponent::HasComponent, + impl HasComponent: ERC20Component::HasComponent, + +NoncesComponent::HasComponent, + +Drop + > of ERC20Component::ERC20HooksTrait { + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut votes_component = get_dep_component_mut!(ref self, Votes); + votes_component.transfer_voting_units(from, recipient, amount); + } + } + #[constructor] fn constructor(ref self: ContractState) { self.erc20.initializer("MyToken", "MTK"); diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index bfc7e74ee..6e8cb62f7 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -56,8 +56,6 @@ fn setup_erc721_votes() -> ComponentState { let mut i: u256 = 0; while i < ERC_721_INITIAL_MINT { mock_state.erc721.mint(OWNER(), i); - // We manually transfer voting units here, since this is usually implemented in the hook - state.transfer_voting_units(ZERO(), OWNER(), 1); i += 1; }; state @@ -69,9 +67,6 @@ fn setup_erc20_votes() -> ERC20ComponentState { // Mint SUPPLY tokens to owner mock_state.erc20.mint(OWNER(), SUPPLY); - // We manually transfer voting units here, since this is usually implemented in the hook - state.transfer_voting_units(ZERO(), OWNER(), SUPPLY); - state } @@ -286,13 +281,10 @@ fn test_erc20_burn_updates_votes() { state.delegate(OWNER()); - // Burnsome tokens + // Burn some tokens let burn_amount = 1000; mock_state.erc20.burn(OWNER(), burn_amount); - // Manually update voting units (this would typically be done in a hook) - state.transfer_voting_units(OWNER(), ZERO(), burn_amount); - // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); assert_eq!(state.get_votes(OWNER()), SUPPLY - burn_amount); @@ -314,8 +306,6 @@ fn test_erc721_burn_updates_votes() { let mut i: u256 = 0; while i < burn_amount { mock_state.erc721.burn(i); - // Manually update voting units (this would typically be done in a hook) - state.transfer_voting_units(OWNER(), ZERO(), 1); i += 1; }; From 92bf75ed39323791e0eb84e448106c1ef83cb78c Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 8 Oct 2024 14:02:31 -0400 Subject: [PATCH 17/44] use crate for local imports --- .../governance/src/tests/mocks/votes_mocks.cairo | 4 ++-- packages/governance/src/tests/test_votes.cairo | 12 ++++++------ packages/governance/src/votes/votes.cairo | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/governance/src/tests/mocks/votes_mocks.cairo b/packages/governance/src/tests/mocks/votes_mocks.cairo index 2e5429ef8..9428fd06f 100644 --- a/packages/governance/src/tests/mocks/votes_mocks.cairo +++ b/packages/governance/src/tests/mocks/votes_mocks.cairo @@ -1,6 +1,6 @@ #[starknet::contract] pub(crate) mod ERC721VotesMock { - use openzeppelin_governance::votes::votes::VotesComponent; + use crate::votes::votes::VotesComponent; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc721::ERC721Component; use openzeppelin_utils::cryptography::nonces::NoncesComponent; @@ -92,7 +92,7 @@ pub(crate) mod ERC721VotesMock { #[starknet::contract] pub(crate) mod ERC20VotesMock { - use openzeppelin_governance::votes::votes::VotesComponent; + use crate::votes::votes::VotesComponent; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 6e8cb62f7..d20668e4e 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -1,11 +1,11 @@ -use openzeppelin_governance::tests::mocks::votes_mocks::ERC721VotesMock::SNIP12MetadataImpl; -use openzeppelin_governance::tests::mocks::votes_mocks::{ERC721VotesMock, ERC20VotesMock}; -use openzeppelin_governance::votes::utils::Delegation; -use openzeppelin_governance::votes::votes::TokenVotesTrait; -use openzeppelin_governance::votes::votes::VotesComponent::{ +use crate::tests::mocks::votes_mocks::ERC721VotesMock::SNIP12MetadataImpl; +use crate::tests::mocks::votes_mocks::{ERC721VotesMock, ERC20VotesMock}; +use crate::votes::utils::Delegation; +use crate::votes::votes::TokenVotesTrait; +use crate::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; -use openzeppelin_governance::votes::votes::VotesComponent; +use crate::votes::votes::VotesComponent; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT, OTHER}; use openzeppelin_testing::events::EventSpyExt; diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 4accb677b..424064761 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -21,9 +21,9 @@ pub mod VotesComponent { // We should not use Checkpoints or StorageArray as they are for ERC721Vote // Instead we can rely on Vec use core::num::traits::Zero; + use crate::votes::interface::IVotes; + use crate::votes::utils::Delegation; use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; - use openzeppelin_governance::votes::interface::IVotes; - use openzeppelin_governance::votes::utils::Delegation; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; use openzeppelin_token::erc20::interface::IERC20; From 5a8f8605122e7a8a817084ca11ec243eb7287601 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 8 Oct 2024 14:20:26 -0400 Subject: [PATCH 18/44] refactor account names in tests --- .../governance/src/tests/test_votes.cairo | 86 +++++++++---------- packages/testing/src/constants.cairo | 8 ++ 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index d20668e4e..d4cc8651e 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -7,7 +7,7 @@ use crate::votes::votes::VotesComponent::{ }; use crate::votes::votes::VotesComponent; use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT, OTHER}; +use openzeppelin_testing::constants::{SUPPLY, ZERO, DELEGATOR, DELEGATEE, OTHER}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_token::erc20::ERC20Component::InternalTrait; use openzeppelin_token::erc721::ERC721Component::{ @@ -52,10 +52,10 @@ fn ERC20VOTES_CONTRACT_STATE() -> ERC20VotesMock::ContractState { fn setup_erc721_votes() -> ComponentState { let mut state = COMPONENT_STATE(); let mut mock_state = ERC721VOTES_CONTRACT_STATE(); - // Mint ERC_721_INITIAL_MINT NFTs to OWNER + // Mint ERC_721_INITIAL_MINT NFTs to DELEGATOR let mut i: u256 = 0; while i < ERC_721_INITIAL_MINT { - mock_state.erc721.mint(OWNER(), i); + mock_state.erc721.mint(DELEGATOR(), i); i += 1; }; state @@ -65,8 +65,8 @@ fn setup_erc20_votes() -> ERC20ComponentState { let mut state = ERC20_COMPONENT_STATE(); let mut mock_state = ERC20VOTES_CONTRACT_STATE(); - // Mint SUPPLY tokens to owner - mock_state.erc20.mint(OWNER(), SUPPLY); + // Mint SUPPLY tokens to DELEGATOR + mock_state.erc20.mint(DELEGATOR(), SUPPLY); state } @@ -82,18 +82,18 @@ fn setup_account(public_key: felt252) -> ContractAddress { #[test] fn test_get_votes() { let mut state = setup_erc721_votes(); - start_cheat_caller_address(test_address(), OWNER()); - // Before delegating, the owner has 0 votes - assert_eq!(state.get_votes(OWNER()), 0); - state.delegate(OWNER()); + start_cheat_caller_address(test_address(), DELEGATOR()); + // Before delegating, the DELEGATOR has 0 votes + assert_eq!(state.get_votes(DELEGATOR()), 0); + state.delegate(DELEGATOR()); - assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT); } #[test] fn test_get_past_votes() { let mut state = setup_erc721_votes(); - let mut trace = state.Votes_delegate_checkpoints.read(OWNER()); + let mut trace = state.Votes_delegate_checkpoints.read(DELEGATOR()); start_cheat_block_timestamp_global('ts10'); @@ -101,8 +101,8 @@ fn test_get_past_votes() { trace.push('ts2', 5); trace.push('ts3', 7); - assert_eq!(state.get_past_votes(OWNER(), 'ts2'), 5); - assert_eq!(state.get_past_votes(OWNER(), 'ts5'), 7); + assert_eq!(state.get_past_votes(DELEGATOR(), 'ts2'), 5); + assert_eq!(state.get_past_votes(DELEGATOR(), 'ts5'), 7); } #[test] @@ -111,7 +111,7 @@ fn test_get_past_votes_future_lookup() { let state = setup_erc721_votes(); start_cheat_block_timestamp_global('ts1'); - state.get_past_votes(OWNER(), 'ts2'); + state.get_past_votes(DELEGATOR(), 'ts2'); } #[test] @@ -141,15 +141,15 @@ fn test_self_delegate() { let mut state = setup_erc721_votes(); let contract_address = test_address(); let mut spy = spy_events(); - start_cheat_caller_address(contract_address, OWNER()); + start_cheat_caller_address(contract_address, DELEGATOR()); - state.delegate(OWNER()); - spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), OWNER()); + state.delegate(DELEGATOR()); + spy.assert_event_delegate_changed(contract_address, DELEGATOR(), ZERO(), DELEGATOR()); spy .assert_only_event_delegate_votes_changed( - contract_address, OWNER(), 0, ERC_721_INITIAL_MINT + contract_address, DELEGATOR(), 0, ERC_721_INITIAL_MINT ); - assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT); } #[test] @@ -157,26 +157,26 @@ fn test_delegate_to_recipient_updates_votes() { let mut state = setup_erc721_votes(); let contract_address = test_address(); let mut spy = spy_events(); - start_cheat_caller_address(contract_address, OWNER()); + start_cheat_caller_address(contract_address, DELEGATOR()); - state.delegate(RECIPIENT()); - spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), RECIPIENT()); + state.delegate(DELEGATEE()); + spy.assert_event_delegate_changed(contract_address, DELEGATOR(), ZERO(), DELEGATEE()); spy .assert_only_event_delegate_votes_changed( - contract_address, RECIPIENT(), 0, ERC_721_INITIAL_MINT + contract_address, DELEGATEE(), 0, ERC_721_INITIAL_MINT ); - assert_eq!(state.get_votes(RECIPIENT()), ERC_721_INITIAL_MINT); - assert_eq!(state.get_votes(OWNER()), 0); + assert_eq!(state.get_votes(DELEGATEE()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATOR()), 0); } #[test] fn test_delegate_to_recipient_updates_delegates() { let mut state = setup_erc721_votes(); - start_cheat_caller_address(test_address(), OWNER()); - state.delegate(OWNER()); - assert_eq!(state.delegates(OWNER()), OWNER()); - state.delegate(RECIPIENT()); - assert_eq!(state.delegates(OWNER()), RECIPIENT()); + start_cheat_caller_address(test_address(), DELEGATOR()); + state.delegate(DELEGATOR()); + assert_eq!(state.delegates(DELEGATOR()), DELEGATOR()); + state.delegate(DELEGATEE()); + assert_eq!(state.delegates(DELEGATOR()), DELEGATEE()); } #[test] @@ -195,7 +195,7 @@ fn test_delegate_by_sig() { let nonce = 0; let expiry = 'ts2'; let delegator = account; - let delegatee = RECIPIENT(); + let delegatee = DELEGATEE(); // Create and sign the delegation message let delegation = Delegation { delegatee, nonce, expiry }; @@ -219,7 +219,7 @@ fn test_delegate_by_sig_past_expiry() { let expiry = 'ts4'; let signature = array![0, 0]; - state.delegate_by_sig(OWNER(), RECIPIENT(), 0, expiry, signature); + state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 0, expiry, signature); } #[test] @@ -228,7 +228,7 @@ fn test_delegate_by_sig_invalid_nonce() { let mut state = setup_erc721_votes(); let signature = array![0, 0]; - state.delegate_by_sig(OWNER(), RECIPIENT(), 1, 0, signature); + state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 1, 0, signature); } #[test] @@ -241,7 +241,7 @@ fn test_delegate_by_sig_invalid_signature() { let nonce = 0; let expiry = 'ts2'; let delegator = account; - let delegatee = RECIPIENT(); + let delegatee = DELEGATEE(); let delegation = Delegation { delegatee, nonce, expiry }; let msg_hash = delegation.get_message_hash(delegator); let (r, s) = key_pair.sign(msg_hash).unwrap(); @@ -259,7 +259,7 @@ fn test_delegate_by_sig_invalid_signature() { fn test_erc721_get_voting_units() { let state = setup_erc721_votes(); - assert_eq!(state.get_voting_units(OWNER()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_voting_units(DELEGATOR()), ERC_721_INITIAL_MINT); assert_eq!(state.get_voting_units(OTHER()), 0); } @@ -267,7 +267,7 @@ fn test_erc721_get_voting_units() { fn test_erc20_get_voting_units() { let mut state = setup_erc20_votes(); - assert_eq!(state.get_voting_units(OWNER()), SUPPLY); + assert_eq!(state.get_voting_units(DELEGATOR()), SUPPLY); assert_eq!(state.get_voting_units(OTHER()), 0); } @@ -276,18 +276,18 @@ fn test_erc20_burn_updates_votes() { let mut state = setup_erc20_votes(); let mut mock_state = ERC20VOTES_CONTRACT_STATE(); let contract_address = test_address(); - start_cheat_caller_address(contract_address, OWNER()); + start_cheat_caller_address(contract_address, DELEGATOR()); start_cheat_block_timestamp_global('ts1'); - state.delegate(OWNER()); + state.delegate(DELEGATOR()); // Burn some tokens let burn_amount = 1000; - mock_state.erc20.burn(OWNER(), burn_amount); + mock_state.erc20.burn(DELEGATOR(), burn_amount); // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); - assert_eq!(state.get_votes(OWNER()), SUPPLY - burn_amount); + assert_eq!(state.get_votes(DELEGATOR()), SUPPLY - burn_amount); assert_eq!(state.get_past_total_supply('ts1'), SUPPLY - burn_amount); } @@ -296,10 +296,10 @@ fn test_erc721_burn_updates_votes() { let mut state = setup_erc721_votes(); let mut mock_state = ERC721VOTES_CONTRACT_STATE(); let contract_address = test_address(); - start_cheat_caller_address(contract_address, OWNER()); + start_cheat_caller_address(contract_address, DELEGATOR()); start_cheat_block_timestamp_global('ts1'); - state.delegate(OWNER()); + state.delegate(DELEGATOR()); // Burn some tokens let burn_amount = 3; @@ -311,7 +311,7 @@ fn test_erc721_burn_updates_votes() { // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); - assert_eq!(state.get_votes(OWNER()), ERC_721_INITIAL_MINT - burn_amount); + assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT - burn_amount); assert_eq!(state.get_past_total_supply('ts1'), ERC_721_INITIAL_MINT - burn_amount); } diff --git a/packages/testing/src/constants.cairo b/packages/testing/src/constants.cairo index b005f7b47..04c4ebe63 100644 --- a/packages/testing/src/constants.cairo +++ b/packages/testing/src/constants.cairo @@ -91,6 +91,14 @@ pub fn OPERATOR() -> ContractAddress { contract_address_const::<'OPERATOR'>() } +pub fn DELEGATOR() -> ContractAddress { + contract_address_const::<'DELEGATOR'>() +} + +pub fn DELEGATEE() -> ContractAddress { + contract_address_const::<'DELEGATEE'>() +} + pub fn DATA(success: bool) -> Span { let value = if success { SUCCESS From 48b0e7b2a90f6033ec78fa130d6628951664c2b1 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 10 Oct 2024 15:33:26 -0400 Subject: [PATCH 19/44] improve docs and remove ERC20Votes --- docs/modules/ROOT/pages/api/erc20.adoc | 196 +------------------ docs/modules/ROOT/pages/api/governance.adoc | 197 +++++++++++++++++++- docs/modules/ROOT/pages/governance.adoc | 118 ++++++++++++ 3 files changed, 311 insertions(+), 200 deletions(-) diff --git a/docs/modules/ROOT/pages/api/erc20.adoc b/docs/modules/ROOT/pages/api/erc20.adoc index ba4328868..daefb5e90 100644 --- a/docs/modules/ROOT/pages/api/erc20.adoc +++ b/docs/modules/ROOT/pages/api/erc20.adoc @@ -456,200 +456,6 @@ See <>. See <>. -== Extensions - -[.contract] -[[ERC20VotesComponent]] -=== `++ERC20VotesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/token/src/erc20/extensions/erc20_votes.cairo[{github-icon},role=heading-link] - -```cairo -use openzeppelin_token::extensions::ERC20VotesComponent; -``` - -:DelegateChanged: xref:ERC20VotesComponent-DelegateChanged[DelegateChanged] -:DelegateVotesChanged: xref:ERC20VotesComponent-DelegateVotesChanged[DelegateVotesChanged] - -Extension of ERC20 to support voting and delegation. - -NOTE: Implementing xref:#ERC20Component[ERC20Component] is a requirement for this component to be implemented. - -WARNING: To track voting units, this extension requires that the -xref:#ERC20VotesComponent-transfer_voting_units[transfer_voting_units] function is called after every transfer, -mint, or burn operation. For this, the xref:ERC20Component-ERC20HooksTrait[ERC20HooksTrait] must be used. - -This extension keeps a history (checkpoints) of each account’s vote power. Vote power can be delegated either by calling -the xref:#ERC20VotesComponent-delegate[delegate] function directly, or by providing a signature to be used with -xref:#ERC20VotesComponent-delegate_by_sig[delegate_by_sig]. Voting power can be queried through the public accessors -xref:#ERC20VotesComponent-get_votes[get_votes] and xref:#ERC20VotesComponent-get_past_votes[get_past_votes]. - -By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - -[.contract-index#ERC20VotesComponent-Embeddable-Impls] -.Embeddable Implementations --- -[.sub-index#ERC20VotesComponent-Embeddable-Impls-ERC20VotesImpl] -.ERC20VotesImpl -* xref:#ERC20VotesComponent-get_votes[`++get_votes(self, account)++`] -* xref:#ERC20VotesComponent-get_past_votes[`++get_past_votes(self, account, timepoint)++`] -* xref:#ERC20VotesComponent-get_past_total_supply[`++get_past_total_supply(self, timepoint)++`] -* xref:#ERC20VotesComponent-delegates[`++delegates(self, account)++`] -* xref:#ERC20VotesComponent-delegate[`++delegate(self, delegatee)++`] -* xref:#ERC20VotesComponent-delegate_by_sig[`++delegate_by_sig(self, delegator, delegatee, nonce, expiry, signature)++`] --- - -[.contract-index] -.Internal implementations --- -.InternalImpl -* xref:#ERC20VotesComponent-get_total_supply[`++get_total_supply(self)++`] -* xref:#ERC20VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] -* xref:#ERC20VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] -* xref:#ERC20VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] -* xref:#ERC20VotesComponent-num_checkpoints[`++num_checkpoints(self, account)++`] -* xref:#ERC20VotesComponent-checkpoints[`++checkpoints(self, account, pos)++`] -* xref:#ERC20VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] --- - -[.contract-index] -.Events --- -* xref:#ERC20VotesComponent-DelegateChanged[`++DelegateChanged(delegator, from_delegate, to_delegate)++`] -* xref:#ERC20VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] --- - -[#ERC20VotesComponent-Embeddable-functions] -==== Embeddable functions - -[.contract-item] -[[ERC20VotesComponent-get_votes]] -==== `[.contract-item-name]#++get_votes++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#external# - -Returns the current amount of votes that `account` has. - -[.contract-item] -[[ERC20VotesComponent-get_past_votes]] -==== `[.contract-item-name]#++get_past_votes++#++(self: @ContractState, account: ContractAddress, timepoint: u64) → u256++` [.item-kind]#external# - -Returns the amount of votes that `account` had at a specific moment in the past. - -Requirements: - -- `timepoint` must be in the past. - -[.contract-item] -[[ERC20VotesComponent-get_past_total_supply]] -==== `[.contract-item-name]#++get_past_total_supply++#++(self: @ContractState, timepoint: u64) → u256++` [.item-kind]#external# - -Returns the total supply of votes available at a specific moment in the past. - -NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. -Votes that have not been delegated are still part of total supply, even though they would not participate in a -vote. - -[.contract-item] -[[ERC20VotesComponent-delegates]] -==== `[.contract-item-name]#++delegates++#++(self: @ContractState, account: ContractAddress) → ContractAddress++` [.item-kind]#external# - -Returns the delegate that `account` has chosen. - -[.contract-item] -[[ERC20VotesComponent-delegate]] -==== `[.contract-item-name]#++delegate++#++(ref self: ContractState, delegatee: ContractAddress)++` [.item-kind]#external# - -Delegates votes from the caller to `delegatee`. - -Emits a {DelegateChanged} event. - -May emit one or two {DelegateVotesChanged} events. - -[.contract-item] -[[ERC20VotesComponent-delegate_by_sig]] -==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ContractState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# - -Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. - -Requirements: - -- `expiry` must not be in the past. -- `nonce` must match the account's current nonce. -- `delegator` must implement `SRC6::is_valid_signature`. -- `signature` should be valid for the message hash. - -Emits a {DelegateChanged} event. - -May emit one or two {DelegateVotesChanged} events. - -[#ERC20VotesComponent-Internal-functions] -==== Internal functions - -[.contract-item] -[[ERC20VotesComponent-get_total_supply]] -==== `[.contract-item-name]#++get_total_supply++#++(self: @ContractState) → u256++` [.item-kind]#internal# - -Returns the current total supply of votes. - -[.contract-item] -[[ERC20VotesComponent-_delegate]] -==== `[.contract-item-name]#++_delegate++#++(ref self: ContractState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# - -Delegates all of ``account``'s voting units to `delegatee`. - -Emits a {DelegateChanged} event. - -May emit one or two {DelegateVotesChanged} events. - -[.contract-item] -[[ERC20VotesComponent-move_delegate_votes]] -==== `[.contract-item-name]#++move_delegate_votes++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# - -Moves `amount` of delegated votes from `from` to `to`. - -May emit one or two {DelegateVotesChanged} events. - -[.contract-item] -[[ERC20VotesComponent-transfer_voting_units]] -==== `[.contract-item-name]#++transfer_voting_units++#++(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# - -Transfers, mints, or burns voting units. - -To register a mint, `from` should be zero. To register a burn, `to` -should be zero. Total supply of voting units will be adjusted with mints and burns. - -May emit one or two {DelegateVotesChanged} events. - -[.contract-item] -[[ERC20VotesComponent-num_checkpoints]] -==== `[.contract-item-name]#++num_checkpoints++#++(self: @ContractState, account: ContractAddress) → u32++` [.item-kind]#internal# - -Returns the number of checkpoints for `account`. - -[.contract-item] -[[ERC20VotesComponent-checkpoints]] -==== `[.contract-item-name]#++checkpoints++#++(self: @ContractState, account: ContractAddress, pos: u32) → Checkpoint++` [.item-kind]#internal# - -Returns the `pos`-th checkpoint for `account`. - -[.contract-item] -[[ERC20VotesComponent-get_voting_units]] -==== `[.contract-item-name]#++get_voting_units++#++(self: @ContractState, account: ContractAddress) → u256++` [.item-kind]#internal# - -Returns the voting units of an `account`. - -[#ERC20VotesComponent-Events] -==== Events - -[.contract-item] -[[ERC20VotesComponent-DelegateChanged]] -==== `[.contract-item-name]#++DelegateChanged++#++(delegator: ContractAddress, from_delegate: ContractAddress, to_delegate: ContractAddress)++` [.item-kind]#event# - -Emitted when `delegator` delegates their votes from `from_delegate` to `to_delegate`. - -[.contract-item] -[[ERC20VotesComponent-DelegateVotesChanged]] -==== `[.contract-item-name]#++DelegateVotesChanged++#++(delegate: ContractAddress, previous_votes: u256, new_votes: u256)++` [.item-kind]#event# - -Emitted when `delegate` votes are updated from `previous_votes` to `new_votes`. - == Presets [.contract] @@ -716,4 +522,4 @@ Upgrades the contract to a new implementation given by `new_class_hash`. Requirements: - The caller is the contract owner. -- `new_class_hash` cannot be zero. +- `new_class_hash` cannot be zero. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 9e5f083b1..a8410269e 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -5,6 +5,9 @@ :CallCancelled: xref:ITimelock-CallCancelled[CallCancelled] :MinDelayChanged: xref:ITimelock-MinDelayChanged[MinDelayChanged] :RoleGranted: xref:api/access.adoc#IAccessControl-RoleGranted[IAccessControl::RoleGranted] +:DelegateChanged: xref:VotesComponent-DelegateChanged[DelegateChanged] +:DelegateVotesChanged: xref:VotesComponent-DelegateVotesChanged[DelegateVotesChanged] +:TokenVotesTrait: xref:TokenVotesTrait[TokenVotesTrait] = Governance @@ -596,19 +599,20 @@ Emitted when operation `id` is cancelled. Emitted when the minimum delay for future operations is modified. -== Utils +== Votes + +The Votes component provides a flexible system for tracking voting power and delegation. It can be implemented for various token standards, including ERC20 and ERC721. [.contract] [[IVotes]] -=== `++IVotes++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/utils/interfaces/votes.cairo[{github-icon},role=heading-link] +=== `++IVotes++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/interface.cairo[{github-icon},role=heading-link] [.hljs-theme-dark] ```cairo -use openzeppelin_governance::utils::interfaces::IVotes; +use openzeppelin_governance::votes::interface::IVotes; ``` -Common interface for Votes-enabled contracts. For an implementation example see -xref:/api/erc20.adoc#ERC20VotesComponent[ERC20VotesComponent]. +Common interface for Votes-enabled contracts. [.contract-index] .Functions @@ -663,3 +667,186 @@ Delegates votes from the sender to `delegatee`. ==== `[.contract-item-name]#++delegate_by_sig++#++(delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# Delegates votes from `delegator` to `delegatee`. + + +[.contract] +[[VotesComponent]] +=== `++VotesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/votes.cairo[{github-icon},role=heading-link] + +[.hljs-theme-dark] +```cairo +use openzeppelin_governance::votes::VotesComponent; +``` + +By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + +NOTE: When using this module, your contract must implement the {TokenVotesTrait} for your token contract. This is done automatically for ERC20 and ERC721, but if you are using a custom token contract, you must implement this trait. + +[.contract-index#VotesComponent-Embeddable-Impls] +.Embeddable Implementations +-- +[.sub-index#VotesComponent-Embeddable-Impls-VotesImpl] +.VotesImpl +* xref:#VotesComponent-get_votes[`++get_votes(self, account)++`] +* xref:#VotesComponent-get_past_votes[`++get_past_votes(self, account, timepoint)++`] +* xref:#VotesComponent-get_past_total_supply[`++get_past_total_supply(self, timepoint)++`] +* xref:#VotesComponent-delegates[`++delegates(self, account)++`] +* xref:#VotesComponent-delegate[`++delegate(self, delegatee)++`] +* xref:#VotesComponent-delegate_by_sig[`++delegate_by_sig(self, delegator, delegatee, nonce, expiry, signature)++`] +-- + +[.contract-index] +.Internal implementations +-- +.InternalImpl +* xref:#VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] +* xref:#VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] +* xref:#VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] +-- + +[.contract-index] +.Events +-- +* xref:#VotesComponent-DelegateChanged[`++DelegateChanged(delegator, from_delegate, to_delegate)++`] +* xref:#VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] +-- + +[#VotesComponent-Functions] +==== Functions + +[.contract-item] +[[VotesComponent-get_votes]] +==== `[.contract-item-name]#++get_votes++#++(self: @ComponentState, account: ContractAddress) → u256++` [.item-kind]#external# + +Returns the current amount of votes that `account` has. + +[.contract-item] +[[VotesComponent-get_past_votes]] +==== `[.contract-item-name]#++get_past_votes++#++(self: @ComponentState, account: ContractAddress, timepoint: u64) → u256++` [.item-kind]#external# + +Returns the amount of votes that `account` had at a specific moment in the past. + +Requirements: + +- `timepoint` must be in the past. + +[.contract-item] +[[VotesComponent-get_past_total_supply]] +==== `[.contract-item-name]#++get_past_total_supply++#++(self: @ComponentState, timepoint: u64) → u256++` [.item-kind]#external# + +Returns the total supply of votes available at a specific moment in the past. + +NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. +Votes that have not been delegated are still part of total supply, even though they would not participate in a +vote. + +Requirements: + +- `timepoint` must be in the past. + +[.contract-item] +[[VotesComponent-delegates]] +==== `[.contract-item-name]#++delegates++#++(self: @ComponentState, account: ContractAddress) → ContractAddress++` [.item-kind]#external# + +Returns the delegate that `account` has chosen. + +[.contract-item] +[[VotesComponent-delegate]] +==== `[.contract-item-name]#++delegate++#++(ref self: ComponentState, delegatee: ContractAddress)++` [.item-kind]#external# + +Delegates votes from the sender to `delegatee`. + +Emits a {DelegateChanged} event. + +May emit one or two {DelegateVotesChanged} events. + +[.contract-item] +[[VotesComponent-delegate_by_sig]] +==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ComponentState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# + +Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. + +Requirements: + +- `expiry` must not be in the past. +- `nonce` must match the account's current nonce. +- `delegator` must implement `SRC6::is_valid_signature`. +- `signature` should be valid for the message hash. + +Emits a {DelegateChanged} event. + +May emit one or two {DelegateVotesChanged} events. + +[#VotesComponent-Internal-functions] +==== Internal functions + +[.contract-item] +[[VotesComponent-_delegate]] +==== `[.contract-item-name]#++_delegate++#++(ref self: ComponentState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# + +Delegates all of ``account``'s voting units to `delegatee`. + +Emits a {DelegateChanged} event. + +May emit one or two {DelegateVotesChanged} events. + +[.contract-item] +[[VotesComponent-move_delegate_votes]] +==== `[.contract-item-name]#++move_delegate_votes++#++(ref self: ComponentState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# + +Moves delegated votes from one delegate to another. + +May emit one or two {DelegateVotesChanged} events. + +[.contract-item] +[[VotesComponent-transfer_voting_units]] +==== `[.contract-item-name]#++transfer_voting_units++#++(ref self: ComponentState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# + +Transfers, mints, or burns voting units. + +To register a mint, `from` should be zero. To register a burn, `to` +should be zero. Total supply of voting units will be adjusted with mints and burns. + +May emit one or two {DelegateVotesChanged} events. + +[#VotesComponent-Events] +==== Events + +[.contract-item] +[[VotesComponent-DelegateChanged]] +==== `[.contract-item-name]#++DelegateChanged++#++(delegator: ContractAddress, from_delegate: ContractAddress, to_delegate: ContractAddress)++` [.item-kind]#event# + +Emitted when an account changes their delegate. + +[.contract-item] +[[VotesComponent-DelegateVotesChanged]] +==== `[.contract-item-name]#++DelegateVotesChanged++#++(delegate: ContractAddress, previous_votes: u256, new_votes: u256)++` [.item-kind]#event# + +Emitted when a token transfer or delegate change results in changes to a delegate's number of votes. + +[.contract] +[[TokenVotesTrait]] +=== `++TokenVotesTrait++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/votes.cairo[{github-icon},role=heading-link] + +```cairo +pub trait TokenVotesTrait { + fn get_voting_units(self: @TState, account: ContractAddress) -> u256; +} +``` + +This trait must be implemented for tokens that want to use the VotesComponent. It is already implemented for ERC20 and ERC721. + +[.contract-index] +.Functions +-- +* xref:#TokenVotesTrait-get_voting_units[`++get_voting_units(self, account)++`] +-- + +[#TokenVotesTrait-Functions] +==== Functions + +[.contract-item] +[[TokenVotesTrait-get_voting_units]] +==== `[.contract-item-name]#++get_voting_units++#++(self: @TState, account: ContractAddress) → u256++` [.item-kind]#external# + +Returns the number of voting units for a given account. For ERC20, this is typically the token balance. For ERC721, this is typically the number of tokens owned. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 0678b3ba8..0cc5722ac 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -1,6 +1,7 @@ = Governance :timelock-component: xref:api/governance.adoc#TimelockControllerComponent[TimelockControllerComponent] +:votes-component: xref:api/governance.adoc#VotesComponent[VotesComponent] :accesscontrol-component: xref:api/access.adoc#AccessControlComponent[AccessControlComponent] :src5-component: xref:api/introspection.adoc#SRC5Component[SRC5Component] @@ -191,3 +192,120 @@ pub trait TimelockABI { fn renounceRole(ref self: TState, role: felt252, account: ContractAddress); } ---- + +== Votes + +The {votes-component} provides a flexible system for tracking voting power and delegation. It can be implemented for various token standards, including ERC20 and ERC721. This system allows token holders to delegate their voting power to other addresses, enabling more active participation in governance. + +NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + +=== Key Features + +1. *Delegation*: Token holders can delegate their voting power to any address, including themselves.Vote power can be delegated either by calling +the xref:api/governance.adoc#VotesComponent-delegate[delegate] function directly, or by providing a signature to be used with +xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig]. +2. *Historical lookups*: The system keeps track of voting power at different points in time, allowing for accurate voting in proposals that span multiple blocks. +3. *Automatic updates*: Voting power is updated automatically when tokens are transferred, minted, or burned(after a user has delegated to themselves or someone else). + +=== Usage +To use the {votes-component}, you need to integrate it into your token contract. This component is designed to work seamlessly with `ERC20` and `ERC721` tokens, but it can also be adapted for other token types by implementing the xref:api/governance.adoc#TokenVotesTrait[TokenVotesTrait]. Additionally, you must embed the xref:api/introspection.adoc#SRC5Component[SRC5Component] to enable delegation by signatures. + +Here's an example of how to structure a simple ERC20Votes contract: + + +[source,cairo] +---- +#[starknet::contract] +mod ERC20VotesContract { + use openzeppelin_governance::votes::VotesComponent; + use openzeppelin_token::erc20::ERC20Component; + 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; + impl VotesInternalImpl = VotesComponent::InternalImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + // Nonces + #[abi(embed_v0)] + impl NoncesImpl = NoncesComponent::NoncesImpl; + + #[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(crate) impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'DAPP_NAME' + } + fn version() -> felt252 { + 'DAPP_VERSION' + } + } + + // We need to call the VotesComponent::transfer_voting_units function + // after every mint, burn and transfer. + // For this, we use the ERC20Component::ERC20HooksTrait. + impl ERC20VotesHooksImpl< + TContractState, + impl Votes: VotesComponent::HasComponent, + impl HasComponent: ERC20Component::HasComponent, + +NoncesComponent::HasComponent, + +Drop + > of ERC20Component::ERC20HooksTrait { + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut votes_component = get_dep_component_mut!(ref self, Votes); + votes_component.transfer_voting_units(from, recipient, amount); + } + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc20.initializer("MyToken", "MTK"); + } +} +---- + +The VotesComponent will automatically track voting power as tokens are transferred, minted, or burned. + + + + + + + +For a detailed API reference, see the xref:api/governance.adoc#VotesComponent[VotesComponent API documentation]. From 1ad70373b27daf4abcd4fcf774229de4939178fa Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 10 Oct 2024 16:50:44 -0400 Subject: [PATCH 20/44] + --- CHANGELOG.md | 5 +---- Scarb.lock | 1 + packages/governance/README.md | 1 + packages/governance/src/tests.cairo | 1 - packages/governance/src/votes/votes.cairo | 2 +- packages/test_common/Scarb.toml | 1 + 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 161bdf14b..b6f87d47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,16 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `VotesComponent` with implementation for ERC721 and ERC20 tokens (#1114) -### Changed (Breaking) - -- Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) - ### Changed - Bump scarb to v2.8.4 (#1146) ### Changed (Breaking) +- Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) - Bump snforge to 0.31.0 - Remove openzeppelin_utils::selectors (#1163) - Remove `DualCase dispatchers` (#1163) diff --git a/Scarb.lock b/Scarb.lock index 360ebbbe4..d1a995644 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -115,6 +115,7 @@ dependencies = [ "openzeppelin_access", "openzeppelin_account", "openzeppelin_finance", + "openzeppelin_governance", "openzeppelin_introspection", "openzeppelin_security", "openzeppelin_testing", diff --git a/packages/governance/README.md b/packages/governance/README.md index 89296ae46..9dc2004ac 100644 --- a/packages/governance/README.md +++ b/packages/governance/README.md @@ -12,3 +12,4 @@ This crate includes primitives for on-chain governance. ### Components - [`TimelockControllerComponent`](https://docs.openzeppelin.com/contracts-cairo/0.17.0/api/governance#TimelockControllerComponent) +- [`VotesComponent`](https://docs.openzeppelin.com/contracts-cairo/0.17.0/api/governance#VotesComponent) diff --git a/packages/governance/src/tests.cairo b/packages/governance/src/tests.cairo index 0ac19bf2e..c71f711c4 100644 --- a/packages/governance/src/tests.cairo +++ b/packages/governance/src/tests.cairo @@ -1,4 +1,3 @@ mod test_timelock; mod test_utils; -#[cfg(test)] mod test_votes; diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index a01b08073..ea0d9ff82 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.15.1 (governance/votes/votes.cairo) +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/votes/votes.cairo) use starknet::ContractAddress; diff --git a/packages/test_common/Scarb.toml b/packages/test_common/Scarb.toml index 1d488b357..10ad734d5 100644 --- a/packages/test_common/Scarb.toml +++ b/packages/test_common/Scarb.toml @@ -27,6 +27,7 @@ openzeppelin_security = { path = "../security" } openzeppelin_token = { path = "../token" } openzeppelin_testing = { path = "../testing" } openzeppelin_utils = { path = "../utils" } +openzeppelin_governance = { path = "../governance" } [lib] From 70f04b5fd361484bed8d0bd0c503d5aec3f88a48 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 10 Oct 2024 18:39:05 -0400 Subject: [PATCH 21/44] use vec in trace and remove storage array --- .../governance/src/tests/test_votes.cairo | 6 +- packages/governance/src/votes/votes.cairo | 28 ++--- packages/utils/src/structs.cairo | 1 - packages/utils/src/structs/checkpoint.cairo | 104 +++++------------ .../utils/src/structs/storage_array.cairo | 107 ------------------ packages/utils/src/tests.cairo | 1 + packages/utils/src/tests/mocks.cairo | 1 - .../utils/src/tests/test_checkpoint.cairo | 38 +++++++ 8 files changed, 85 insertions(+), 201 deletions(-) delete mode 100644 packages/utils/src/structs/storage_array.cairo delete mode 100644 packages/utils/src/tests/mocks.cairo create mode 100644 packages/utils/src/tests/test_checkpoint.cairo diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 8050192ec..946a30bd4 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -22,7 +22,7 @@ use snforge_std::{ }; use snforge_std::{EventSpy}; use starknet::ContractAddress; -use starknet::storage::{StoragePointerReadAccess, StorageMapReadAccess}; +use starknet::storage::StoragePathEntry; const ERC_721_INITIAL_MINT: u256 = 10; @@ -93,7 +93,7 @@ fn test_get_votes() { #[test] fn test_get_past_votes() { let mut state = setup_erc721_votes(); - let mut trace = state.Votes_delegate_checkpoints.read(DELEGATOR()); + let mut trace = state.Votes_delegate_checkpoints.entry(DELEGATOR()); start_cheat_block_timestamp_global('ts10'); @@ -117,7 +117,7 @@ fn test_get_past_votes_future_lookup() { #[test] fn test_get_past_total_supply() { let mut state = setup_erc721_votes(); - let mut trace = state.Votes_total_checkpoints.read(); + let mut trace = state.Votes_total_checkpoints.deref(); start_cheat_block_timestamp_global('ts10'); trace.push('ts1', 3); diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index ea0d9ff82..7b1225777 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -18,8 +18,6 @@ use starknet::ContractAddress; /// be used at a time to ensure consistent voting power calculations. #[starknet::component] pub mod VotesComponent { - // We should not use Checkpoints or StorageArray as they are for ERC721Vote - // Instead we can rely on Vec use core::num::traits::Zero; use crate::votes::interface::IVotes; use crate::votes::utils::Delegation; @@ -33,9 +31,7 @@ pub mod VotesComponent { use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; - use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess - }; + use starknet::storage::{Map, StoragePathEntry, StorageMapReadAccess, StorageMapWriteAccess}; use super::{TokenVotesTrait, ContractAddress}; #[storage] @@ -87,7 +83,7 @@ pub mod VotesComponent { > of IVotes> { /// Returns the current amount of votes that `account` has. fn get_votes(self: @ComponentState, account: ContractAddress) -> u256 { - self.Votes_delegate_checkpoints.read(account).latest() + self.Votes_delegate_checkpoints.entry(account).latest() } /// Returns the amount of votes that `account` had at a specific moment in the past. @@ -100,7 +96,7 @@ pub mod VotesComponent { ) -> u256 { let current_timepoint = starknet::get_block_timestamp(); assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); - self.Votes_delegate_checkpoints.read(account).upper_lookup_recent(timepoint) + self.Votes_delegate_checkpoints.entry(account).upper_lookup_recent(timepoint) } /// Returns the total supply of votes available at a specific moment in the past. @@ -111,7 +107,7 @@ pub mod VotesComponent { fn get_past_total_supply(self: @ComponentState, timepoint: u64) -> u256 { let current_timepoint = starknet::get_block_timestamp(); assert(timepoint < current_timepoint, Errors::FUTURE_LOOKUP); - self.Votes_total_checkpoints.read().upper_lookup_recent(timepoint) + self.Votes_total_checkpoints.deref().upper_lookup_recent(timepoint) } /// Returns the delegate that `account` has chosen. @@ -259,15 +255,15 @@ pub mod VotesComponent { let block_timestamp = starknet::get_block_timestamp(); if from != to && amount > 0 { if from.is_non_zero() { - let mut trace = self.Votes_delegate_checkpoints.read(from); + let mut trace = self.Votes_delegate_checkpoints.entry(from); let (previous_votes, new_votes) = trace - .push(block_timestamp, trace.latest() - amount); + .push(block_timestamp, trace.into().latest() - amount); self.emit(DelegateVotesChanged { delegate: from, previous_votes, new_votes }); } if to.is_non_zero() { - let mut trace = self.Votes_delegate_checkpoints.read(to); + let mut trace = self.Votes_delegate_checkpoints.entry(to); let (previous_votes, new_votes) = trace - .push(block_timestamp, trace.latest() + amount); + .push(block_timestamp, trace.into().latest() + amount); self.emit(DelegateVotesChanged { delegate: to, previous_votes, new_votes }); } } @@ -287,12 +283,12 @@ pub mod VotesComponent { ) { let block_timestamp = starknet::get_block_timestamp(); if from.is_zero() { - let mut trace = self.Votes_total_checkpoints.read(); - trace.push(block_timestamp, trace.latest() + amount); + let mut trace = self.Votes_total_checkpoints.deref(); + trace.push(block_timestamp, trace.into().latest() + amount); } if to.is_zero() { - let mut trace = self.Votes_total_checkpoints.read(); - trace.push(block_timestamp, trace.latest() - amount); + let mut trace = self.Votes_total_checkpoints.deref(); + trace.push(block_timestamp, trace.into().latest() - amount); } self.move_delegate_votes(self.delegates(from), self.delegates(to), amount); } diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 3b3d8eaa6..4c0eada6d 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -1,2 +1 @@ pub mod checkpoint; -pub mod storage_array; diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index f4e2e1b55..fbfcf7fed 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -3,14 +3,15 @@ use core::num::traits::Sqrt; use crate::math; +use starknet::storage::{StoragePath, StorageAsPath, Vec, VecTrait, Mutable, MutableVecTrait}; +use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::storage_access::StorePacking; -use super::storage_array::{StorageArray, StorageArrayTrait}; /// `Trace` struct, for checkpointing values as they change at different points in /// time, and later looking up past values by block timestamp. -#[derive(Copy, Drop, starknet::Store)] +#[starknet::storage_node] pub struct Trace { - pub checkpoints: StorageArray + pub checkpoints: Vec } /// Generic checkpoint representation. @@ -24,21 +25,21 @@ pub struct Checkpoint { pub impl TraceImpl of TraceTrait { /// Pushes a (`key`, `value`) pair into a Trace so that it is stored as the checkpoint /// and returns both the previous and the new value. - fn push(ref self: Trace, key: u64, value: u256) -> (u256, u256) { - self.checkpoints._insert(key, value) + fn push(self: StoragePath>, key: u64, value: u256) -> (u256, u256) { + self.checkpoints.as_path()._insert(key, value) } /// Returns the value in the last (most recent) checkpoint with the key lower than or equal to /// the search key, or zero if there is none. - fn upper_lookup(self: @Trace, key: u64) -> u256 { - let checkpoints = self.checkpoints; + fn upper_lookup(self: StoragePath, key: u64) -> u256 { + let checkpoints = self.checkpoints.as_path(); let len = checkpoints.len(); - let pos = checkpoints._upper_binary_lookup(key, 0, len); + let pos = checkpoints._upper_binary_lookup(key, 0, len).into(); if pos == 0 { 0 } else { - checkpoints.read_at(pos - 1).value + checkpoints[pos - 1].read().value } } @@ -47,8 +48,8 @@ pub impl TraceImpl of TraceTrait { /// /// NOTE: This is a variant of `upper_lookup` that is optimised to /// find "recent" checkpoints (checkpoints with high keys). - fn upper_lookup_recent(self: @Trace, key: u64) -> u256 { - let checkpoints = self.checkpoints; + fn upper_lookup_recent(self: StoragePath, key: u64) -> u256 { + let checkpoints = self.checkpoints.as_path(); let len = checkpoints.len(); let mut low = 0; @@ -56,57 +57,55 @@ pub impl TraceImpl of TraceTrait { if (len > 5) { let mid = len - len.sqrt().into(); - if (key < checkpoints.read_at(mid).key) { + if (key < checkpoints[mid].read().key) { high = mid; } else { low = mid + 1; } } - let pos = checkpoints._upper_binary_lookup(key, low, high); - if pos == 0 { 0 } else { - checkpoints.read_at(pos - 1).value + checkpoints[pos - 1].read().value } } /// Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - fn latest(self: @Trace) -> u256 { + fn latest(self: StoragePath) -> u256 { let checkpoints = self.checkpoints; let pos = checkpoints.len(); if pos == 0 { 0 } else { - checkpoints.read_at(pos - 1).value + checkpoints[pos - 1].read().value } } /// Returns whether there is a checkpoint in the structure (i.e. it is not empty), /// and if so the key and value in the most recent checkpoint. - fn latest_checkpoint(self: @Trace) -> (bool, u64, u256) { + fn latest_checkpoint(self: StoragePath) -> (bool, u64, u256) { let checkpoints = self.checkpoints; let pos = checkpoints.len(); if (pos == 0) { (false, 0, 0) } else { - let checkpoint: Checkpoint = checkpoints.read_at(pos - 1); + let checkpoint = checkpoints[pos - 1].read(); (true, checkpoint.key, checkpoint.value) } } /// Returns the number of checkpoints. - fn length(self: @Trace) -> u32 { + fn length(self: StoragePath) -> u64 { self.checkpoints.len() } /// Returns the checkpoint at given position. - fn at(self: @Trace, pos: u32) -> Checkpoint { + fn at(self: StoragePath, pos: u64) -> Checkpoint { assert(pos < self.length(), 'Array overflow'); - self.checkpoints.read_at(pos) + self.checkpoints[pos].read() } } @@ -114,26 +113,25 @@ pub impl TraceImpl of TraceTrait { impl CheckpointImpl of CheckpointTrait { /// Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a /// new checkpoint, or by updating the last one. - fn _insert(ref self: StorageArray, key: u64, value: u256) -> (u256, u256) { + fn _insert(self: StoragePath>>, key: u64, value: u256) -> (u256, u256) { let pos = self.len(); if (pos > 0) { - let mut last: Checkpoint = self.read_at(pos - 1); + let mut last = self[pos - 1].read(); // Checkpoint keys must be non-decreasing assert(last.key <= key, 'Unordered insertion'); - // Update or append new checkpoint let prev = last.value; if (last.key == key) { last.value = value; - self.write_at(pos - 1, last); + self[pos - 1].write(last); } else { - self.append(Checkpoint { key: key, value: value }); + self.append().write(Checkpoint { key, value }); } (prev, value) } else { - self.append(Checkpoint { key: key, value: value }); + self.append().write(Checkpoint { key, value }); (0, value) } } @@ -141,7 +139,9 @@ impl CheckpointImpl of CheckpointTrait { /// Returns the index of the last (most recent) checkpoint with the key lower than or equal to /// the search key, or `high` if there is none. `low` and `high` define a section where to do /// the search, with inclusive `low` and exclusive `high`. - fn _upper_binary_lookup(self: @StorageArray, key: u64, low: u32, high: u32) -> u32 { + fn _upper_binary_lookup( + self: StoragePath>, key: u64, low: u64, high: u64 + ) -> u64 { let mut _low = low; let mut _high = high; loop { @@ -149,7 +149,7 @@ impl CheckpointImpl of CheckpointTrait { break; } let mid = math::average(_low, _high); - if (self.read_at(mid).key > key) { + if (self[mid].read().key > key) { _high = mid; } else { _low = mid + 1; @@ -171,7 +171,7 @@ const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; /// - `key` is stored at range [4,67] bits (0-indexed), taking the most significant usable bits. /// - `value.low` is stored at range [124, 251], taking the less significant bits (at the end). /// - `value.high` is stored as the second tuple element. -impl CheckpointStorePacking of StorePacking { +pub impl CheckpointStorePacking of StorePacking { fn pack(value: Checkpoint) -> (felt252, felt252) { let checkpoint = value; // shift-left by 184 bits @@ -195,45 +195,3 @@ impl CheckpointStorePacking of StorePacking { } } } - -#[cfg(test)] -mod test { - use core::num::traits::Bounded; - use super::Checkpoint; - use super::CheckpointStorePacking; - use super::_2_POW_184; - - const KEY_MASK: u256 = 0xffffffffffffffff; - const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; - - #[test] - fn test_pack_big_key_and_value() { - let key = Bounded::MAX; - let value = Bounded::MAX; - let checkpoint = Checkpoint { key, value }; - - let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); - - let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; - let expected_low: u256 = key_and_low.into() & LOW_MASK; - let expected_high: felt252 = Bounded::::MAX.into(); - - assert_eq!(key.into(), expected_key); - assert_eq!(value.low.into(), expected_low); - assert_eq!(high, expected_high); - } - - #[test] - fn test_unpack_big_key_and_value() { - let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); - let high = Bounded::::MAX.into(); - - let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); - - let expected_key: u64 = Bounded::MAX; - let expected_value: u256 = Bounded::MAX; - - assert_eq!(checkpoint.key, expected_key); - assert_eq!(checkpoint.value, expected_value); - } -} diff --git a/packages/utils/src/structs/storage_array.cairo b/packages/utils/src/structs/storage_array.cairo deleted file mode 100644 index ee960cb8a..000000000 --- a/packages/utils/src/structs/storage_array.cairo +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (utils/structs/storage_array.cairo) - -use core::hash::{HashStateExTrait, HashStateTrait}; -use core::poseidon::PoseidonTrait; -use starknet::storage_access::{ - StorageBaseAddress, storage_address_from_base, storage_base_address_from_felt252 -}; -use starknet::syscalls::{storage_read_syscall, storage_write_syscall}; -use starknet::{Store, SyscallResultTrait, SyscallResult}; - -const NOT_IMPLEMENTED: felt252 = 'Not implemented'; - -/// Represents an Array that can be stored in storage. -#[derive(Copy, Drop)] -pub struct StorageArray { - address_domain: u32, - base: StorageBaseAddress -} - -impl StoreStorageArray, impl TStore: Store> of Store> { - #[inline(always)] - fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { - SyscallResult::Ok(StorageArray { address_domain, base }) - } - #[inline(always)] - fn write( - address_domain: u32, base: StorageBaseAddress, value: StorageArray - ) -> SyscallResult<()> { - SyscallResult::Err(array![NOT_IMPLEMENTED]) - } - #[inline(always)] - fn read_at_offset( - address_domain: u32, base: StorageBaseAddress, offset: u8 - ) -> SyscallResult> { - SyscallResult::Err(array![NOT_IMPLEMENTED]) - } - #[inline(always)] - fn write_at_offset( - address_domain: u32, base: StorageBaseAddress, offset: u8, value: StorageArray - ) -> SyscallResult<()> { - SyscallResult::Err(array![NOT_IMPLEMENTED]) - } - #[inline(always)] - fn size() -> u8 { - // 0 was selected because the read method doesn't actually read from storage - 0_u8 - } -} - -/// Trait for accessing a storage array. -/// -/// `read_at` and `write_at` don't check the length of the array, caution must be exercised. -/// The current length of the array is stored at the base StorageBaseAddress as felt. -pub trait StorageArrayTrait { - fn read_at(self: @StorageArray, index: usize) -> T; - fn write_at(ref self: StorageArray, index: usize, value: T) -> (); - fn append(ref self: StorageArray, value: T) -> (); - fn len(self: @StorageArray) -> u32; -} - -impl StorageArrayImpl, impl TStore: Store> of StorageArrayTrait { - fn read_at(self: @StorageArray, index: usize) -> T { - // Get the storage address of the element - let storage_address_felt: felt252 = storage_address_from_base(*self.base).into(); - - let mut state = PoseidonTrait::new(); - let element_address = state.update_with(storage_address_felt + index.into()).finalize(); - - // Read the element from storage - TStore::read(*self.address_domain, storage_base_address_from_felt252(element_address)) - .unwrap_syscall() - } - - fn write_at(ref self: StorageArray, index: usize, value: T) { - // Get the storage address of the element - let storage_address_felt: felt252 = storage_address_from_base(self.base).into(); - - let mut state = PoseidonTrait::new(); - let element_address = state.update_with(storage_address_felt + index.into()).finalize(); - - // Write the element to storage - TStore::write( - self.address_domain, storage_base_address_from_felt252(element_address), value - ) - .unwrap_syscall() - } - - fn append(ref self: StorageArray, value: T) { - let len = self.len(); - - // Write the element to storage - self.write_at(len, value); - - // Update the len - let new_len: felt252 = (len + 1).into(); - storage_write_syscall(self.address_domain, storage_address_from_base(self.base), new_len) - .unwrap_syscall(); - } - - fn len(self: @StorageArray) -> u32 { - storage_read_syscall(*self.address_domain, storage_address_from_base(*self.base)) - .unwrap_syscall() - .try_into() - .unwrap() - } -} diff --git a/packages/utils/src/tests.cairo b/packages/utils/src/tests.cairo index fedcc380c..74bd036b1 100644 --- a/packages/utils/src/tests.cairo +++ b/packages/utils/src/tests.cairo @@ -1,2 +1,3 @@ +mod test_checkpoint; mod test_nonces; mod test_snip12; diff --git a/packages/utils/src/tests/mocks.cairo b/packages/utils/src/tests/mocks.cairo deleted file mode 100644 index 7a238265a..000000000 --- a/packages/utils/src/tests/mocks.cairo +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod nonces_mocks; diff --git a/packages/utils/src/tests/test_checkpoint.cairo b/packages/utils/src/tests/test_checkpoint.cairo new file mode 100644 index 000000000..20741b978 --- /dev/null +++ b/packages/utils/src/tests/test_checkpoint.cairo @@ -0,0 +1,38 @@ +use core::num::traits::Bounded; +use crate::structs::checkpoint::Checkpoint; +use crate::structs::checkpoint::CheckpointStorePacking; + +const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; +const KEY_MASK: u256 = 0xffffffffffffffff; +const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; + +#[test] +fn test_pack_big_key_and_value() { + let key = Bounded::MAX; + let value = Bounded::MAX; + let checkpoint = Checkpoint { key, value }; + + let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); + + let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; + let expected_low: u256 = key_and_low.into() & LOW_MASK; + let expected_high: felt252 = Bounded::::MAX.into(); + + assert_eq!(key.into(), expected_key); + assert_eq!(value.low.into(), expected_low); + assert_eq!(high, expected_high); +} + +#[test] +fn test_unpack_big_key_and_value() { + let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); + let high = Bounded::::MAX.into(); + + let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); + + let expected_key: u64 = Bounded::MAX; + let expected_value: u256 = Bounded::MAX; + + assert_eq!(checkpoint.key, expected_key); + assert_eq!(checkpoint.value, expected_value); +} From e2eb675b3287fc6a99e3e122ad86a6221d303ccb Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 10 Oct 2024 19:18:57 -0400 Subject: [PATCH 22/44] add tests for trace & checkpoint --- packages/utils/src/structs/checkpoint.cairo | 4 +- .../utils/src/tests/test_checkpoint.cairo | 164 +++++++++++++++--- 2 files changed, 139 insertions(+), 29 deletions(-) diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index fbfcf7fed..b4578b464 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -7,6 +7,8 @@ use starknet::storage::{StoragePath, StorageAsPath, Vec, VecTrait, Mutable, Muta use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::storage_access::StorePacking; +const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; + /// `Trace` struct, for checkpointing values as they change at different points in /// time, and later looking up past values by block timestamp. #[starknet::storage_node] @@ -159,8 +161,6 @@ impl CheckpointImpl of CheckpointTrait { } } -const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; - /// Packs a Checkpoint into a (felt252, felt252). /// /// The packing is done as follows: diff --git a/packages/utils/src/tests/test_checkpoint.cairo b/packages/utils/src/tests/test_checkpoint.cairo index 20741b978..00948c77f 100644 --- a/packages/utils/src/tests/test_checkpoint.cairo +++ b/packages/utils/src/tests/test_checkpoint.cairo @@ -1,38 +1,148 @@ -use core::num::traits::Bounded; -use crate::structs::checkpoint::Checkpoint; -use crate::structs::checkpoint::CheckpointStorePacking; -const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; -const KEY_MASK: u256 = 0xffffffffffffffff; -const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; -#[test] -fn test_pack_big_key_and_value() { - let key = Bounded::MAX; - let value = Bounded::MAX; - let checkpoint = Checkpoint { key, value }; +#[starknet::interface] +trait IMockTrace { + fn push_checkpoint(ref self: TContractState, key: u64, value: u256) -> (u256, u256); + fn get_latest(self: @TContractState) -> u256; + fn get_at_key(self: @TContractState, key: u64) -> u256; + fn get_length(self: @TContractState) -> u64; +} + +#[starknet::contract] +pub mod MockTrace { + use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; + + #[storage] + struct Storage { + trace: Trace, + } + + #[abi(embed_v0)] + impl MockTraceImpl of super::IMockTrace { + fn push_checkpoint(ref self: ContractState, key: u64, value: u256) -> (u256, u256) { + self.trace.deref().push(key, value) + } - let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); + fn get_latest(self: @ContractState) -> u256 { + self.trace.deref().latest() + } - let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; - let expected_low: u256 = key_and_low.into() & LOW_MASK; - let expected_high: felt252 = Bounded::::MAX.into(); + fn get_at_key(self: @ContractState, key: u64) -> u256 { + self.trace.deref().upper_lookup(key) + } - assert_eq!(key.into(), expected_key); - assert_eq!(value.low.into(), expected_low); - assert_eq!(high, expected_high); + fn get_length(self: @ContractState) -> u64 { + self.trace.deref().length() + } + } } -#[test] -fn test_unpack_big_key_and_value() { - let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); - let high = Bounded::::MAX.into(); +mod tests { + use core::num::traits::Bounded; + use crate::structs::checkpoint::Checkpoint; + use crate::structs::checkpoint::CheckpointStorePacking; + use super::IMockTrace; + + const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; + const KEY_MASK: u256 = 0xffffffffffffffff; + const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; + + fn CONTRACT_STATE() -> super::MockTrace::ContractState { + super::MockTrace::contract_state_for_testing() + } + + #[test] + fn test_push_checkpoint() { + let mut mock_trace = CONTRACT_STATE(); + + let (prev, new) = mock_trace.push_checkpoint(100, 1000); + assert(prev == 0, 'Incorrect previous value'); + assert(new == 1000, 'Incorrect new value'); + + let (prev, new) = mock_trace.push_checkpoint(200, 2000); + assert(prev == 1000, 'Incorrect previous value'); + assert(new == 2000, 'Incorrect new value'); + } + + #[test] + fn test_get_latest() { + let mut mock_trace = CONTRACT_STATE(); + + mock_trace.push_checkpoint(100, 1000); + mock_trace.push_checkpoint(200, 2000); + + let latest = mock_trace.get_latest(); + assert(latest == 2000, 'Incorrect latest value'); + } + + #[test] + fn test_get_at_key() { + let mut mock_trace = CONTRACT_STATE(); + + mock_trace.push_checkpoint(100, 1000); + mock_trace.push_checkpoint(200, 2000); + mock_trace.push_checkpoint(300, 3000); + + let value_at_150 = mock_trace.get_at_key(150); + assert(value_at_150 == 1000, 'Incorrect value at key 150'); + + let value_at_250 = mock_trace.get_at_key(250); + assert(value_at_250 == 2000, 'Incorrect value at key 250'); + + let value_at_350 = mock_trace.get_at_key(350); + assert(value_at_350 == 3000, 'Incorrect value at key 350'); + } + + #[test] + fn test_get_length() { + let mut mock_trace = CONTRACT_STATE(); + + assert(mock_trace.get_length() == 0, 'Initial length should be 0'); + + mock_trace.push_checkpoint(100, 1000); + assert(mock_trace.get_length() == 1, 'Length should be 1'); + + mock_trace.push_checkpoint(200, 2000); + assert(mock_trace.get_length() == 2, 'Length should be 2'); + } + + #[test] + #[should_panic(expected: ('Unordered insertion',))] + fn test_unordered_insertion() { + let mut mock_trace = CONTRACT_STATE(); + + mock_trace.push_checkpoint(200, 2000); + mock_trace.push_checkpoint(100, 1000); // This should panic + } + + #[test] + fn test_pack_big_key_and_value() { + let key = Bounded::MAX; + let value = Bounded::MAX; + let checkpoint = Checkpoint { key, value }; + + let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); + + let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; + let expected_low: u256 = key_and_low.into() & LOW_MASK; + let expected_high: felt252 = Bounded::::MAX.into(); + + assert_eq!(key.into(), expected_key); + assert_eq!(value.low.into(), expected_low); + assert_eq!(high, expected_high); + } + + #[test] + fn test_unpack_big_key_and_value() { + let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); + let high = Bounded::::MAX.into(); - let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); + let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); - let expected_key: u64 = Bounded::MAX; - let expected_value: u256 = Bounded::MAX; + let expected_key: u64 = Bounded::MAX; + let expected_value: u256 = Bounded::MAX; - assert_eq!(checkpoint.key, expected_key); - assert_eq!(checkpoint.value, expected_value); + assert_eq!(checkpoint.key, expected_key); + assert_eq!(checkpoint.value, expected_value); + } } From a0f6ab653a6ff84e36c9725266686ace0fb7e195 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Thu, 10 Oct 2024 19:26:06 -0400 Subject: [PATCH 23/44] refactor tests --- packages/test_common/src/mocks.cairo | 2 + .../test_common/src/mocks/checkpoint.cairo | 36 +++++++++++++++ .../utils/src/tests/test_checkpoint.cairo | 45 ++----------------- 3 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 packages/test_common/src/mocks/checkpoint.cairo diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo index 7fd51520f..027f99625 100644 --- a/packages/test_common/src/mocks.cairo +++ b/packages/test_common/src/mocks.cairo @@ -1,5 +1,6 @@ pub mod access; pub mod account; +pub mod checkpoint; pub mod erc1155; pub mod erc20; pub mod erc2981; @@ -14,3 +15,4 @@ pub mod timelock; pub mod upgrades; pub mod vesting; pub mod votes; + diff --git a/packages/test_common/src/mocks/checkpoint.cairo b/packages/test_common/src/mocks/checkpoint.cairo new file mode 100644 index 000000000..857c28eb7 --- /dev/null +++ b/packages/test_common/src/mocks/checkpoint.cairo @@ -0,0 +1,36 @@ +#[starknet::interface] +pub trait IMockTrace { + fn push_checkpoint(ref self: TContractState, key: u64, value: u256) -> (u256, u256); + fn get_latest(self: @TContractState) -> u256; + fn get_at_key(self: @TContractState, key: u64) -> u256; + fn get_length(self: @TContractState) -> u64; +} + +#[starknet::contract] +pub mod MockTrace { + use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; + + #[storage] + struct Storage { + trace: Trace, + } + + #[abi(embed_v0)] + impl MockTraceImpl of super::IMockTrace { + fn push_checkpoint(ref self: ContractState, key: u64, value: u256) -> (u256, u256) { + self.trace.deref().push(key, value) + } + + fn get_latest(self: @ContractState) -> u256 { + self.trace.deref().latest() + } + + fn get_at_key(self: @ContractState, key: u64) -> u256 { + self.trace.deref().upper_lookup(key) + } + + fn get_length(self: @ContractState) -> u64 { + self.trace.deref().length() + } + } +} diff --git a/packages/utils/src/tests/test_checkpoint.cairo b/packages/utils/src/tests/test_checkpoint.cairo index 00948c77f..03cd26c6c 100644 --- a/packages/utils/src/tests/test_checkpoint.cairo +++ b/packages/utils/src/tests/test_checkpoint.cairo @@ -1,54 +1,15 @@ - - -#[starknet::interface] -trait IMockTrace { - fn push_checkpoint(ref self: TContractState, key: u64, value: u256) -> (u256, u256); - fn get_latest(self: @TContractState) -> u256; - fn get_at_key(self: @TContractState, key: u64) -> u256; - fn get_length(self: @TContractState) -> u64; -} - -#[starknet::contract] -pub mod MockTrace { - use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; - - #[storage] - struct Storage { - trace: Trace, - } - - #[abi(embed_v0)] - impl MockTraceImpl of super::IMockTrace { - fn push_checkpoint(ref self: ContractState, key: u64, value: u256) -> (u256, u256) { - self.trace.deref().push(key, value) - } - - fn get_latest(self: @ContractState) -> u256 { - self.trace.deref().latest() - } - - fn get_at_key(self: @ContractState, key: u64) -> u256 { - self.trace.deref().upper_lookup(key) - } - - fn get_length(self: @ContractState) -> u64 { - self.trace.deref().length() - } - } -} - mod tests { use core::num::traits::Bounded; use crate::structs::checkpoint::Checkpoint; use crate::structs::checkpoint::CheckpointStorePacking; - use super::IMockTrace; + use openzeppelin_test_common::mocks::checkpoint::{IMockTrace, MockTrace}; const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; const KEY_MASK: u256 = 0xffffffffffffffff; const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; - fn CONTRACT_STATE() -> super::MockTrace::ContractState { - super::MockTrace::contract_state_for_testing() + fn CONTRACT_STATE() -> MockTrace::ContractState { + MockTrace::contract_state_for_testing() } #[test] From 37c073e42f9c44e398ee6d449f77d7d5389e11cc Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sat, 12 Oct 2024 23:13:35 -0400 Subject: [PATCH 24/44] fixes --- CHANGELOG.md | 2 + .../governance/src/tests/test_votes.cairo | 51 ++++-- packages/governance/src/votes.cairo | 2 +- .../votes/{utils.cairo => delegation.cairo} | 12 +- packages/governance/src/votes/interface.cairo | 6 +- packages/governance/src/votes/votes.cairo | 125 +++++++++++--- packages/test_common/src/mocks/votes.cairo | 2 +- .../utils/src/tests/test_checkpoint.cairo | 160 +++++++++--------- 8 files changed, 230 insertions(+), 130 deletions(-) rename packages/governance/src/votes/{utils.cairo => delegation.cairo} (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f87d47b..2b5f01cdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) - Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) + - `Trace` now uses `Vec`instead of `StorageArray`and because of that it is now a `Storage Node` struct +- Remove `StorageArray` from `openzeppelin_utils` (#1114) - Bump snforge to 0.31.0 - Remove openzeppelin_utils::selectors (#1163) - Remove `DualCase dispatchers` (#1163) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 946a30bd4..a9e89abfa 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -1,5 +1,5 @@ -use crate::votes::utils::Delegation; -use crate::votes::votes::TokenVotesTrait; +use crate::votes::delegation::Delegation; +use crate::votes::votes::VotingUnitsTrait; use crate::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; @@ -24,7 +24,7 @@ use snforge_std::{EventSpy}; use starknet::ContractAddress; use starknet::storage::StoragePathEntry; -const ERC_721_INITIAL_MINT: u256 = 10; +const ERC721_INITIAL_MINT: u256 = 10; // // Setup @@ -52,11 +52,9 @@ fn ERC20VOTES_CONTRACT_STATE() -> ERC20VotesMock::ContractState { fn setup_erc721_votes() -> ComponentState { let mut state = COMPONENT_STATE(); let mut mock_state = ERC721VOTES_CONTRACT_STATE(); - // Mint ERC_721_INITIAL_MINT NFTs to DELEGATOR - let mut i: u256 = 0; - while i < ERC_721_INITIAL_MINT { + // Mint ERC721_INITIAL_MINT NFTs to DELEGATOR + for i in 0..ERC721_INITIAL_MINT { mock_state.erc721.mint(DELEGATOR(), i); - i += 1; }; state } @@ -87,7 +85,7 @@ fn test_get_votes() { assert_eq!(state.get_votes(DELEGATOR()), 0); state.delegate(DELEGATOR()); - assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT); } #[test] @@ -101,8 +99,11 @@ fn test_get_past_votes() { trace.push('ts2', 5); trace.push('ts3', 7); + assert_eq!(state.get_past_votes(DELEGATOR(), 'ts1'), 3); assert_eq!(state.get_past_votes(DELEGATOR(), 'ts2'), 5); assert_eq!(state.get_past_votes(DELEGATOR(), 'ts5'), 7); + // This is because we had not delegated at 'ts0' + assert_eq!(state.get_past_votes(DELEGATOR(), 'ts0'), 0); } #[test] @@ -124,10 +125,26 @@ fn test_get_past_total_supply() { trace.push('ts2', 5); trace.push('ts3', 7); + // At ts 'ts0', the total supply is the initial mint + assert_eq!(state.get_past_total_supply('ts0'), ERC721_INITIAL_MINT); + assert_eq!(state.get_past_total_supply('ts1'), 3); assert_eq!(state.get_past_total_supply('ts2'), 5); assert_eq!(state.get_past_total_supply('ts5'), 7); } +#[test] +fn test_get_past_total_supply_before_checkpoints() { + start_cheat_block_timestamp_global('ts1'); + let mut state = setup_erc721_votes(); + let mut trace = state.Votes_total_checkpoints.deref(); + + start_cheat_block_timestamp_global('ts10'); + trace.push('ts1', 3); + trace.push('ts2', 5); + + assert_eq!(state.get_past_total_supply('ts0'), 0); +} + #[test] #[should_panic(expected: ('Votes: future Lookup',))] fn test_get_past_total_supply_future_lookup() { @@ -147,9 +164,9 @@ fn test_self_delegate() { spy.assert_event_delegate_changed(contract_address, DELEGATOR(), ZERO(), DELEGATOR()); spy .assert_only_event_delegate_votes_changed( - contract_address, DELEGATOR(), 0, ERC_721_INITIAL_MINT + contract_address, DELEGATOR(), 0, ERC721_INITIAL_MINT ); - assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT); } #[test] @@ -163,9 +180,9 @@ fn test_delegate_to_recipient_updates_votes() { spy.assert_event_delegate_changed(contract_address, DELEGATOR(), ZERO(), DELEGATEE()); spy .assert_only_event_delegate_votes_changed( - contract_address, DELEGATEE(), 0, ERC_721_INITIAL_MINT + contract_address, DELEGATEE(), 0, ERC721_INITIAL_MINT ); - assert_eq!(state.get_votes(DELEGATEE()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_votes(DELEGATEE()), ERC721_INITIAL_MINT); assert_eq!(state.get_votes(DELEGATOR()), 0); } @@ -259,7 +276,7 @@ fn test_delegate_by_sig_invalid_signature() { fn test_erc721_get_voting_units() { let state = setup_erc721_votes(); - assert_eq!(state.get_voting_units(DELEGATOR()), ERC_721_INITIAL_MINT); + assert_eq!(state.get_voting_units(DELEGATOR()), ERC721_INITIAL_MINT); assert_eq!(state.get_voting_units(OTHER()), 0); } @@ -303,16 +320,14 @@ fn test_erc721_burn_updates_votes() { // Burn some tokens let burn_amount = 3; - let mut i: u256 = 0; - while i < burn_amount { + for i in 0..burn_amount { mock_state.erc721.burn(i); - i += 1; }; // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); - assert_eq!(state.get_votes(DELEGATOR()), ERC_721_INITIAL_MINT - burn_amount); - assert_eq!(state.get_past_total_supply('ts1'), ERC_721_INITIAL_MINT - burn_amount); + assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT - burn_amount); + assert_eq!(state.get_past_total_supply('ts1'), ERC721_INITIAL_MINT - burn_amount); } // diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index 93d7040ab..85d8526ed 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -1,5 +1,5 @@ pub mod interface; -pub mod utils; +pub mod delegation; pub mod votes; pub use votes::VotesComponent; diff --git a/packages/governance/src/votes/utils.cairo b/packages/governance/src/votes/delegation.cairo similarity index 88% rename from packages/governance/src/votes/utils.cairo rename to packages/governance/src/votes/delegation.cairo index 9c1002d96..e0814d647 100644 --- a/packages/governance/src/votes/utils.cairo +++ b/packages/governance/src/votes/delegation.cairo @@ -1,16 +1,14 @@ -// -// Offchain message hash generation helpers. -// - -// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") -// -// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/votes/delegation.cairo) use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; use openzeppelin_utils::cryptography::snip12::{StructHash}; use starknet::ContractAddress; +// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") +// +// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. pub const DELEGATION_TYPE_HASH: felt252 = 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index 597045720..28b467b51 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/votes/interface.cairo) + use starknet::ContractAddress; /// Common interface for Votes-enabled contracts. @@ -32,5 +35,4 @@ pub trait IVotes { expiry: u64, signature: Array ); -} - +} \ No newline at end of file diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 7b1225777..d3b03f478 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -6,21 +6,104 @@ use starknet::ContractAddress; /// # Votes Component /// -/// The Votes component provides a flexible system for tracking voting power and delegation -/// that is currently implemented for ERC20 and ERC721 tokens. It allows accounts to delegate -/// their voting power to a representative, who can then use the pooled voting power in -/// governance decisions. Voting power must be delegated to be counted, and an account can -/// delegate to itself if it wishes to vote directly. +/// The Votes component provides a flexible system for tracking and delegating voting power. +/// that is currently implemented for ERC20 and ERC721 tokens. An account can delegate +/// their voting power to a representative, that will pool delegated voting units from different +/// delegators and can then use it to vote in decisions. Voting power must be delegated to be counted, +/// and an account must delegate to itself if it wishes to vote directly without a trusted +/// representative. /// -/// This component offers a unified interface for voting mechanisms across ERC20 and ERC721 -/// token standards, with the potential to be extended to other token standards in the future. -/// It's important to note that only one token implementation (either ERC20 or ERC721) should -/// be used at a time to ensure consistent voting power calculations. +/// When integrating the Votes component, the ´VotingUnitsTrait´ must be implemented to get the voting +/// units for a given account as a function of the implementing contract. For simplicity, this module +/// already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box +/// if the respective components are integrated. +/// +/// NOTE: ERC20 and ERC721 tokens implementing this component must call ´transfer_voting_units´ +/// whenever a transfer, mint, or burn operation is performed. Hooks can be leveraged for this purpose, +/// as shown in the following ERC20 example: +/// +/// ```cairo +/// #[starknet::contract] +/// pub mod ERC20VotesContract { +/// use openzeppelin_governance::votes::VotesComponent; +/// use openzeppelin_token::erc20::ERC20Component; +/// 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); +/// +/// #[abi(embed_v0)] +/// impl VotesImpl = VotesComponent::VotesImpl; +/// impl VotesInternalImpl = VotesComponent::InternalImpl; +/// +/// #[abi(embed_v0)] +/// impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; +/// impl ERC20InternalImpl = ERC20Component::InternalImpl; +/// +/// #[abi(embed_v0)] +/// impl NoncesImpl = NoncesComponent::NoncesImpl; +/// +/// #[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 +/// } +/// +/// pub impl SNIP12MetadataImpl of SNIP12Metadata { +/// fn name() -> felt252 { +/// 'DAPP_NAME' +/// } +/// fn version() -> felt252 { +/// 'DAPP_VERSION' +/// } +/// } +/// +/// impl ERC20VotesHooksImpl< +/// TContractState, +/// impl Votes: VotesComponent::HasComponent, +/// impl HasComponent: ERC20Component::HasComponent, +/// +NoncesComponent::HasComponent, +/// +Drop +/// > of ERC20Component::ERC20HooksTrait { +/// fn after_update( +/// ref self: ERC20Component::ComponentState, +/// from: ContractAddress, +/// recipient: ContractAddress, +/// amount: u256 +/// ) { +/// let mut votes_component = get_dep_component_mut!(ref self, Votes); +/// votes_component.transfer_voting_units(from, recipient, amount); +/// } +/// } +/// +/// #[constructor] +/// fn constructor(ref self: ContractState) { +/// self.erc20.initializer("MyToken", "MTK"); +/// } +/// } #[starknet::component] pub mod VotesComponent { use core::num::traits::Zero; use crate::votes::interface::IVotes; - use crate::votes::utils::Delegation; + use crate::votes::delegation::Delegation; use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; @@ -32,12 +115,12 @@ pub mod VotesComponent { use openzeppelin_utils::nonces::NoncesComponent; use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; use starknet::storage::{Map, StoragePathEntry, StorageMapReadAccess, StorageMapWriteAccess}; - use super::{TokenVotesTrait, ContractAddress}; + use super::{VotingUnitsTrait, ContractAddress}; #[storage] pub struct Storage { - pub Votes_delegatee: Map::, - pub Votes_delegate_checkpoints: Map::, + pub Votes_delegatee: Map, + pub Votes_delegate_checkpoints: Map, pub Votes_total_checkpoints: Trace, } @@ -49,6 +132,7 @@ pub mod VotesComponent { } #[derive(Drop, PartialEq, starknet::Event)] + /// Emitted when `delegator` delegates their votes from `from_delegate` to `to_delegate`. pub struct DelegateChanged { #[key] pub delegator: ContractAddress, @@ -58,6 +142,7 @@ pub mod VotesComponent { pub to_delegate: ContractAddress } + /// Emitted when `delegate` votes are updated from `previous_votes` to `new_votes`. #[derive(Drop, PartialEq, starknet::Event)] pub struct DelegateVotesChanged { #[key] @@ -77,7 +162,7 @@ pub mod VotesComponent { TContractState, +HasComponent, impl Nonces: NoncesComponent::HasComponent, - +TokenVotesTrait>, + +VotingUnitsTrait>, +SNIP12Metadata, +Drop > of IVotes> { @@ -159,7 +244,7 @@ pub mod VotesComponent { let is_valid_signature_felt = ISRC6Dispatcher { contract_address: delegator } .is_valid_signature(hash, signature); - // Check either 'VALID' or True for backwards compatibility. + // Check either 'VALID' or true for backwards compatibility. let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED || is_valid_signature_felt == 1; @@ -181,7 +266,7 @@ pub mod VotesComponent { impl ERC721: ERC721Component::HasComponent, +ERC721Component::ERC721HooksTrait, +Drop - > of TokenVotesTrait> { + > of VotingUnitsTrait> { /// Returns the number of voting units for a given account. /// /// This implementation is specific to ERC721 tokens, where each token @@ -200,7 +285,7 @@ pub mod VotesComponent { +HasComponent, impl ERC20: ERC20Component::HasComponent, +ERC20Component::ERC20HooksTrait - > of TokenVotesTrait> { + > of VotingUnitsTrait> { /// Returns the number of voting units for a given account. /// /// This implementation is specific to ERC20 tokens, where the balance @@ -217,7 +302,7 @@ pub mod VotesComponent { pub impl InternalImpl< TContractState, +HasComponent, - impl TokenTrait: TokenVotesTrait>, + +VotingUnitsTrait>, +NoncesComponent::HasComponent, +SNIP12Metadata, +Drop @@ -239,7 +324,7 @@ pub mod VotesComponent { ); self .move_delegate_votes( - from_delegate, delegatee, TokenTrait::get_voting_units(@self, account) + from_delegate, delegatee, self.get_voting_units(account) ); } @@ -296,6 +381,6 @@ pub mod VotesComponent { } /// Common trait for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) -pub trait TokenVotesTrait { +pub trait VotingUnitsTrait { fn get_voting_units(self: @TState, account: ContractAddress) -> u256; } diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index a4aa8312b..ef33bbc68 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -12,7 +12,7 @@ pub mod ERC721VotesMock { component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); - //Votes + // Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; impl VotesInternalImpl = VotesComponent::InternalImpl; diff --git a/packages/utils/src/tests/test_checkpoint.cairo b/packages/utils/src/tests/test_checkpoint.cairo index 03cd26c6c..45eee91b0 100644 --- a/packages/utils/src/tests/test_checkpoint.cairo +++ b/packages/utils/src/tests/test_checkpoint.cairo @@ -1,109 +1,107 @@ -mod tests { - use core::num::traits::Bounded; - use crate::structs::checkpoint::Checkpoint; - use crate::structs::checkpoint::CheckpointStorePacking; - use openzeppelin_test_common::mocks::checkpoint::{IMockTrace, MockTrace}; +use core::num::traits::Bounded; +use crate::structs::checkpoint::Checkpoint; +use crate::structs::checkpoint::CheckpointStorePacking; +use openzeppelin_test_common::mocks::checkpoint::{IMockTrace, MockTrace}; - const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; - const KEY_MASK: u256 = 0xffffffffffffffff; - const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; +const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; +const KEY_MASK: u256 = 0xffffffffffffffff; +const LOW_MASK: u256 = 0xffffffffffffffffffffffffffffffff; - fn CONTRACT_STATE() -> MockTrace::ContractState { - MockTrace::contract_state_for_testing() - } +fn CONTRACT_STATE() -> MockTrace::ContractState { + MockTrace::contract_state_for_testing() +} - #[test] - fn test_push_checkpoint() { - let mut mock_trace = CONTRACT_STATE(); +#[test] +fn test_push_checkpoint() { + let mut mock_trace = CONTRACT_STATE(); - let (prev, new) = mock_trace.push_checkpoint(100, 1000); - assert(prev == 0, 'Incorrect previous value'); - assert(new == 1000, 'Incorrect new value'); + let (prev, new) = mock_trace.push_checkpoint(100, 1000); + assert_eq!(prev, 0); + assert_eq!(new, 1000); - let (prev, new) = mock_trace.push_checkpoint(200, 2000); - assert(prev == 1000, 'Incorrect previous value'); - assert(new == 2000, 'Incorrect new value'); - } + let (prev, new) = mock_trace.push_checkpoint(200, 2000); + assert_eq!(prev, 1000); + assert_eq!(new, 2000); +} - #[test] - fn test_get_latest() { - let mut mock_trace = CONTRACT_STATE(); +#[test] +fn test_get_latest() { + let mut mock_trace = CONTRACT_STATE(); - mock_trace.push_checkpoint(100, 1000); - mock_trace.push_checkpoint(200, 2000); + mock_trace.push_checkpoint(100, 1000); + mock_trace.push_checkpoint(200, 2000); - let latest = mock_trace.get_latest(); - assert(latest == 2000, 'Incorrect latest value'); - } + let latest = mock_trace.get_latest(); + assert_eq!(latest, 2000); +} - #[test] - fn test_get_at_key() { - let mut mock_trace = CONTRACT_STATE(); +#[test] +fn test_get_at_key() { + let mut mock_trace = CONTRACT_STATE(); - mock_trace.push_checkpoint(100, 1000); - mock_trace.push_checkpoint(200, 2000); - mock_trace.push_checkpoint(300, 3000); + mock_trace.push_checkpoint(100, 1000); + mock_trace.push_checkpoint(200, 2000); + mock_trace.push_checkpoint(300, 3000); - let value_at_150 = mock_trace.get_at_key(150); - assert(value_at_150 == 1000, 'Incorrect value at key 150'); + let value_at_150 = mock_trace.get_at_key(150); + assert_eq!(value_at_150, 1000); - let value_at_250 = mock_trace.get_at_key(250); - assert(value_at_250 == 2000, 'Incorrect value at key 250'); + let value_at_250 = mock_trace.get_at_key(250); + assert_eq!(value_at_250, 2000); - let value_at_350 = mock_trace.get_at_key(350); - assert(value_at_350 == 3000, 'Incorrect value at key 350'); - } + let value_at_350 = mock_trace.get_at_key(350); + assert_eq!(value_at_350, 3000); +} - #[test] - fn test_get_length() { - let mut mock_trace = CONTRACT_STATE(); +#[test] +fn test_get_length() { + let mut mock_trace = CONTRACT_STATE(); - assert(mock_trace.get_length() == 0, 'Initial length should be 0'); + assert_eq!(mock_trace.get_length(), 0); - mock_trace.push_checkpoint(100, 1000); - assert(mock_trace.get_length() == 1, 'Length should be 1'); + mock_trace.push_checkpoint(100, 1000); + assert_eq!(mock_trace.get_length(), 1); - mock_trace.push_checkpoint(200, 2000); - assert(mock_trace.get_length() == 2, 'Length should be 2'); - } + mock_trace.push_checkpoint(200, 2000); + assert_eq!(mock_trace.get_length(), 2); +} - #[test] - #[should_panic(expected: ('Unordered insertion',))] - fn test_unordered_insertion() { - let mut mock_trace = CONTRACT_STATE(); +#[test] +#[should_panic(expected: ('Unordered insertion',))] +fn test_unordered_insertion() { + let mut mock_trace = CONTRACT_STATE(); - mock_trace.push_checkpoint(200, 2000); - mock_trace.push_checkpoint(100, 1000); // This should panic - } + mock_trace.push_checkpoint(200, 2000); + mock_trace.push_checkpoint(100, 1000); // This should panic +} - #[test] - fn test_pack_big_key_and_value() { - let key = Bounded::MAX; - let value = Bounded::MAX; - let checkpoint = Checkpoint { key, value }; +#[test] +fn test_pack_big_key_and_value() { + let key = Bounded::MAX; + let value = Bounded::MAX; + let checkpoint = Checkpoint { key, value }; - let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); + let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); - let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; - let expected_low: u256 = key_and_low.into() & LOW_MASK; - let expected_high: felt252 = Bounded::::MAX.into(); + let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; + let expected_low: u256 = key_and_low.into() & LOW_MASK; + let expected_high: felt252 = Bounded::::MAX.into(); - assert_eq!(key.into(), expected_key); - assert_eq!(value.low.into(), expected_low); - assert_eq!(high, expected_high); - } + assert_eq!(key.into(), expected_key); + assert_eq!(value.low.into(), expected_low); + assert_eq!(high, expected_high); +} - #[test] - fn test_unpack_big_key_and_value() { - let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); - let high = Bounded::::MAX.into(); +#[test] +fn test_unpack_big_key_and_value() { + let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); + let high = Bounded::::MAX.into(); - let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); + let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); - let expected_key: u64 = Bounded::MAX; - let expected_value: u256 = Bounded::MAX; + let expected_key: u64 = Bounded::MAX; + let expected_value: u256 = Bounded::MAX; - assert_eq!(checkpoint.key, expected_key); - assert_eq!(checkpoint.value, expected_value); - } + assert_eq!(checkpoint.key, expected_key); + assert_eq!(checkpoint.value, expected_value); } From b38263978304e15b9f5c1202cc7bb523f1f7e418 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sat, 12 Oct 2024 23:18:44 -0400 Subject: [PATCH 25/44] fmt --- .../governance/src/tests/test_votes.cairo | 4 ++-- packages/governance/src/votes.cairo | 2 +- packages/governance/src/votes/interface.cairo | 2 +- packages/governance/src/votes/votes.cairo | 23 ++++++++----------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index a9e89abfa..2983c5636 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -1,9 +1,9 @@ use crate::votes::delegation::Delegation; -use crate::votes::votes::VotingUnitsTrait; use crate::votes::votes::VotesComponent::{ DelegateChanged, DelegateVotesChanged, VotesImpl, InternalImpl, }; use crate::votes::votes::VotesComponent; +use crate::votes::votes::VotingUnitsTrait; use openzeppelin_test_common::mocks::votes::ERC721VotesMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::votes::{ERC721VotesMock, ERC20VotesMock}; use openzeppelin_testing as utils; @@ -141,7 +141,7 @@ fn test_get_past_total_supply_before_checkpoints() { start_cheat_block_timestamp_global('ts10'); trace.push('ts1', 3); trace.push('ts2', 5); - + assert_eq!(state.get_past_total_supply('ts0'), 0); } diff --git a/packages/governance/src/votes.cairo b/packages/governance/src/votes.cairo index 85d8526ed..1225881f9 100644 --- a/packages/governance/src/votes.cairo +++ b/packages/governance/src/votes.cairo @@ -1,5 +1,5 @@ -pub mod interface; pub mod delegation; +pub mod interface; pub mod votes; pub use votes::VotesComponent; diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index 28b467b51..e959ef595 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -35,4 +35,4 @@ pub trait IVotes { expiry: u64, signature: Array ); -} \ No newline at end of file +} diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index d3b03f478..a193ab340 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -9,18 +9,18 @@ use starknet::ContractAddress; /// The Votes component provides a flexible system for tracking and delegating voting power. /// that is currently implemented for ERC20 and ERC721 tokens. An account can delegate /// their voting power to a representative, that will pool delegated voting units from different -/// delegators and can then use it to vote in decisions. Voting power must be delegated to be counted, -/// and an account must delegate to itself if it wishes to vote directly without a trusted +/// delegators and can then use it to vote in decisions. Voting power must be delegated to be +/// counted, and an account must delegate to itself if it wishes to vote directly without a trusted /// representative. /// -/// When integrating the Votes component, the ´VotingUnitsTrait´ must be implemented to get the voting -/// units for a given account as a function of the implementing contract. For simplicity, this module -/// already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box -/// if the respective components are integrated. +/// When integrating the Votes component, the ´VotingUnitsTrait´ must be implemented to get the +/// voting units for a given account as a function of the implementing contract. For simplicity, +/// this module already provides two implementations for ERC20 and ERC721 tokens, which will work +/// out of the box if the respective components are integrated. /// /// NOTE: ERC20 and ERC721 tokens implementing this component must call ´transfer_voting_units´ -/// whenever a transfer, mint, or burn operation is performed. Hooks can be leveraged for this purpose, -/// as shown in the following ERC20 example: +/// whenever a transfer, mint, or burn operation is performed. Hooks can be leveraged for this +/// purpose, as shown in the following ERC20 example: /// /// ```cairo /// #[starknet::contract] @@ -102,8 +102,8 @@ use starknet::ContractAddress; #[starknet::component] pub mod VotesComponent { use core::num::traits::Zero; - use crate::votes::interface::IVotes; use crate::votes::delegation::Delegation; + use crate::votes::interface::IVotes; use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc20::ERC20Component; @@ -322,10 +322,7 @@ pub mod VotesComponent { .emit( DelegateChanged { delegator: account, from_delegate, to_delegate: delegatee } ); - self - .move_delegate_votes( - from_delegate, delegatee, self.get_voting_units(account) - ); + self.move_delegate_votes(from_delegate, delegatee, self.get_voting_units(account)); } /// Moves delegated votes from one delegate to another. From 02b1b3b55c23aa54d504a3946258846595d947cb Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 00:45:02 -0400 Subject: [PATCH 26/44] improve docs --- docs/modules/ROOT/pages/api/governance.adoc | 18 +-- docs/modules/ROOT/pages/governance.adoc | 129 ++++++++++++++++++-- packages/governance/Scarb.toml | 2 +- packages/governance/src/votes/votes.cairo | 2 +- 4 files changed, 133 insertions(+), 18 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index bf15dd0b2..17dbdd37c 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -7,7 +7,7 @@ :RoleGranted: xref:api/access.adoc#IAccessControl-RoleGranted[IAccessControl::RoleGranted] :DelegateChanged: xref:VotesComponent-DelegateChanged[DelegateChanged] :DelegateVotesChanged: xref:VotesComponent-DelegateVotesChanged[DelegateVotesChanged] -:TokenVotesTrait: xref:TokenVotesTrait[TokenVotesTrait] +:VotingUnitsTrait: xref:VotingUnitsTrait[VotingUnitsTrait] = Governance @@ -605,7 +605,7 @@ Emitted when the minimum delay for future operations is modified. == Votes -The Votes component provides a flexible system for tracking voting power and delegation. It can be implemented for various token standards, including ERC20 and ERC721. +The `VotesComponent` provides a flexible system for tracking voting power and delegation. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. [.contract] [[IVotes]] @@ -684,7 +684,7 @@ use openzeppelin_governance::votes::VotesComponent; By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. -NOTE: When using this module, your contract must implement the {TokenVotesTrait} for your token contract. This is done automatically for ERC20 and ERC721, but if you are using a custom token contract, you must implement this trait. +NOTE: When using this module, your contract must implement the {VotingUnitsTrait}. For convinience, this is done automatically for `ERC20` and `ERC721` tokens. [.contract-index#VotesComponent-Embeddable-Impls] .Embeddable Implementations @@ -811,6 +811,8 @@ Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` should be zero. Total supply of voting units will be adjusted with mints and burns. +Special care must be taken to ensure that this function is called correctly. In `ERC20` and `ERC721` this is done usually via the hooks. + May emit one or two {DelegateVotesChanged} events. [#VotesComponent-Events] @@ -829,11 +831,11 @@ Emitted when an account changes their delegate. Emitted when a token transfer or delegate change results in changes to a delegate's number of votes. [.contract] -[[TokenVotesTrait]] -=== `++TokenVotesTrait++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/votes.cairo[{github-icon},role=heading-link] +[[VotingUnitsTrait]] +=== `++VotingUnitsTrait++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/votes.cairo[{github-icon},role=heading-link] ```cairo -pub trait TokenVotesTrait { +pub trait VotingUnitsTrait { fn get_voting_units(self: @TState, account: ContractAddress) -> u256; } ``` @@ -843,14 +845,14 @@ This trait must be implemented for tokens that want to use the VotesComponent. I [.contract-index] .Functions -- -* xref:#TokenVotesTrait-get_voting_units[`++get_voting_units(self, account)++`] +* xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] -- [#TokenVotesTrait-Functions] ==== Functions [.contract-item] -[[TokenVotesTrait-get_voting_units]] +[[VotingUnitsTrait-get_voting_units]] ==== `[.contract-item-name]#++get_voting_units++#++(self: @TState, account: ContractAddress) → u256++` [.item-kind]#external# Returns the number of voting units for a given account. For ERC20, this is typically the token balance. For ERC721, this is typically the number of tokens owned. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 97ee53856..6c6ccb129 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -4,6 +4,10 @@ :votes-component: xref:api/governance.adoc#VotesComponent[VotesComponent] :accesscontrol-component: xref:api/access.adoc#AccessControlComponent[AccessControlComponent] :src5-component: xref:api/introspection.adoc#SRC5Component[SRC5Component] +:delegate: xref:api/governance.adoc#VotesComponent-delegate[delegate] +:delegate_by_sig: xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig] +:voting_units_trait: xref:api/governance.adoc#VotingUnitsTrait[VotingUnitsTrait] +:votes-usage: xref:../governance.adoc#usage_2[usage] Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. @@ -201,20 +205,21 @@ pub trait TimelockABI { == Votes -The {votes-component} provides a flexible system for tracking voting power and delegation. It can be implemented for various token standards, including ERC20 and ERC721. This system allows token holders to delegate their voting power to other addresses, enabling more active participation in governance. +The {votes-component} provides a flexible system for tracking voting power and delegation. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. +IMPORTANT: Transfering of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples on how to implement this. + === Key Features -1. *Delegation*: Token holders can delegate their voting power to any address, including themselves.Vote power can be delegated either by calling -the xref:api/governance.adoc#VotesComponent-delegate[delegate] function directly, or by providing a signature to be used with -xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig]. +1. *Delegation*: Users can delegate their voting power to any address, including themselves.Vote power can be delegated either by calling +the {delegate} function directly, or by providing a signature to be used with +{delegate_by_sig}. 2. *Historical lookups*: The system keeps track of voting power at different points in time, allowing for accurate voting in proposals that span multiple blocks. -3. *Automatic updates*: Voting power is updated automatically when tokens are transferred, minted, or burned(after a user has delegated to themselves or someone else). === Usage -To use the {votes-component}, you need to integrate it into your token contract. This component is designed to work seamlessly with `ERC20` and `ERC721` tokens, but it can also be adapted for other token types by implementing the xref:api/governance.adoc#TokenVotesTrait[TokenVotesTrait]. Additionally, you must embed the xref:api/introspection.adoc#SRC5Component[SRC5Component] to enable delegation by signatures. +When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated.Additionally, you must embed the {src-component} to enable delegation by signatures. Here's an example of how to structure a simple ERC20Votes contract: @@ -306,12 +311,120 @@ mod ERC20VotesContract { } ---- -The VotesComponent will automatically track voting power as tokens are transferred, minted, or burned. +And here's an example of how to structure a simple ERC721Votes contract: + + +[source,cairo] +---- +#[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; + impl VotesInternalImpl = VotesComponent::InternalImpl; + + // ERC721 + #[abi(embed_v0)] + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // Nonces + #[abi(embed_v0)] + impl NoncesImpl = NoncesComponent::NoncesImpl; + + #[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' + } + } + impl ERC721VotesHooksImpl< + TContractState, + impl Votes: VotesComponent::HasComponent, + impl HasComponent: ERC721Component::HasComponent, + +NoncesComponent::HasComponent, + +SRC5Component::HasComponent, + +Drop + > of ERC721Component::ERC721HooksTrait { + fn before_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let mut votes_component = get_dep_component_mut!(ref self, Votes); + // We use the internal function here since it does not check if the token id exists + // which is necessary for mints + let previous_owner = self._owner_of(token_id); + votes_component.transfer_voting_units(previous_owner, to, 1); + } + } + #[constructor] + fn constructor(ref self: ContractState) { + self.erc721.initializer("MyToken", "MTK", ""); + } +} +---- +=== Interface -For a detailed API reference, see the xref:api/governance.adoc#VotesComponent[VotesComponent API documentation]. +This is the full interface of the `VotesImpl`` implementation: +[source,cairo] +---- +#[starknet::interface] +pub trait IVotes { + // 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: Array); + + // INonces + fn nonces(self: @TState, owner: ContractAddress) -> felt252; +} +---- diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index 28e5cd9a1..907044d2d 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -26,7 +26,7 @@ starknet.workspace = true openzeppelin_access = { path = "../access" } openzeppelin_introspection = { path = "../introspection" } openzeppelin_account = { path = "../account"} -openzeppelin_token = {path= "../token"} +openzeppelin_token = { path= "../token"} [dev-dependencies] assert_macros.workspace = true diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index a193ab340..94eccfb5e 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -7,7 +7,7 @@ use starknet::ContractAddress; /// # Votes Component /// /// The Votes component provides a flexible system for tracking and delegating voting power. -/// that is currently implemented for ERC20 and ERC721 tokens. An account can delegate +/// It is currently implemented for ERC20 and ERC721 tokens. An account can delegate /// their voting power to a representative, that will pool delegated voting units from different /// delegators and can then use it to vote in decisions. Voting power must be delegated to be /// counted, and an account must delegate to itself if it wishes to vote directly without a trusted From 46c5b5a83eac86eb7f139f7456eb2d33dcfb3396 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 12:55:23 -0400 Subject: [PATCH 27/44] typos --- docs/modules/ROOT/pages/api/governance.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 17dbdd37c..4ac6d0c1d 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -684,7 +684,7 @@ use openzeppelin_governance::votes::VotesComponent; By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. -NOTE: When using this module, your contract must implement the {VotingUnitsTrait}. For convinience, this is done automatically for `ERC20` and `ERC721` tokens. +NOTE: When using this module, your contract must implement the {VotingUnitsTrait}. For convenience, this is done automatically for `ERC20` and `ERC721` tokens. [.contract-index#VotesComponent-Embeddable-Impls] .Embeddable Implementations diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6c6ccb129..5ca204538 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -209,7 +209,7 @@ The {votes-component} provides a flexible system for tracking voting power and d NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. -IMPORTANT: Transfering of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples on how to implement this. +IMPORTANT: Transferring of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples on how to implement this. === Key Features From 246f4cf5ede6f0d6fa33fb2d289adcbeb39f78e5 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 13:08:42 -0400 Subject: [PATCH 28/44] Update CHANGELOG.md Co-authored-by: Eric Nordelo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b5f01cdd..c07e21542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) - Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) - - `Trace` now uses `Vec`instead of `StorageArray`and because of that it is now a `Storage Node` struct + - `Trace` is now declared as a `storage_node` and now uses `Vec` instead of `StorageArray`. - Remove `StorageArray` from `openzeppelin_utils` (#1114) - Bump snforge to 0.31.0 - Remove openzeppelin_utils::selectors (#1163) From 378ba760ea342bbfecdc24ef180f6eaeb2957e79 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 13:35:51 -0400 Subject: [PATCH 29/44] + --- docs/modules/ROOT/pages/api/governance.adoc | 12 ++++++++---- docs/modules/ROOT/pages/governance.adoc | 12 ++++++++---- packages/governance/src/votes/votes.cairo | 20 +++++++++++++++++++- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 4ac6d0c1d..8f66b14b5 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -672,7 +672,6 @@ Delegates votes from the sender to `delegatee`. Delegates votes from `delegator` to `delegatee`. - [.contract] [[VotesComponent]] === `++VotesComponent++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.17.0/packages/governance/src/votes/votes.cairo[{github-icon},role=heading-link] @@ -840,7 +839,7 @@ pub trait VotingUnitsTrait { } ``` -This trait must be implemented for tokens that want to use the VotesComponent. It is already implemented for ERC20 and ERC721. +A trait that must be implemented when integrating {VotesComponent} into a contract. It offers a mechanism to retrieve the number of voting units for a given account at the current time. [.contract-index] .Functions @@ -848,11 +847,16 @@ This trait must be implemented for tokens that want to use the VotesComponent. I * xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] -- -[#TokenVotesTrait-Functions] +[#VotingUnitsTrait-Functions] ==== Functions [.contract-item] [[VotingUnitsTrait-get_voting_units]] ==== `[.contract-item-name]#++get_voting_units++#++(self: @TState, account: ContractAddress) → u256++` [.item-kind]#external# -Returns the number of voting units for a given account. For ERC20, this is typically the token balance. For ERC721, this is typically the number of tokens owned. \ No newline at end of file +Returns the number of voting units for a given account. For ERC20, this is typically the token balance. For ERC721, this is typically the number of tokens owned. + +WARNING: While any formula can be used as a measure of voting units, the internal vote accounting of the contract may be +compromised if voting units are transferred in any external flow by following a different formula. + +For example, when implementing the hook for ERC20, the number of voting units transferred should match the formula given by the +`get_voting_units` implementation. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 5ca204538..6a4c2979e 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -8,6 +8,8 @@ :delegate_by_sig: xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig] :voting_units_trait: xref:api/governance.adoc#VotingUnitsTrait[VotingUnitsTrait] :votes-usage: xref:../governance.adoc#usage_2[usage] +:nonces-component: xref:api/utils.adoc#NoncesComponent[NoncesComponent] +:snip12-metadata: xref:api/utils.adoc#SNIP12Metadata[SNIP12Metadata] Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. @@ -209,17 +211,19 @@ The {votes-component} provides a flexible system for tracking voting power and d NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. -IMPORTANT: Transferring of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples on how to implement this. +IMPORTANT: The transferring of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples of how to implement this. === Key Features -1. *Delegation*: Users can delegate their voting power to any address, including themselves.Vote power can be delegated either by calling +1. *Delegation*: Users can delegate their voting power to any address, including themselves. Vote power can be delegated either by calling the {delegate} function directly, or by providing a signature to be used with {delegate_by_sig}. 2. *Historical lookups*: The system keeps track of voting power at different points in time, allowing for accurate voting in proposals that span multiple blocks. === Usage -When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated.Additionally, you must embed the {src-component} to enable delegation by signatures. + +When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated. +Additionally, you must implement the {nonces-component} and the {snip12-metadata} trait to enable delegation by signatures. Here's an example of how to structure a simple ERC20Votes contract: @@ -411,7 +415,7 @@ pub mod ERC721VotesContract { === Interface -This is the full interface of the `VotesImpl`` implementation: +This is the full interface of the `VotesImpl` implementation: [source,cairo] ---- #[starknet::interface] diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 94eccfb5e..02ffbfe96 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -272,6 +272,10 @@ pub mod VotesComponent { /// This implementation is specific to ERC721 tokens, where each token /// represents one voting unit. The function returns the balance of /// ERC721 tokens for the specified account. + /// /// + /// WARNING: This implementation assumes tokens map to voting units 1:1. + /// Any deviation from this formula when transferring voting units (e.g. by using hooks) + /// may compromise the internal vote accounting. fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { @@ -290,6 +294,10 @@ pub mod VotesComponent { /// /// This implementation is specific to ERC20 tokens, where the balance /// of tokens directly represents the number of voting units. + /// + /// WARNING: This implementation assumes tokens map to voting units 1:1. + /// Any deviation from this formula when transferring voting units (e.g. by using hooks) + /// may compromise the internal vote accounting. fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { @@ -377,7 +385,17 @@ pub mod VotesComponent { } } -/// Common trait for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) +/// A trait that must be implemented when integrating {VotesComponent} into a contract. It +/// offers a mechanism to retrieve the number of voting units for a given account at the current +/// time. pub trait VotingUnitsTrait { + /// Returns the number of voting units for a given account. For ERC20, this is typically the + /// token balance. For ERC721, this is typically the number of tokens owned. + /// + /// WARNING: While any formula can be used as a measure of voting units, the internal vote + /// accounting of the contract may be compromised if voting units are transferred in any + /// external flow by following a different formula. For example, when implementing the hook for + /// ERC20, the number of voting units transferred should match the formula given by the + /// `get_voting_units` implementation. fn get_voting_units(self: @TState, account: ContractAddress) -> u256; } From e68b3dcab940bbccd75fcd17e84cb070ff72cd36 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 13:50:37 -0400 Subject: [PATCH 30/44] + --- docs/modules/ROOT/pages/api/governance.adoc | 1 + docs/modules/ROOT/pages/governance.adoc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 8f66b14b5..a08b8f63c 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -8,6 +8,7 @@ :DelegateChanged: xref:VotesComponent-DelegateChanged[DelegateChanged] :DelegateVotesChanged: xref:VotesComponent-DelegateVotesChanged[DelegateVotesChanged] :VotingUnitsTrait: xref:VotingUnitsTrait[VotingUnitsTrait] +:VotesComponent: xref:VotesComponent[VotesComponent] = Governance diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6a4c2979e..d28db236f 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -222,7 +222,8 @@ the {delegate} function directly, or by providing a signature to be used with === Usage -When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated. +When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. + +For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated. + Additionally, you must implement the {nonces-component} and the {snip12-metadata} trait to enable delegation by signatures. Here's an example of how to structure a simple ERC20Votes contract: From a5110b2481bc3cc32e97d4c6a4bb2fd2ce86b88e Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 13:52:17 -0400 Subject: [PATCH 31/44] ++ --- packages/governance/src/votes/votes.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 02ffbfe96..208bf4130 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -99,6 +99,7 @@ use starknet::ContractAddress; /// self.erc20.initializer("MyToken", "MTK"); /// } /// } +/// ``` #[starknet::component] pub mod VotesComponent { use core::num::traits::Zero; From d1e1a2e1ac2669dd669eabe82b5422c5b09df6eb Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 17:04:35 -0400 Subject: [PATCH 32/44] add more tests and refactor --- docs/modules/ROOT/pages/api/governance.adoc | 14 ++- docs/modules/ROOT/pages/governance.adoc | 2 +- .../governance/src/tests/test_votes.cairo | 103 ++++++++++++++++-- packages/governance/src/votes/votes.cairo | 38 +++---- 4 files changed, 127 insertions(+), 30 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index a08b8f63c..ec3fae955 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -606,7 +606,7 @@ Emitted when the minimum delay for future operations is modified. == Votes -The `VotesComponent` provides a flexible system for tracking voting power and delegation. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. +The `VotesComponent` provides a flexible system for tracking and delegating voting power. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. [.contract] [[IVotes]] @@ -708,6 +708,18 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait * xref:#VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] -- +[.contract-index] +.VotingUnitsTrait implementations +-- +.ERC20VotesImpl +* xref:#VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] + +.ERC721VotesImpl +* xref:#VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] +-- +NOTE: For simplicity, this module already provides two implementations for `ERC20` and `ERC721` tokens, which will work out of the box if the respective components are integrated. + +You can read more about the `VotingUnitsTrait` in the {VotingUnitsTrait} section. + [.contract-index] .Events -- diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index d28db236f..01936e13a 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -207,7 +207,7 @@ pub trait TimelockABI { == Votes -The {votes-component} provides a flexible system for tracking voting power and delegation. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. +The {votes-component} provides a flexible system for tracking and delegating voting power. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance. NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 2983c5636..cc60f3da2 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -7,7 +7,7 @@ use crate::votes::votes::VotingUnitsTrait; use openzeppelin_test_common::mocks::votes::ERC721VotesMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::votes::{ERC721VotesMock, ERC20VotesMock}; use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{SUPPLY, ZERO, DELEGATOR, DELEGATEE, OTHER}; +use openzeppelin_testing::constants::{SUPPLY, ZERO, DELEGATOR, DELEGATEE, OTHER, RECIPIENT}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_token::erc20::ERC20Component::InternalTrait; use openzeppelin_token::erc721::ERC721Component::{ @@ -18,11 +18,12 @@ use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use openzeppelin_utils::structs::checkpoint::TraceTrait; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; use snforge_std::{ - start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address + start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address, + start_cheat_chain_id_global }; use snforge_std::{EventSpy}; -use starknet::ContractAddress; use starknet::storage::StoragePathEntry; +use starknet::{ContractAddress, contract_address_const}; const ERC721_INITIAL_MINT: u256 = 10; @@ -227,6 +228,33 @@ fn test_delegate_by_sig() { assert_eq!(state.delegates(account), delegatee); } +#[test] +fn test_delegate_by_sig_hash_generation() { + start_cheat_chain_id_global('SN_TEST'); + + let nonce = 0; + let expiry = 'ts2'; + let delegator = contract_address_const::< + 0x70b0526a4bfbc9ca717c96aeb5a8afac85181f4585662273668928585a0d628 + >(); + let delegatee = RECIPIENT(); + let delegation = Delegation { delegatee, nonce, expiry }; + + let hash = delegation.get_message_hash(delegator); + + // This hash was computed using starknet js sdk from the following values: + // - name: 'DAPP_NAME' + // - version: 'DAPP_VERSION' + // - chainId: 'SN_TEST' + // - account: 0x70b0526a4bfbc9ca717c96aeb5a8afac85181f4585662273668928585a0d628 + // - delegatee: 'RECIPIENT' + // - nonce: 0 + // - expiry: 'ts2' + // - revision: '1' + let expected_hash = 0x314bd38b22b62d576691d8dafd9f8ea0601329ebe686bc64ca28e4d8821d5a0; + assert_eq!(hash, expected_hash); +} + #[test] #[should_panic(expected: ('Votes: expired signature',))] fn test_delegate_by_sig_past_expiry() { @@ -268,6 +296,50 @@ fn test_delegate_by_sig_invalid_signature() { state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r + 1, s]); } +#[test] +#[should_panic(expected: ('Votes: invalid signature',))] +fn test_delegate_by_sig_bad_delegatee() { + let mut state = setup_erc721_votes(); + let key_pair = StarkCurveKeyPairImpl::generate(); + let account = setup_account(key_pair.public_key); + + let nonce = 0; + let expiry = 'ts2'; + let delegator = account; + let delegatee = DELEGATEE(); + let bad_delegatee = contract_address_const::<0x1234>(); + let delegation = Delegation { delegatee, nonce, expiry }; + let msg_hash = delegation.get_message_hash(delegator); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + start_cheat_block_timestamp_global('ts1'); + // Use a different delegatee than the one signed for + state.delegate_by_sig(delegator, bad_delegatee, nonce, expiry, array![r, s]); +} + +#[test] +#[should_panic(expected: ('Nonces: invalid nonce',))] +fn test_delegate_by_sig_reused_signature() { + let mut state = setup_erc721_votes(); + let key_pair = StarkCurveKeyPairImpl::generate(); + let account = setup_account(key_pair.public_key); + + let nonce = 0; + let expiry = 'ts2'; + let delegator = account; + let delegatee = DELEGATEE(); + let delegation = Delegation { delegatee, nonce, expiry }; + let msg_hash = delegation.get_message_hash(delegator); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + start_cheat_block_timestamp_global('ts1'); + // First delegation (should succeed) + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); + + // Attempt to reuse the same signature (should fail) + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); +} + // // Tests specific to ERC721Votes and ERC20Votes // @@ -298,12 +370,17 @@ fn test_erc20_burn_updates_votes() { state.delegate(DELEGATOR()); - // Burn some tokens + // Set spy and burn some tokens + let mut spy = spy_events(); let burn_amount = 1000; mock_state.erc20.burn(DELEGATOR(), burn_amount); // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); + spy + .assert_event_delegate_votes_changed( + contract_address, DELEGATOR(), SUPPLY, SUPPLY - burn_amount + ); assert_eq!(state.get_votes(DELEGATOR()), SUPPLY - burn_amount); assert_eq!(state.get_past_total_supply('ts1'), SUPPLY - burn_amount); } @@ -318,11 +395,20 @@ fn test_erc721_burn_updates_votes() { state.delegate(DELEGATOR()); - // Burn some tokens + // Set spy and burn some tokens + let mut spy = spy_events(); let burn_amount = 3; - for i in 0..burn_amount { - mock_state.erc721.burn(i); - }; + for i in 0 + ..burn_amount { + mock_state.erc721.burn(i); + spy + .assert_event_delegate_votes_changed( + contract_address, + DELEGATOR(), + ERC721_INITIAL_MINT - i, + ERC721_INITIAL_MINT - i - 1 + ); + }; // We need to move the timestamp forward to be able to call get_past_total_supply start_cheat_block_timestamp_global('ts2'); @@ -384,4 +470,3 @@ impl VotesSpyHelpersImpl of VotesSpyHelpers { self.assert_no_events_left_from(contract); } } - diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 208bf4130..096744208 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -260,50 +260,50 @@ pub mod VotesComponent { // Internal // - impl ERC721VotesImpl< + impl ERC20VotesImpl< TContractState, +HasComponent, - +SRC5Component::HasComponent, - impl ERC721: ERC721Component::HasComponent, - +ERC721Component::ERC721HooksTrait, - +Drop + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait > of VotingUnitsTrait> { /// Returns the number of voting units for a given account. /// - /// This implementation is specific to ERC721 tokens, where each token - /// represents one voting unit. The function returns the balance of - /// ERC721 tokens for the specified account. - /// /// + /// This implementation is specific to ERC20 tokens, where the balance + /// of tokens directly represents the number of voting units. + /// /// WARNING: This implementation assumes tokens map to voting units 1:1. /// Any deviation from this formula when transferring voting units (e.g. by using hooks) /// may compromise the internal vote accounting. fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { - let erc721_component = get_dep_component!(self, ERC721); - erc721_component.balance_of(account).into() + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(account) } } - impl ERC20VotesImpl< + impl ERC721VotesImpl< TContractState, +HasComponent, - impl ERC20: ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait + +SRC5Component::HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + +Drop > of VotingUnitsTrait> { /// Returns the number of voting units for a given account. /// - /// This implementation is specific to ERC20 tokens, where the balance - /// of tokens directly represents the number of voting units. - /// + /// This implementation is specific to ERC721 tokens, where each token + /// represents one voting unit. The function returns the balance of + /// ERC721 tokens for the specified account. + /// /// /// WARNING: This implementation assumes tokens map to voting units 1:1. /// Any deviation from this formula when transferring voting units (e.g. by using hooks) /// may compromise the internal vote accounting. fn get_voting_units( self: @ComponentState, account: ContractAddress ) -> u256 { - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.balance_of(account) + let erc721_component = get_dep_component!(self, ERC721); + erc721_component.balance_of(account).into() } } From cc4c9a7674d5a0d9a892d79545670563e0f1baaf Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Sun, 13 Oct 2024 19:23:17 -0400 Subject: [PATCH 33/44] add missing internal functions and tests --- .../governance/src/tests/test_votes.cairo | 45 ++++++++++++++++++- packages/governance/src/votes/votes.cairo | 19 +++++++- packages/utils/src/structs/checkpoint.cairo | 4 +- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index cc60f3da2..69ca5c644 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -15,7 +15,7 @@ use openzeppelin_token::erc721::ERC721Component::{ }; use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; -use openzeppelin_utils::structs::checkpoint::TraceTrait; +use openzeppelin_utils::structs::checkpoint::{Checkpoint, TraceTrait}; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; use snforge_std::{ start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address, @@ -340,6 +340,37 @@ fn test_delegate_by_sig_reused_signature() { state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); } +#[test] +fn test_num_checkpoints() { + let mut state = setup_erc721_votes(); + let mut trace = state.Votes_delegate_checkpoints.entry(DELEGATOR()); + + start_cheat_block_timestamp_global('ts10'); + + trace.push('ts1', 3); + trace.push('ts2', 5); + trace.push('ts3', 7); + + assert_eq!(state.num_checkpoints(DELEGATOR()), 3); + assert_eq!(state.num_checkpoints(OTHER()), 0); +} + +#[test] +fn test_checkpoints() { + let mut state = setup_erc721_votes(); + let mut trace = state.Votes_delegate_checkpoints.entry(DELEGATOR()); + + start_cheat_block_timestamp_global('ts10'); + + trace.push('ts1', 3); + trace.push('ts2', 5); + trace.push('ts3', 7); + + assert_eq!(state.checkpoints(DELEGATOR(), 0), Checkpoint { key: 'ts1', value: 3 }); + assert_eq!(state.checkpoints(DELEGATOR(), 1), Checkpoint { key: 'ts2', value: 5 }); + assert_eq!(state.checkpoints(DELEGATOR(), 2), Checkpoint { key: 'ts3', value: 7 }); +} + // // Tests specific to ERC721Votes and ERC20Votes // @@ -416,6 +447,18 @@ fn test_erc721_burn_updates_votes() { assert_eq!(state.get_past_total_supply('ts1'), ERC721_INITIAL_MINT - burn_amount); } +#[test] +fn test_erc_721_get_total_supply() { + let state = setup_erc721_votes(); + assert_eq!(state.get_total_supply(), ERC721_INITIAL_MINT); +} + +#[test] +fn test_erc_20_get_total_supply() { + let state = setup_erc20_votes(); + assert_eq!(state.get_total_supply(), SUPPLY); +} + // // Helpers // diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 096744208..47dfe98ba 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -114,7 +114,7 @@ pub mod VotesComponent { use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; - use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; + use openzeppelin_utils::structs::checkpoint::{Checkpoint, Trace, TraceTrait}; use starknet::storage::{Map, StoragePathEntry, StorageMapReadAccess, StorageMapWriteAccess}; use super::{VotingUnitsTrait, ContractAddress}; @@ -316,6 +316,11 @@ pub mod VotesComponent { +SNIP12Metadata, +Drop > of InternalTrait { + /// Returns the current total supply of votes. + fn get_total_supply(self: @ComponentState) -> u256 { + self.Votes_total_checkpoints.deref().latest() + } + /// Delegates all of `account`'s voting units to `delegatee`. /// /// Emits a `DelegateChanged` event. @@ -383,6 +388,18 @@ pub mod VotesComponent { } self.move_delegate_votes(self.delegates(from), self.delegates(to), amount); } + + /// Returns the number of checkpoints for `account`. + fn num_checkpoints(self: @ComponentState, account: ContractAddress) -> u64 { + self.Votes_delegate_checkpoints.entry(account).length() + } + + /// Returns the `pos`-th checkpoint for `account`. + fn checkpoints( + self: @ComponentState, account: ContractAddress, pos: u64 + ) -> Checkpoint { + self.Votes_delegate_checkpoints.entry(account).at(pos) + } } } diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index b4578b464..237b2ba10 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -17,7 +17,7 @@ pub struct Trace { } /// Generic checkpoint representation. -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, Debug, PartialEq)] pub struct Checkpoint { pub key: u64, pub value: u256 @@ -106,7 +106,7 @@ pub impl TraceImpl of TraceTrait { /// Returns the checkpoint at given position. fn at(self: StoragePath, pos: u64) -> Checkpoint { - assert(pos < self.length(), 'Array overflow'); + assert(pos < self.length(), 'Vec overflow'); self.checkpoints[pos].read() } } From 52695d7a7d87fa7d15c7613fd272018505ce0034 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 14 Oct 2024 15:31:29 -0400 Subject: [PATCH 34/44] add VotesABI and format --- docs/modules/ROOT/pages/api/governance.adoc | 6 ++++- docs/modules/ROOT/pages/governance.adoc | 4 ++-- packages/governance/src/votes/interface.cairo | 22 +++++++++++++++++++ packages/governance/src/votes/votes.cairo | 14 ++++++++++-- packages/utils/src/structs/checkpoint.cairo | 2 +- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index ec3fae955..999304d05 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -9,6 +9,7 @@ :DelegateVotesChanged: xref:VotesComponent-DelegateVotesChanged[DelegateVotesChanged] :VotingUnitsTrait: xref:VotingUnitsTrait[VotingUnitsTrait] :VotesComponent: xref:VotesComponent[VotesComponent] +:IVotes: xref:IVotes[IVotes] = Governance @@ -681,6 +682,7 @@ Delegates votes from `delegator` to `delegatee`. ```cairo use openzeppelin_governance::votes::VotesComponent; ``` +Component that implements the {IVotes} interface and provides a flexible system for tracking and delegating voting power. By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. @@ -823,7 +825,9 @@ Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` should be zero. Total supply of voting units will be adjusted with mints and burns. -Special care must be taken to ensure that this function is called correctly. In `ERC20` and `ERC721` this is done usually via the hooks. +CAUTION: If voting units are a function of an underlying transferrable asset (like a token), this function should be +called every time the underlying asset is transferred to keep the internal accounting of voting power in sync. For +`ERC20` and `ERC721` tokens, this is usually done using hooks. May emit one or two {DelegateVotesChanged} events. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 01936e13a..a008f27df 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -420,8 +420,8 @@ This is the full interface of the `VotesImpl` implementation: [source,cairo] ---- #[starknet::interface] -pub trait IVotes { - // IVotes +pub trait VotesABI { + // Votes 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; diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index e959ef595..fccbfe814 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -36,3 +36,25 @@ pub trait IVotes { signature: Array ); } + +/// Common interface to interact with the `Votes` component. +#[starknet::interface] +pub trait VotesABI { + // Votes + 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: Array + ); + + // Nonces + fn nonces(self: @TState, owner: ContractAddress) -> felt252; +} diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 47dfe98ba..f6e6d0e12 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -3,7 +3,6 @@ use starknet::ContractAddress; - /// # Votes Component /// /// The Votes component provides a flexible system for tracking and delegating voting power. @@ -271,6 +270,9 @@ pub mod VotesComponent { /// This implementation is specific to ERC20 tokens, where the balance /// of tokens directly represents the number of voting units. /// + /// NOTE: This implementation will work out of the box if the ERC20 component + /// is implemented in the final contract. + /// /// WARNING: This implementation assumes tokens map to voting units 1:1. /// Any deviation from this formula when transferring voting units (e.g. by using hooks) /// may compromise the internal vote accounting. @@ -295,7 +297,10 @@ pub mod VotesComponent { /// This implementation is specific to ERC721 tokens, where each token /// represents one voting unit. The function returns the balance of /// ERC721 tokens for the specified account. - /// /// + /// + /// NOTE: This implementation will work out of the box if the ERC721 component + /// is implemented in the final contract. + /// /// WARNING: This implementation assumes tokens map to voting units 1:1. /// Any deviation from this formula when transferring voting units (e.g. by using hooks) /// may compromise the internal vote accounting. @@ -370,6 +375,11 @@ pub mod VotesComponent { /// To register a mint, `from` should be zero. To register a burn, `to` /// should be zero. Total supply of voting units will be adjusted with mints and burns. /// + /// WARNING: If voting units are a function of an underlying transferrable asset (like a + /// token), this function should be called every time the underlying asset is transferred to + /// keep the internal accounting of voting power in sync. For ERC20 and ERC721 tokens, this + /// is usually done using hooks. + /// /// May emit one or two `DelegateVotesChanged` events. fn transfer_voting_units( ref self: ComponentState, diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index 237b2ba10..8ec87bb40 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -171,7 +171,7 @@ impl CheckpointImpl of CheckpointTrait { /// - `key` is stored at range [4,67] bits (0-indexed), taking the most significant usable bits. /// - `value.low` is stored at range [124, 251], taking the less significant bits (at the end). /// - `value.high` is stored as the second tuple element. -pub impl CheckpointStorePacking of StorePacking { +pub(crate) impl CheckpointStorePacking of StorePacking { fn pack(value: Checkpoint) -> (felt252, felt252) { let checkpoint = value; // shift-left by 184 bits From 9a601c8b04e8d8e3280ac2bf6efcc666c55a3d33 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 14 Oct 2024 16:13:24 -0400 Subject: [PATCH 35/44] docs: trait implementations --- docs/modules/ROOT/pages/api/finance.adoc | 2 +- docs/modules/ROOT/pages/api/governance.adoc | 22 ++++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/modules/ROOT/pages/api/finance.adoc b/docs/modules/ROOT/pages/api/finance.adoc index 60479e47f..f8c5761ac 100755 --- a/docs/modules/ROOT/pages/api/finance.adoc +++ b/docs/modules/ROOT/pages/api/finance.adoc @@ -114,7 +114,7 @@ use openzeppelin_finance::vesting::VestingComponent; Vesting component implementing the xref:IVesting[`IVesting`] interface. [.contract-index] -.Vesting Schedule Trait +.Vesting Schedule Trait Implementations -- .functions * xref:#VestingComponent-calculate_vested_amount[`++calculate_vested_amount(self, token, total_allocation, diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 999304d05..2e5e7a29a 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -688,6 +688,16 @@ By default, token balance does not account for voting power. This makes transfer NOTE: When using this module, your contract must implement the {VotingUnitsTrait}. For convenience, this is done automatically for `ERC20` and `ERC721` tokens. +[.contract-index] +.Voting Units Trait Implementations +-- +.ERC20VotesImpl +* xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] + +.ERC721VotesImpl +* xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] +-- + [.contract-index#VotesComponent-Embeddable-Impls] .Embeddable Implementations -- @@ -710,18 +720,6 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait * xref:#VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] -- -[.contract-index] -.VotingUnitsTrait implementations --- -.ERC20VotesImpl -* xref:#VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] - -.ERC721VotesImpl -* xref:#VotesComponent-get_voting_units[`++get_voting_units(self, account)++`] --- -NOTE: For simplicity, this module already provides two implementations for `ERC20` and `ERC721` tokens, which will work out of the box if the respective components are integrated. + -You can read more about the `VotingUnitsTrait` in the {VotingUnitsTrait} section. - [.contract-index] .Events -- From 1f17d1a75fabdcc144248dcbf97a23d39e5acd4b Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 14 Oct 2024 16:27:44 -0400 Subject: [PATCH 36/44] remove debug and partialeq from checkpoint --- packages/governance/src/tests/test_votes.cairo | 16 ++++++++++++---- packages/utils/src/structs/checkpoint.cairo | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 69ca5c644..2cebb4ad6 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -15,7 +15,7 @@ use openzeppelin_token::erc721::ERC721Component::{ }; use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; -use openzeppelin_utils::structs::checkpoint::{Checkpoint, TraceTrait}; +use openzeppelin_utils::structs::checkpoint::TraceTrait; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; use snforge_std::{ start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, test_address, @@ -366,9 +366,17 @@ fn test_checkpoints() { trace.push('ts2', 5); trace.push('ts3', 7); - assert_eq!(state.checkpoints(DELEGATOR(), 0), Checkpoint { key: 'ts1', value: 3 }); - assert_eq!(state.checkpoints(DELEGATOR(), 1), Checkpoint { key: 'ts2', value: 5 }); - assert_eq!(state.checkpoints(DELEGATOR(), 2), Checkpoint { key: 'ts3', value: 7 }); + let checkpoint0 = state.checkpoints(DELEGATOR(), 0); + assert_eq!(checkpoint0.key, 'ts1'); + assert_eq!(checkpoint0.value, 3); + + let checkpoint1 = state.checkpoints(DELEGATOR(), 1); + assert_eq!(checkpoint1.key, 'ts2'); + assert_eq!(checkpoint1.value, 5); + + let checkpoint2 = state.checkpoints(DELEGATOR(), 2); + assert_eq!(checkpoint2.key, 'ts3'); + assert_eq!(checkpoint2.value, 7); } // diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index 8ec87bb40..e449ff8b1 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -17,7 +17,7 @@ pub struct Trace { } /// Generic checkpoint representation. -#[derive(Copy, Drop, Serde, Debug, PartialEq)] +#[derive(Copy, Drop, Serde)] pub struct Checkpoint { pub key: u64, pub value: u256 From 23ef982a0b12f096e0164128cc1279e7e0ccefad Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Mon, 14 Oct 2024 18:20:35 -0400 Subject: [PATCH 37/44] refactor hook and improve docs --- docs/modules/ROOT/pages/governance.adoc | 61 ++++++++----------- .../governance/src/tests/test_votes.cairo | 2 +- packages/governance/src/votes/votes.cairo | 14 ++--- packages/test_common/src/mocks/votes.cairo | 31 +++------- 4 files changed, 41 insertions(+), 67 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index a008f27df..44d6d690b 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -8,8 +8,8 @@ :delegate_by_sig: xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig] :voting_units_trait: xref:api/governance.adoc#VotingUnitsTrait[VotingUnitsTrait] :votes-usage: xref:../governance.adoc#usage_2[usage] -:nonces-component: xref:api/utils.adoc#NoncesComponent[NoncesComponent] -:snip12-metadata: xref:api/utils.adoc#SNIP12Metadata[SNIP12Metadata] +:nonces-component: xref:api/utilities.adoc#NoncesComponent[NoncesComponent] +:snip12-metadata: xref:api/utilities.adoc#snip12[SNIP12Metadata] Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. @@ -215,15 +215,13 @@ IMPORTANT: The transferring of voting units must be handled by the implementing === Key Features -1. *Delegation*: Users can delegate their voting power to any address, including themselves. Vote power can be delegated either by calling -the {delegate} function directly, or by providing a signature to be used with -{delegate_by_sig}. -2. *Historical lookups*: The system keeps track of voting power at different points in time, allowing for accurate voting in proposals that span multiple blocks. +1. *Delegation*: Users can delegate their voting power to any address, including themselves. Vote power can be delegated either by calling the {delegate} function directly, or by providing a signature to be used with {delegate_by_sig}. +2. *Historical lookups*: The system keeps track of historical snapshots for each account, allowing to query the voting power of an account at a specific block timestamp. This can be used for example to determine the voting power of an account when a proposal was created, rather than using the current balance. === Usage -When integrating the Votes component, the `VotingUnitsTrait` must be implemented to get the voting units for a given account as a function of the implementing contract. + -For simplicity, this module already provides two implementations for ERC20 and ERC721 tokens, which will work out of the box if the respective components are integrated. + +When integrating the `VotesComponent`, the {voting_units_trait} must be implemented to get the voting units for a given account as a function of the implementing contract. + +For simplicity, this module already provides two implementations for `ERC20` and `ERC721` tokens, which will work out of the box if the respective components are integrated. + Additionally, you must implement the {nonces-component} and the {snip12-metadata} trait to enable delegation by signatures. Here's an example of how to structure a simple ERC20Votes contract: @@ -288,24 +286,18 @@ mod ERC20VotesContract { } } - // We need to call the VotesComponent::transfer_voting_units function - // after every mint, burn and transfer. - // For this, we use the ERC20Component::ERC20HooksTrait. - impl ERC20VotesHooksImpl< - TContractState, - impl Votes: VotesComponent::HasComponent, - impl HasComponent: ERC20Component::HasComponent, - +NoncesComponent::HasComponent, - +Drop - > of ERC20Component::ERC20HooksTrait { + // We need to call the `transfer_voting_units` function after + // every mint, burn and transfer. + // For this, we use the `after_update` hook of the `ERC20Component::ERC20HooksTrait`. + impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { fn after_update( - ref self: ERC20Component::ComponentState, + ref self: ERC20Component::ComponentState, from: ContractAddress, recipient: ContractAddress, amount: u256 ) { - let mut votes_component = get_dep_component_mut!(ref self, Votes); - votes_component.transfer_voting_units(from, recipient, amount); + let mut contract_state = ERC20Component::HasComponent::get_contract_mut(ref self); + contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); } } @@ -384,26 +376,25 @@ pub mod ERC721VotesContract { } } - impl ERC721VotesHooksImpl< - TContractState, - impl Votes: VotesComponent::HasComponent, - impl HasComponent: ERC721Component::HasComponent, - +NoncesComponent::HasComponent, - +SRC5Component::HasComponent, - +Drop - > of ERC721Component::ERC721HooksTrait { + // We need to call the `transfer_voting_units` function after + // every mint, burn and transfer. + // For this, we use the `before_update` hook of the + //`ERC721Component::ERC721HooksTrait`. + // This hook is called before the transfer is executed. + // This gives us access to the previous owner. + impl ERC721VotesHooksImpl of ERC721Component::ERC721HooksTrait { fn before_update( - ref self: ERC721Component::ComponentState, + ref self: ERC721Component::ComponentState, to: ContractAddress, token_id: u256, auth: ContractAddress ) { - let mut votes_component = get_dep_component_mut!(ref self, Votes); + let mut contract_state = ERC721Component::HasComponent::get_contract_mut(ref self); - // We use the internal function here since it does not check if the token id exists - // which is necessary for mints + // We use the internal function here since it does not check if the token + // id exists which is necessary for mints let previous_owner = self._owner_of(token_id); - votes_component.transfer_voting_units(previous_owner, to, 1); + contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1); } } @@ -421,7 +412,7 @@ This is the full interface of the `VotesImpl` implementation: ---- #[starknet::interface] pub trait VotesABI { - // Votes + // 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; diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 2cebb4ad6..7288179aa 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -376,7 +376,7 @@ fn test_checkpoints() { let checkpoint2 = state.checkpoints(DELEGATOR(), 2); assert_eq!(checkpoint2.key, 'ts3'); - assert_eq!(checkpoint2.value, 7); + assert_eq!(checkpoint2.value, 7); } // diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index f6e6d0e12..55ab1f5a5 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -75,21 +75,15 @@ use starknet::ContractAddress; /// } /// } /// -/// impl ERC20VotesHooksImpl< -/// TContractState, -/// impl Votes: VotesComponent::HasComponent, -/// impl HasComponent: ERC20Component::HasComponent, -/// +NoncesComponent::HasComponent, -/// +Drop -/// > of ERC20Component::ERC20HooksTrait { +/// impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { /// fn after_update( -/// ref self: ERC20Component::ComponentState, +/// ref self: ERC20Component::ComponentState, /// from: ContractAddress, /// recipient: ContractAddress, /// amount: u256 /// ) { -/// let mut votes_component = get_dep_component_mut!(ref self, Votes); -/// votes_component.transfer_voting_units(from, recipient, amount); +/// let mut contract_state = ERC20Component::HasComponent::get_contract_mut(ref self); +/// contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); /// } /// } /// diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index ef33bbc68..b8aa1959f 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -61,26 +61,21 @@ pub mod ERC721VotesMock { } } - impl ERC721VotesHooksImpl< - TContractState, - impl Votes: VotesComponent::HasComponent, - impl HasComponent: ERC721Component::HasComponent, - +NoncesComponent::HasComponent, - +SRC5Component::HasComponent, - +Drop - > of ERC721Component::ERC721HooksTrait { + impl ERC721VotesHooksImpl of ERC721Component::ERC721HooksTrait { + // We need to use the `before_update` hook to check the previous owner + // before the transfer is executed. fn before_update( - ref self: ERC721Component::ComponentState, + ref self: ERC721Component::ComponentState, to: ContractAddress, token_id: u256, auth: ContractAddress ) { - let mut votes_component = get_dep_component_mut!(ref self, Votes); + let mut contract_state = ERC721Component::HasComponent::get_contract_mut(ref self); // We use the internal function here since it does not check if the token id exists // which is necessary for mints let previous_owner = self._owner_of(token_id); - votes_component.transfer_voting_units(previous_owner, to, 1); + contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1); } } @@ -147,21 +142,15 @@ pub mod ERC20VotesMock { } } - impl ERC20VotesHooksImpl< - TContractState, - impl Votes: VotesComponent::HasComponent, - impl HasComponent: ERC20Component::HasComponent, - +NoncesComponent::HasComponent, - +Drop - > of ERC20Component::ERC20HooksTrait { + impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { fn after_update( - ref self: ERC20Component::ComponentState, + ref self: ERC20Component::ComponentState, from: ContractAddress, recipient: ContractAddress, amount: u256 ) { - let mut votes_component = get_dep_component_mut!(ref self, Votes); - votes_component.transfer_voting_units(from, recipient, amount); + let mut contract_state = ERC20Component::HasComponent::get_contract_mut(ref self); + contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); } } From ad2c11bae6a31ffb89c3a7d7f73597fe800cfda0 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 13:17:59 -0400 Subject: [PATCH 38/44] + --- docs/modules/ROOT/pages/api/governance.adoc | 46 ++++++++++- docs/modules/ROOT/pages/governance.adoc | 2 +- .../governance/src/tests/test_votes.cairo | 14 ++-- packages/governance/src/votes/interface.cairo | 4 +- packages/governance/src/votes/votes.cairo | 79 ++----------------- packages/utils/src/structs/checkpoint.cairo | 2 +- .../utils/src/tests/test_checkpoint.cairo | 6 +- 7 files changed, 61 insertions(+), 92 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 2e5e7a29a..234c409eb 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -670,7 +670,7 @@ Delegates votes from the sender to `delegatee`. [.contract-item] [[IVotes-delegate_by_sig]] -==== `[.contract-item-name]#++delegate_by_sig++#++(delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# +==== `[.contract-item-name]#++delegate_by_sig++#++(delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Span)++` [.item-kind]#external# Delegates votes from `delegator` to `delegatee`. @@ -692,10 +692,10 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait .Voting Units Trait Implementations -- .ERC20VotesImpl -* xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] +* xref:#VotesComponent-ERC20VotesImpl-get_voting_units[`++get_voting_units(self, account)++`] .ERC721VotesImpl -* xref:#VotingUnitsTrait-get_voting_units[`++get_voting_units(self, account)++`] +* xref:#VotesComponent-ERC721VotesImpl-get_voting_units[`++get_voting_units(self, account)++`] -- [.contract-index#VotesComponent-Embeddable-Impls] @@ -727,6 +727,44 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait * xref:#VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] -- +[#VotesComponent-VotingUnitsTrait-Implementations] +==== ERC20VotesImpl + +[.contract-item] +[[VotesComponent-ERC20VotesImpl-get_voting_units]] +==== `[.contract-item-name]#++get_voting_units++#++(self: @ComponentState, account: ContractAddress) → u256++` [.item-kind]#internal# + +Returns the number of voting units for a given account. + +This implementation is specific to ERC20 tokens, where the balance +of tokens directly represents the number of voting units. + +NOTE: This implementation will work out of the box if the ERC20 component +is implemented in the final contract. + +WARNING: This implementation assumes tokens map to voting units 1:1. +Any deviation from this formula when transferring voting units (e.g. by using hooks) +may compromise the internal vote accounting. + +==== ERC721VotesImpl + +[.contract-item] +[[VotesComponent-ERC721VotesImpl-get_voting_units]] +==== `[.contract-item-name]#++get_voting_units++#++(self: @ComponentState, account: ContractAddress) → u256++` [.item-kind]#internal# + +Returns the number of voting units for a given account. + +This implementation is specific to ERC721 tokens, where each token +represents one voting unit. The function returns the balance of +ERC721 tokens for the specified account. + +NOTE: This implementation will work out of the box if the ERC721 component +is implemented in the final contract. + +WARNING: This implementation assumes tokens map to voting units 1:1. +Any deviation from this formula when transferring voting units (e.g. by using hooks) +may compromise the internal vote accounting. + [#VotesComponent-Functions] ==== Functions @@ -778,7 +816,7 @@ May emit one or two {DelegateVotesChanged} events. [.contract-item] [[VotesComponent-delegate_by_sig]] -==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ComponentState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Array)++` [.item-kind]#external# +==== `[.contract-item-name]#++delegate_by_sig++#++(ref self: ComponentState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Span)++` [.item-kind]#external# Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 44d6d690b..c0f14df52 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -418,7 +418,7 @@ pub trait VotesABI { 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: Array); + fn delegate_by_sig(ref self: TState, delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Span); // INonces fn nonces(self: @TState, owner: ContractAddress) -> felt252; diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 7288179aa..684023680 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -222,7 +222,7 @@ fn test_delegate_by_sig() { // Set up event spy and execute delegation let mut spy = spy_events(); - state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s].span()); spy.assert_only_event_delegate_changed(contract_address, delegator, ZERO(), delegatee); assert_eq!(state.delegates(account), delegatee); @@ -264,7 +264,7 @@ fn test_delegate_by_sig_past_expiry() { let expiry = 'ts4'; let signature = array![0, 0]; - state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 0, expiry, signature); + state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 0, expiry, signature.span()); } #[test] @@ -273,7 +273,7 @@ fn test_delegate_by_sig_invalid_nonce() { let mut state = setup_erc721_votes(); let signature = array![0, 0]; - state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 1, 0, signature); + state.delegate_by_sig(DELEGATOR(), DELEGATEE(), 1, 0, signature.span()); } #[test] @@ -293,7 +293,7 @@ fn test_delegate_by_sig_invalid_signature() { start_cheat_block_timestamp_global('ts1'); // Use an invalid signature - state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r + 1, s]); + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r + 1, s].span()); } #[test] @@ -314,7 +314,7 @@ fn test_delegate_by_sig_bad_delegatee() { start_cheat_block_timestamp_global('ts1'); // Use a different delegatee than the one signed for - state.delegate_by_sig(delegator, bad_delegatee, nonce, expiry, array![r, s]); + state.delegate_by_sig(delegator, bad_delegatee, nonce, expiry, array![r, s].span()); } #[test] @@ -334,10 +334,10 @@ fn test_delegate_by_sig_reused_signature() { start_cheat_block_timestamp_global('ts1'); // First delegation (should succeed) - state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s].span()); // Attempt to reuse the same signature (should fail) - state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s]); + state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s].span()); } #[test] diff --git a/packages/governance/src/votes/interface.cairo b/packages/governance/src/votes/interface.cairo index fccbfe814..cd48494c2 100644 --- a/packages/governance/src/votes/interface.cairo +++ b/packages/governance/src/votes/interface.cairo @@ -33,7 +33,7 @@ pub trait IVotes { delegatee: ContractAddress, nonce: felt252, expiry: u64, - signature: Array + signature: Span ); } @@ -52,7 +52,7 @@ pub trait VotesABI { delegatee: ContractAddress, nonce: felt252, expiry: u64, - signature: Array + signature: Span ); // Nonces diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 55ab1f5a5..a2b40bb4b 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -21,78 +21,9 @@ use starknet::ContractAddress; /// whenever a transfer, mint, or burn operation is performed. Hooks can be leveraged for this /// purpose, as shown in the following ERC20 example: /// -/// ```cairo -/// #[starknet::contract] -/// pub mod ERC20VotesContract { -/// use openzeppelin_governance::votes::VotesComponent; -/// use openzeppelin_token::erc20::ERC20Component; -/// 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); -/// -/// #[abi(embed_v0)] -/// impl VotesImpl = VotesComponent::VotesImpl; -/// impl VotesInternalImpl = VotesComponent::InternalImpl; -/// -/// #[abi(embed_v0)] -/// impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; -/// impl ERC20InternalImpl = ERC20Component::InternalImpl; -/// -/// #[abi(embed_v0)] -/// impl NoncesImpl = NoncesComponent::NoncesImpl; -/// -/// #[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 -/// } -/// -/// pub impl SNIP12MetadataImpl of SNIP12Metadata { -/// fn name() -> felt252 { -/// 'DAPP_NAME' -/// } -/// fn version() -> felt252 { -/// 'DAPP_VERSION' -/// } -/// } -/// -/// impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { -/// fn after_update( -/// ref self: ERC20Component::ComponentState, -/// from: ContractAddress, -/// recipient: ContractAddress, -/// amount: u256 -/// ) { -/// let mut contract_state = ERC20Component::HasComponent::get_contract_mut(ref self); -/// contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); -/// } -/// } -/// -/// #[constructor] -/// fn constructor(ref self: ContractState) { -/// self.erc20.initializer("MyToken", "MTK"); -/// } -/// } -/// ``` +/// See [the documentation] +/// (https://docs.openzeppelin.com/contracts-cairo/0.17.0/governance.html#usage_2) +/// for examples and more details. #[starknet::component] pub mod VotesComponent { use core::num::traits::Zero; @@ -223,7 +154,7 @@ pub mod VotesComponent { delegatee: ContractAddress, nonce: felt252, expiry: u64, - signature: Array + signature: Span ) { assert(starknet::get_block_timestamp() <= expiry, Errors::EXPIRED_SIGNATURE); @@ -236,7 +167,7 @@ pub mod VotesComponent { let hash = delegation.get_message_hash(delegator); let is_valid_signature_felt = ISRC6Dispatcher { contract_address: delegator } - .is_valid_signature(hash, signature); + .is_valid_signature(hash, signature.into()); // Check either 'VALID' or true for backwards compatibility. let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index e449ff8b1..b0fa3b5bd 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -171,7 +171,7 @@ impl CheckpointImpl of CheckpointTrait { /// - `key` is stored at range [4,67] bits (0-indexed), taking the most significant usable bits. /// - `value.low` is stored at range [124, 251], taking the less significant bits (at the end). /// - `value.high` is stored as the second tuple element. -pub(crate) impl CheckpointStorePacking of StorePacking { +impl CheckpointStorePacking of StorePacking { fn pack(value: Checkpoint) -> (felt252, felt252) { let checkpoint = value; // shift-left by 184 bits diff --git a/packages/utils/src/tests/test_checkpoint.cairo b/packages/utils/src/tests/test_checkpoint.cairo index 45eee91b0..3f3a45d02 100644 --- a/packages/utils/src/tests/test_checkpoint.cairo +++ b/packages/utils/src/tests/test_checkpoint.cairo @@ -1,7 +1,7 @@ use core::num::traits::Bounded; use crate::structs::checkpoint::Checkpoint; -use crate::structs::checkpoint::CheckpointStorePacking; use openzeppelin_test_common::mocks::checkpoint::{IMockTrace, MockTrace}; +use starknet::storage_access::StorePacking; const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; const KEY_MASK: u256 = 0xffffffffffffffff; @@ -81,7 +81,7 @@ fn test_pack_big_key_and_value() { let value = Bounded::MAX; let checkpoint = Checkpoint { key, value }; - let (key_and_low, high) = CheckpointStorePacking::pack(checkpoint); + let (key_and_low, high) = StorePacking::pack(checkpoint); let expected_key: u256 = (key_and_low.into() / _2_POW_184.into()) & KEY_MASK; let expected_low: u256 = key_and_low.into() & LOW_MASK; @@ -97,7 +97,7 @@ fn test_unpack_big_key_and_value() { let key_and_low = Bounded::::MAX.into() * _2_POW_184 + Bounded::::MAX.into(); let high = Bounded::::MAX.into(); - let checkpoint = CheckpointStorePacking::unpack((key_and_low, high)); + let checkpoint: Checkpoint = StorePacking::unpack((key_and_low, high)); let expected_key: u64 = Bounded::MAX; let expected_value: u256 = Bounded::MAX; From 37ddb41f03cf6d71c9e5ee21f4baac062cae851d Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 17:20:37 -0400 Subject: [PATCH 39/44] + --- docs/modules/ROOT/pages/api/governance.adoc | 30 +++++++-- docs/modules/ROOT/pages/governance.adoc | 3 +- packages/governance/Scarb.toml | 4 +- packages/governance/src/votes/votes.cairo | 8 +-- packages/test_common/src/mocks/erc20.cairo | 66 +++++++++++++++++++ packages/token/src/erc20/snip12_utils.cairo | 1 - .../token/src/erc20/snip12_utils/votes.cairo | 26 -------- packages/token/src/tests/erc20.cairo | 2 +- 8 files changed, 100 insertions(+), 40 deletions(-) delete mode 100644 packages/token/src/erc20/snip12_utils/votes.cairo diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index 234c409eb..cbc3a8cc5 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -672,7 +672,7 @@ Delegates votes from the sender to `delegatee`. [[IVotes-delegate_by_sig]] ==== `[.contract-item-name]#++delegate_by_sig++#++(delegator: ContractAddress, delegatee: ContractAddress, nonce: felt252, expiry: u64, signature: Span)++` [.item-kind]#external# -Delegates votes from `delegator` to `delegatee`. +Delegates votes from `delegator` to `delegatee` through a SNIP12 message signature validation. [.contract] [[VotesComponent]] @@ -715,9 +715,12 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait .Internal implementations -- .InternalImpl +* xref:#VotesComponent-get_total_supply[`++get_total_supply(self)++`] * xref:#VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] * xref:#VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] * xref:#VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] +* xref:#VotesComponent-num_checkpoints[`++num_checkpoints(self, account)++`] +* xref:#VotesComponent-checkpoints[`++checkpoints(self, account, pos)++`] -- [.contract-index] @@ -727,7 +730,7 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait * xref:#VotesComponent-DelegateVotesChanged[`++DelegateVotesChanged(delegate, previous_votes, new_votes)++`] -- -[#VotesComponent-VotingUnitsTrait-Implementations] +[#VotesComponent-ERC20VotesImpl] ==== ERC20VotesImpl [.contract-item] @@ -746,6 +749,7 @@ WARNING: This implementation assumes tokens map to voting units 1:1. Any deviation from this formula when transferring voting units (e.g. by using hooks) may compromise the internal vote accounting. +[#VotesComponent-ERC721VotesImpl] ==== ERC721VotesImpl [.contract-item] @@ -834,6 +838,12 @@ May emit one or two {DelegateVotesChanged} events. [#VotesComponent-Internal-functions] ==== Internal functions +[.contract-item] +[[VotesComponent-get_total_supply]] +==== `[.contract-item-name]#++get_total_supply++#++(self: @ComponentState) → u256++` [.item-kind]#internal# + +Returns the current total supply of votes. + [.contract-item] [[VotesComponent-_delegate]] ==== `[.contract-item-name]#++_delegate++#++(ref self: ComponentState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# @@ -861,12 +871,22 @@ Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` should be zero. Total supply of voting units will be adjusted with mints and burns. -CAUTION: If voting units are a function of an underlying transferrable asset (like a token), this function should be -called every time the underlying asset is transferred to keep the internal accounting of voting power in sync. For -`ERC20` and `ERC721` tokens, this is usually done using hooks. +WARNING: If voting units are based on an underlying transferable asset (like a token), you must call this function every time the asset is transferred to keep the internal voting power accounting in sync. For ERC20 and ERC721 tokens, this is typically handled using hooks. May emit one or two {DelegateVotesChanged} events. +[.contract-item] +[[VotesComponent-num_checkpoints]] +==== `[.contract-item-name]#++num_checkpoints++#++(self: @ComponentState, account: ContractAddress) → u64++` [.item-kind]#internal# + +Returns the number of checkpoints for `account`. + +[.contract-item] +[[VotesComponent-checkpoints]] +==== `[.contract-item-name]#++checkpoints++#++(self: @ComponentState, account: ContractAddress, pos: u64) → Checkpoint++` [.item-kind]#internal# + +Returns the `pos`-th checkpoint for `account`. + [#VotesComponent-Events] ==== Events diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index c0f14df52..90c23b916 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -216,7 +216,8 @@ IMPORTANT: The transferring of voting units must be handled by the implementing === Key Features 1. *Delegation*: Users can delegate their voting power to any address, including themselves. Vote power can be delegated either by calling the {delegate} function directly, or by providing a signature to be used with {delegate_by_sig}. -2. *Historical lookups*: The system keeps track of historical snapshots for each account, allowing to query the voting power of an account at a specific block timestamp. This can be used for example to determine the voting power of an account when a proposal was created, rather than using the current balance. +2. *Historical lookups*: The system keeps track of historical snapshots for each account, which allows the voting power of an account to be queried at a specific timestamp. + +This can be used for example to determine the voting power of an account when a proposal was created, rather than using the current balance. === Usage diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index 907044d2d..5ab2fc5d2 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -25,8 +25,8 @@ fmt.workspace = true starknet.workspace = true openzeppelin_access = { path = "../access" } openzeppelin_introspection = { path = "../introspection" } -openzeppelin_account = { path = "../account"} -openzeppelin_token = { path= "../token"} +openzeppelin_account = { path = "../account" } +openzeppelin_token = { path= "../token" } [dev-dependencies] assert_macros.workspace = true diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index a2b40bb4b..0f6414c1b 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -300,10 +300,10 @@ pub mod VotesComponent { /// To register a mint, `from` should be zero. To register a burn, `to` /// should be zero. Total supply of voting units will be adjusted with mints and burns. /// - /// WARNING: If voting units are a function of an underlying transferrable asset (like a - /// token), this function should be called every time the underlying asset is transferred to - /// keep the internal accounting of voting power in sync. For ERC20 and ERC721 tokens, this - /// is usually done using hooks. + /// WARNING: If voting units are based on an underlying transferable asset (like a token), + /// you must call this function every time the asset is transferred to keep the internal + /// voting power accounting in sync. For ERC20 and ERC721 tokens, this is typically handled + /// using hooks. /// /// May emit one or two `DelegateVotesChanged` events. fn transfer_voting_units( diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index 3df5d01df..a77d3275c 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -77,3 +77,69 @@ pub mod SnakeERC20Mock { self.erc20.mint(recipient, initial_supply); } } + +#[starknet::contract] +pub mod DualCaseERC20PermitMock { + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_utils::cryptography::nonces::NoncesComponent; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // ERC20Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + // IERC20Permit + #[abi(embed_v0)] + impl ERC20PermitImpl = ERC20Component::ERC20PermitImpl; + + // ISNIP12Metadata + #[abi(embed_v0)] + impl SNIP12MetadataExternal = + ERC20Component::SNIP12MetadataExternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage, + #[substorage(v0)] + pub nonces: NoncesComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum 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' + } + } + + /// Sets the token `name` and `symbol`. + /// Mints `fixed_supply` tokens to `recipient`. + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } +} diff --git a/packages/token/src/erc20/snip12_utils.cairo b/packages/token/src/erc20/snip12_utils.cairo index 9b562ed1d..e3540a5d5 100644 --- a/packages/token/src/erc20/snip12_utils.cairo +++ b/packages/token/src/erc20/snip12_utils.cairo @@ -1,2 +1 @@ pub mod permit; -pub mod votes; diff --git a/packages/token/src/erc20/snip12_utils/votes.cairo b/packages/token/src/erc20/snip12_utils/votes.cairo deleted file mode 100644 index f611bdaaa..000000000 --- a/packages/token/src/erc20/snip12_utils/votes.cairo +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/snip12_utils/votes.cairo) - -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; -use openzeppelin_utils::cryptography::snip12::StructHash; -use starknet::ContractAddress; - -// sn_keccak("\"Delegation\"(\"delegatee\":\"ContractAddress\",\"nonce\":\"felt\",\"expiry\":\"u128\")") -// -// Since there's no u64 type in SNIP-12, we use u128 for `expiry` in the type hash generation. -pub const DELEGATION_TYPE_HASH: felt252 = - 0x241244ac7acec849adc6df9848262c651eb035a3add56e7f6c7bcda6649e837; - -#[derive(Copy, Drop, Hash)] -pub struct Delegation { - pub delegatee: ContractAddress, - pub nonce: felt252, - pub expiry: u64 -} - -impl StructHashImpl of StructHash { - fn hash_struct(self: @Delegation) -> felt252 { - PoseidonTrait::new().update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() - } -} diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index 4b910a60e..841c19428 100644 --- a/packages/token/src/tests/erc20.cairo +++ b/packages/token/src/tests/erc20.cairo @@ -1,2 +1,2 @@ mod test_erc20; -mod test_erc20_permit; \ No newline at end of file +mod test_erc20_permit; From 879ced3c99a4668751a5d4d0b57bcb287b4b6cab Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 17:54:14 -0400 Subject: [PATCH 40/44] improve tests --- .../governance/src/tests/test_votes.cairo | 157 +++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 684023680..6273cdd6e 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -10,10 +10,12 @@ use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, DELEGATOR, DELEGATEE, OTHER, RECIPIENT}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_token::erc20::ERC20Component::InternalTrait; +use openzeppelin_token::erc20::interface::IERC20; use openzeppelin_token::erc721::ERC721Component::{ ERC721MetadataImpl, InternalImpl as ERC721InternalImpl, }; use openzeppelin_token::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; +use openzeppelin_token::erc721::interface::IERC721; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use openzeppelin_utils::structs::checkpoint::TraceTrait; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; @@ -78,6 +80,10 @@ fn setup_account(public_key: felt252) -> ContractAddress { // Common tests for Votes // +// +// get_votes +// + #[test] fn test_get_votes() { let mut state = setup_erc721_votes(); @@ -89,6 +95,10 @@ fn test_get_votes() { assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT); } +// +// get_past_votes +// + #[test] fn test_get_past_votes() { let mut state = setup_erc721_votes(); @@ -116,6 +126,10 @@ fn test_get_past_votes_future_lookup() { state.get_past_votes(DELEGATOR(), 'ts2'); } +// +// get_past_total_supply +// + #[test] fn test_get_past_total_supply() { let mut state = setup_erc721_votes(); @@ -154,6 +168,21 @@ fn test_get_past_total_supply_future_lookup() { state.get_past_total_supply('ts2'); } +// +// delegates +// + +#[test] +fn test_delegates() { + let mut state = setup_erc721_votes(); + state.delegate(DELEGATOR()); + assert_eq!(state.delegates(DELEGATOR()), DELEGATOR()); +} + +// +// delegate +// + #[test] fn test_self_delegate() { let mut state = setup_erc721_votes(); @@ -171,7 +200,7 @@ fn test_self_delegate() { } #[test] -fn test_delegate_to_recipient_updates_votes() { +fn test_delegate_to_delegatee_updates_votes() { let mut state = setup_erc721_votes(); let contract_address = test_address(); let mut spy = spy_events(); @@ -188,7 +217,7 @@ fn test_delegate_to_recipient_updates_votes() { } #[test] -fn test_delegate_to_recipient_updates_delegates() { +fn test_delegate_to_delegatee_updates_delegates() { let mut state = setup_erc721_votes(); start_cheat_caller_address(test_address(), DELEGATOR()); state.delegate(DELEGATOR()); @@ -197,6 +226,29 @@ fn test_delegate_to_recipient_updates_delegates() { assert_eq!(state.delegates(DELEGATOR()), DELEGATEE()); } +#[test] +fn test_delegate_with_no_balance() { + let mut state = setup_erc721_votes(); + let contract_address = test_address(); + let mut spy = spy_events(); + start_cheat_caller_address(contract_address, OTHER()); + + // OTHER has no balance, so delegating should not change any votes + state.delegate(DELEGATEE()); + + spy.assert_event_delegate_changed(contract_address, OTHER(), ZERO(), DELEGATEE()); + // No DelegateVotesChanged event should be emitted + spy.assert_no_events_left_from(contract_address); + + assert_eq!(state.get_votes(DELEGATEE()), 0); + assert_eq!(state.get_votes(OTHER()), 0); + assert_eq!(state.delegates(OTHER()), DELEGATEE()); +} + +// +// delegate_by_sig +// + #[test] fn test_delegate_by_sig() { // Set up the state @@ -340,6 +392,10 @@ fn test_delegate_by_sig_reused_signature() { state.delegate_by_sig(delegator, delegatee, nonce, expiry, array![r, s].span()); } +// +// num_checkpoints +// + #[test] fn test_num_checkpoints() { let mut state = setup_erc721_votes(); @@ -355,6 +411,10 @@ fn test_num_checkpoints() { assert_eq!(state.num_checkpoints(OTHER()), 0); } +// +// checkpoints +// + #[test] fn test_checkpoints() { let mut state = setup_erc721_votes(); @@ -467,6 +527,99 @@ fn test_erc_20_get_total_supply() { assert_eq!(state.get_total_supply(), SUPPLY); } +#[test] +fn test_erc_20_voting_units_update_with_full_balance_transfer() { + let mut state = setup_erc20_votes(); + let mut mock_state = ERC20VOTES_CONTRACT_STATE(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, DELEGATOR()); + + // DELEGATOR self-delegates + state.delegate(DELEGATOR()); + assert_eq!(state.get_votes(DELEGATOR()), SUPPLY); + + let mut spy = spy_events(); + + // Full balance transfer + mock_state.erc20.transfer(RECIPIENT(), SUPPLY); + + spy.assert_event_delegate_votes_changed(contract_address, DELEGATOR(), SUPPLY, 0); + assert_eq!(state.get_votes(DELEGATOR()), 0); + assert_eq!(state.get_votes(RECIPIENT()), 0); // RECIPIENT hasn't delegated yet + + // RECIPIENT delegates to themselves + start_cheat_caller_address(contract_address, RECIPIENT()); + state.delegate(RECIPIENT()); + + spy.assert_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, SUPPLY); + assert_eq!(state.get_votes(RECIPIENT()), SUPPLY); +} + +#[test] +fn test_erc_20_voting_units_update_with_partial_balance_transfer() { + let mut state = setup_erc20_votes(); + let mut mock_state = ERC20VOTES_CONTRACT_STATE(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, DELEGATOR()); + + // DELEGATOR self-delegates + state.delegate(DELEGATOR()); + assert_eq!(state.get_votes(DELEGATOR()), SUPPLY); + + let mut spy = spy_events(); + + // Partial transfer + let partial_amount = SUPPLY / 2; + mock_state.erc20.transfer(RECIPIENT(), partial_amount); + + spy + .assert_event_delegate_votes_changed( + contract_address, DELEGATOR(), SUPPLY, SUPPLY - partial_amount + ); + assert_eq!(state.get_votes(DELEGATOR()), SUPPLY - partial_amount); + assert_eq!(state.get_votes(RECIPIENT()), 0); // RECIPIENT hasn't delegated yet + + // RECIPIENT delegates to themselves + start_cheat_caller_address(contract_address, RECIPIENT()); + state.delegate(RECIPIENT()); + + spy.assert_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, partial_amount); + assert_eq!(state.get_votes(RECIPIENT()), partial_amount); +} + +#[test] +fn test_erc721_voting_units_update_with_single_token_transfer() { + let mut state = setup_erc721_votes(); + let mut mock_state = ERC721VOTES_CONTRACT_STATE(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, DELEGATOR()); + + // DELEGATOR self-delegates + state.delegate(DELEGATOR()); + assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT); + + let mut spy = spy_events(); + + // Transfer a single token + let token_id = 0; + mock_state.erc721.transfer_from(DELEGATOR(), RECIPIENT(), token_id); + + spy + .assert_event_delegate_votes_changed( + contract_address, DELEGATOR(), ERC721_INITIAL_MINT, ERC721_INITIAL_MINT - 1 + ); + + assert_eq!(state.get_votes(DELEGATOR()), ERC721_INITIAL_MINT - 1); + assert_eq!(state.get_votes(RECIPIENT()), 0); // RECIPIENT hasn't delegated yet + + // RECIPIENT delegates to themselves + start_cheat_caller_address(contract_address, RECIPIENT()); + state.delegate(RECIPIENT()); + + spy.assert_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, 1); + assert_eq!(state.get_votes(RECIPIENT()), 1); +} + // // Helpers // From 0fe82ebdb9877d1c23dfdebb068418d63e285623 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 18:11:13 -0400 Subject: [PATCH 41/44] fix test --- packages/governance/src/tests/test_votes.cairo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 6273cdd6e..273348a72 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -175,6 +175,9 @@ fn test_get_past_total_supply_future_lookup() { #[test] fn test_delegates() { let mut state = setup_erc721_votes(); + let contract_address = test_address(); + start_cheat_caller_address(contract_address, DELEGATOR()); + state.delegate(DELEGATOR()); assert_eq!(state.delegates(DELEGATOR()), DELEGATOR()); } From 865919e8d4037748a778b92f548d11fabe7f0ae3 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 18:19:15 -0400 Subject: [PATCH 42/44] fmt --- packages/governance/src/tests/test_votes.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/tests/test_votes.cairo b/packages/governance/src/tests/test_votes.cairo index 273348a72..6e413df25 100644 --- a/packages/governance/src/tests/test_votes.cairo +++ b/packages/governance/src/tests/test_votes.cairo @@ -177,7 +177,7 @@ fn test_delegates() { let mut state = setup_erc721_votes(); let contract_address = test_address(); start_cheat_caller_address(contract_address, DELEGATOR()); - + state.delegate(DELEGATOR()); assert_eq!(state.delegates(DELEGATOR()), DELEGATOR()); } From 84b2aec266065bdf0f91009f04fa90aecc3aa7da Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 19:09:11 -0400 Subject: [PATCH 43/44] Update CHANGELOG.md Co-authored-by: Eric Nordelo --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad2b4bec..d3e4bdc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove `ERC20Votes` component in favor of `VotesComponent` (#1114) - `Trace` is now declared as a `storage_node` and now uses `Vec` instead of `StorageArray`. + - `delegate_by_sig` `signature` param in the `IVotes` interface updated from `Array` to `Span`. - Remove `StorageArray` from `openzeppelin_utils` (#1114) - Bump snforge to 0.31.0 - Remove openzeppelin_utils::selectors (#1163) From 1f1d549485f162d4e88fd0478cda30ff2a2dd0e6 Mon Sep 17 00:00:00 2001 From: Gustavo Gonzalez Date: Tue, 15 Oct 2024 19:15:49 -0400 Subject: [PATCH 44/44] move _delegate --- docs/modules/ROOT/pages/api/governance.adoc | 22 ++++++------- packages/governance/src/votes/votes.cairo | 36 ++++++++++----------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/modules/ROOT/pages/api/governance.adoc b/docs/modules/ROOT/pages/api/governance.adoc index cbc3a8cc5..69415a386 100644 --- a/docs/modules/ROOT/pages/api/governance.adoc +++ b/docs/modules/ROOT/pages/api/governance.adoc @@ -716,11 +716,11 @@ NOTE: When using this module, your contract must implement the {VotingUnitsTrait -- .InternalImpl * xref:#VotesComponent-get_total_supply[`++get_total_supply(self)++`] -* xref:#VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] * xref:#VotesComponent-move_delegate_votes[`++move_delegate_votes(self, from, to, amount)++`] * xref:#VotesComponent-transfer_voting_units[`++transfer_voting_units(self, from, to, amount)++`] * xref:#VotesComponent-num_checkpoints[`++num_checkpoints(self, account)++`] * xref:#VotesComponent-checkpoints[`++checkpoints(self, account, pos)++`] +* xref:#VotesComponent-_delegate[`++_delegate(self, account, delegatee)++`] -- [.contract-index] @@ -844,16 +844,6 @@ May emit one or two {DelegateVotesChanged} events. Returns the current total supply of votes. -[.contract-item] -[[VotesComponent-_delegate]] -==== `[.contract-item-name]#++_delegate++#++(ref self: ComponentState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# - -Delegates all of ``account``'s voting units to `delegatee`. - -Emits a {DelegateChanged} event. - -May emit one or two {DelegateVotesChanged} events. - [.contract-item] [[VotesComponent-move_delegate_votes]] ==== `[.contract-item-name]#++move_delegate_votes++#++(ref self: ComponentState, from: ContractAddress, to: ContractAddress, amount: u256)++` [.item-kind]#internal# @@ -887,6 +877,16 @@ Returns the number of checkpoints for `account`. Returns the `pos`-th checkpoint for `account`. +[.contract-item] +[[VotesComponent-_delegate]] +==== `[.contract-item-name]#++_delegate++#++(ref self: ComponentState, account: ContractAddress, delegatee: ContractAddress)++` [.item-kind]#internal# + +Delegates all of ``account``'s voting units to `delegatee`. + +Emits a {DelegateChanged} event. + +May emit one or two {DelegateVotesChanged} events. + [#VotesComponent-Events] ==== Events diff --git a/packages/governance/src/votes/votes.cairo b/packages/governance/src/votes/votes.cairo index 0f6414c1b..93e2e3ced 100644 --- a/packages/governance/src/votes/votes.cairo +++ b/packages/governance/src/votes/votes.cairo @@ -251,24 +251,6 @@ pub mod VotesComponent { self.Votes_total_checkpoints.deref().latest() } - /// Delegates all of `account`'s voting units to `delegatee`. - /// - /// Emits a `DelegateChanged` event. - /// May emit one or two `DelegateVotesChanged` events. - fn _delegate( - ref self: ComponentState, - account: ContractAddress, - delegatee: ContractAddress - ) { - let from_delegate = self.delegates(account); - self.Votes_delegatee.write(account, delegatee); - self - .emit( - DelegateChanged { delegator: account, from_delegate, to_delegate: delegatee } - ); - self.move_delegate_votes(from_delegate, delegatee, self.get_voting_units(account)); - } - /// Moves delegated votes from one delegate to another. /// /// May emit one or two `DelegateVotesChanged` events. @@ -335,6 +317,24 @@ pub mod VotesComponent { ) -> Checkpoint { self.Votes_delegate_checkpoints.entry(account).at(pos) } + + /// Delegates all of `account`'s voting units to `delegatee`. + /// + /// Emits a `DelegateChanged` event. + /// May emit one or two `DelegateVotesChanged` events. + fn _delegate( + ref self: ComponentState, + account: ContractAddress, + delegatee: ContractAddress + ) { + let from_delegate = self.delegates(account); + self.Votes_delegatee.write(account, delegatee); + self + .emit( + DelegateChanged { delegator: account, from_delegate, to_delegate: delegatee } + ); + self.move_delegate_votes(from_delegate, delegatee, self.get_voting_units(account)); + } } }