From 8277877a05f5baed9ddf6d318911e4dcca07a40b Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 13 Sep 2024 18:12:39 +0200 Subject: [PATCH 01/76] feat: add features --- Scarb.toml | 3 +++ packages/access/Scarb.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Scarb.toml b/Scarb.toml index e5b802347..11513e588 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -74,3 +74,6 @@ sort-module-level-items = true [tool] fmt.workspace = true + +[features] +mock_contract = [] diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 5b1cc7a09..ccc8969e9 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -32,3 +32,6 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[features] +mock_contract = [] From 14b4b96ff760d80618dbc98bfd93d97191c1c752 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 25 Sep 2024 13:13:52 +0200 Subject: [PATCH 02/76] feat: remove access dual dispatchers (#1154) --- packages/access/src/accesscontrol.cairo | 1 - .../accesscontrol/dual_accesscontrol.cairo | 88 ------ packages/access/src/ownable.cairo | 1 - .../access/src/ownable/dual_ownable.cairo | 55 ---- packages/access/src/tests.cairo | 4 - packages/access/src/tests/mocks.cairo | 1 - .../src/tests/mocks/accesscontrol_mocks.cairo | 185 ----------- .../tests/mocks/non_implementing_mock.cairo | 10 - .../src/tests/mocks/ownable_mocks.cairo | 129 -------- .../src/tests/test_dual_accesscontrol.cairo | 290 ------------------ .../access/src/tests/test_dual_ownable.cairo | 173 ----------- 11 files changed, 937 deletions(-) delete mode 100644 packages/access/src/accesscontrol/dual_accesscontrol.cairo delete mode 100644 packages/access/src/ownable/dual_ownable.cairo delete mode 100644 packages/access/src/tests/mocks/non_implementing_mock.cairo delete mode 100644 packages/access/src/tests/test_dual_accesscontrol.cairo delete mode 100644 packages/access/src/tests/test_dual_ownable.cairo diff --git a/packages/access/src/accesscontrol.cairo b/packages/access/src/accesscontrol.cairo index 7b977e72b..8bfc6fc0b 100644 --- a/packages/access/src/accesscontrol.cairo +++ b/packages/access/src/accesscontrol.cairo @@ -1,5 +1,4 @@ pub mod accesscontrol; -pub mod dual_accesscontrol; pub mod interface; pub use accesscontrol::AccessControlComponent; diff --git a/packages/access/src/accesscontrol/dual_accesscontrol.cairo b/packages/access/src/accesscontrol/dual_accesscontrol.cairo deleted file mode 100644 index 33f080ca2..000000000 --- a/packages/access/src/accesscontrol/dual_accesscontrol.cairo +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.16.0 (access/accesscontrol/dual_accesscontrol.cairo) - -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use openzeppelin_utils::unwrap_and_cast::UnwrapAndCast; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseAccessControl { - pub contract_address: ContractAddress -} - -pub trait DualCaseAccessControlTrait { - fn has_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress) -> bool; - fn get_role_admin(self: @DualCaseAccessControl, role: felt252) -> felt252; - fn grant_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress); - fn revoke_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress); - fn renounce_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress); - fn supports_interface(self: @DualCaseAccessControl, interface_id: felt252) -> bool; -} - -impl DualCaseAccessControlImpl of DualCaseAccessControlTrait { - fn has_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress) -> bool { - let mut args = array![]; - args.append_serde(role); - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::has_role, selectors::hasRole, args.span() - ) - .unwrap_and_cast() - } - - fn get_role_admin(self: @DualCaseAccessControl, role: felt252) -> felt252 { - let mut args = array![]; - args.append_serde(role); - - try_selector_with_fallback( - *self.contract_address, selectors::get_role_admin, selectors::getRoleAdmin, args.span() - ) - .unwrap_and_cast() - } - - fn grant_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress) { - let mut args = array![]; - args.append_serde(role); - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::grant_role, selectors::grantRole, args.span() - ) - .unwrap_syscall(); - } - - fn revoke_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress) { - let mut args = array![]; - args.append_serde(role); - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::revoke_role, selectors::revokeRole, args.span() - ) - .unwrap_syscall(); - } - - fn renounce_role(self: @DualCaseAccessControl, role: felt252, account: ContractAddress) { - let mut args = array![]; - args.append_serde(role); - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::renounce_role, selectors::renounceRole, args.span() - ) - .unwrap_syscall(); - } - - fn supports_interface(self: @DualCaseAccessControl, interface_id: felt252) -> bool { - let mut args = array![]; - args.append_serde(interface_id); - - call_contract_syscall(*self.contract_address, selectors::supports_interface, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/access/src/ownable.cairo b/packages/access/src/ownable.cairo index a2a3a87f5..eb12459d9 100644 --- a/packages/access/src/ownable.cairo +++ b/packages/access/src/ownable.cairo @@ -1,4 +1,3 @@ -pub mod dual_ownable; pub mod interface; pub mod ownable; diff --git a/packages/access/src/ownable/dual_ownable.cairo b/packages/access/src/ownable/dual_ownable.cairo deleted file mode 100644 index 223fc022e..000000000 --- a/packages/access/src/ownable/dual_ownable.cairo +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.16.0 (access/ownable/dual_ownable.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseOwnable { - pub contract_address: ContractAddress -} - -pub trait DualCaseOwnableTrait { - fn owner(self: @DualCaseOwnable) -> ContractAddress; - fn transfer_ownership(self: @DualCaseOwnable, new_owner: ContractAddress); - fn renounce_ownership(self: @DualCaseOwnable); -} - -impl DualCaseOwnableImpl of DualCaseOwnableTrait { - fn owner(self: @DualCaseOwnable) -> ContractAddress { - let args = array![]; - - call_contract_syscall(*self.contract_address, selectors::owner, args.span()) - .unwrap_and_cast() - } - - fn transfer_ownership(self: @DualCaseOwnable, new_owner: ContractAddress) { - let mut args = array![]; - args.append_serde(new_owner); - - try_selector_with_fallback( - *self.contract_address, - selectors::transfer_ownership, - selectors::transferOwnership, - args.span() - ) - .unwrap_syscall(); - } - - fn renounce_ownership(self: @DualCaseOwnable) { - let args = array![]; - - try_selector_with_fallback( - *self.contract_address, - selectors::renounce_ownership, - selectors::renounceOwnership, - args.span() - ) - .unwrap_syscall(); - } -} diff --git a/packages/access/src/tests.cairo b/packages/access/src/tests.cairo index a7b4dadfe..4cb72b65b 100644 --- a/packages/access/src/tests.cairo +++ b/packages/access/src/tests.cairo @@ -3,10 +3,6 @@ pub(crate) mod mocks; #[cfg(test)] mod test_accesscontrol; #[cfg(test)] -mod test_dual_accesscontrol; -#[cfg(test)] -mod test_dual_ownable; -#[cfg(test)] mod test_ownable; #[cfg(test)] mod test_ownable_twostep; diff --git a/packages/access/src/tests/mocks.cairo b/packages/access/src/tests/mocks.cairo index f3eca4c97..4f4ebc2d9 100644 --- a/packages/access/src/tests/mocks.cairo +++ b/packages/access/src/tests/mocks.cairo @@ -1,3 +1,2 @@ pub(crate) mod accesscontrol_mocks; -pub(crate) mod non_implementing_mock; pub(crate) mod ownable_mocks; diff --git a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo b/packages/access/src/tests/mocks/accesscontrol_mocks.cairo index 09eee34b1..13f0b8722 100644 --- a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo +++ b/packages/access/src/tests/mocks/accesscontrol_mocks.cairo @@ -37,188 +37,3 @@ pub(crate) mod DualCaseAccessControlMock { self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); } } - -#[starknet::contract] -pub(crate) mod SnakeAccessControlMock { - use crate::accesscontrol::AccessControlComponent; - use crate::accesscontrol::DEFAULT_ADMIN_ROLE; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // AccessControl - #[abi(embed_v0)] - impl AccessControlImpl = - AccessControlComponent::AccessControlImpl; - impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; - - // SCR5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub accesscontrol: AccessControlComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - AccessControlEvent: AccessControlComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, admin: ContractAddress) { - self.accesscontrol.initializer(); - self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); - } -} - -#[starknet::contract] -pub(crate) mod CamelAccessControlMock { - use crate::accesscontrol::AccessControlComponent; - use crate::accesscontrol::DEFAULT_ADMIN_ROLE; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // AccessControl - #[abi(embed_v0)] - impl AccessControlCamelImpl = - AccessControlComponent::AccessControlCamelImpl; - impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; - - // SCR5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub accesscontrol: AccessControlComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - AccessControlEvent: AccessControlComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, admin: ContractAddress) { - self.accesscontrol.initializer(); - self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); - } -} - -// Although these modules are designed to panic, functions -// still need a valid return value. We chose: -// -// 3 for felt252 -// false for bool - -#[starknet::contract] -pub(crate) mod SnakeAccessControlPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn has_role(self: @ContractState, role: felt252, account: ContractAddress) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn get_role_admin(self: @ContractState, role: felt252) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn grant_role(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn revoke_role(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn renounce_role(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelAccessControlPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn hasRole(self: @ContractState, role: felt252, account: ContractAddress) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn getRoleAdmin(self: @ContractState, role: felt252) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn grantRole(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn revokeRole(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn renounceRole(ref self: ContractState, role: felt252, account: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { - panic!("Some error"); - false - } - } -} diff --git a/packages/access/src/tests/mocks/non_implementing_mock.cairo b/packages/access/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/access/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} diff --git a/packages/access/src/tests/mocks/ownable_mocks.cairo b/packages/access/src/tests/mocks/ownable_mocks.cairo index f3456c68a..996a23f91 100644 --- a/packages/access/src/tests/mocks/ownable_mocks.cairo +++ b/packages/access/src/tests/mocks/ownable_mocks.cairo @@ -28,135 +28,6 @@ pub(crate) mod DualCaseOwnableMock { } } -#[starknet::contract] -pub(crate) mod SnakeOwnableMock { - use crate::ownable::OwnableComponent; - use starknet::ContractAddress; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableImpl; - impl InternalImpl = OwnableComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub ownable: OwnableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - OwnableEvent: OwnableComponent::Event - } - - #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress) { - self.ownable.initializer(owner); - } -} - -#[starknet::contract] -pub(crate) mod CamelOwnableMock { - use crate::ownable::OwnableComponent; - use crate::ownable::interface::IOwnable; - use starknet::ContractAddress; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - impl OwnableCamelOnlyImpl = - OwnableComponent::OwnableCamelOnlyImpl; - impl InternalImpl = OwnableComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub ownable: OwnableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - OwnableEvent: OwnableComponent::Event - } - - #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress) { - self.ownable.initializer(owner); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn owner(self: @ContractState) -> ContractAddress { - self.ownable.owner() - } - } -} - -#[starknet::contract] -pub(crate) mod SnakeOwnablePanicMock { - use core::num::traits::Zero; - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn owner(self: @ContractState) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn renounce_ownership(ref self: ContractState) { - panic!("Some error"); - } - } -} - -#[starknet::contract] -pub(crate) mod CamelOwnablePanicMock { - use core::num::traits::Zero; - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn owner(self: @ContractState) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn transferOwnership(ref self: ContractState, newOwner: ContractAddress) { - panic!("Some error"); - } - - #[external(v0)] - fn renounceOwnership(ref self: ContractState) { - panic!("Some error"); - } - } -} - #[starknet::contract] pub(crate) mod DualCaseTwoStepOwnableMock { use crate::ownable::OwnableComponent; diff --git a/packages/access/src/tests/test_dual_accesscontrol.cairo b/packages/access/src/tests/test_dual_accesscontrol.cairo deleted file mode 100644 index 8c1032266..000000000 --- a/packages/access/src/tests/test_dual_accesscontrol.cairo +++ /dev/null @@ -1,290 +0,0 @@ -use crate::accesscontrol::DEFAULT_ADMIN_ROLE; -use crate::accesscontrol::dual_accesscontrol::DualCaseAccessControl; -use crate::accesscontrol::dual_accesscontrol::DualCaseAccessControlTrait; -use crate::accesscontrol::interface::{ - IACCESSCONTROL_ID, IAccessControlCamelDispatcher, IAccessControlCamelDispatcherTrait -}; -use crate::accesscontrol::interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{ADMIN, AUTHORIZED, ROLE}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::start_cheat_caller_address; - -// -// Setup -// - -fn setup_snake() -> (DualCaseAccessControl, IAccessControlDispatcher) { - let mut calldata = array![]; - calldata.append_serde(ADMIN()); - let target = utils::declare_and_deploy("SnakeAccessControlMock", calldata); - ( - DualCaseAccessControl { contract_address: target }, - IAccessControlDispatcher { contract_address: target } - ) -} - -fn setup_camel() -> (DualCaseAccessControl, IAccessControlCamelDispatcher) { - let mut calldata = array![]; - calldata.append_serde(ADMIN()); - let target = utils::declare_and_deploy("CamelAccessControlMock", calldata); - ( - DualCaseAccessControl { contract_address: target }, - IAccessControlCamelDispatcher { contract_address: target } - ) -} - -fn setup_non_accesscontrol() -> DualCaseAccessControl { - let target = utils::declare_and_deploy("NonImplementingMock", array![]); - DualCaseAccessControl { contract_address: target } -} - -fn setup_accesscontrol_panic() -> (DualCaseAccessControl, DualCaseAccessControl) { - let snake_target = utils::declare_and_deploy("SnakeAccessControlPanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelAccessControlPanicMock", array![]); - ( - DualCaseAccessControl { contract_address: snake_target }, - DualCaseAccessControl { contract_address: camel_target } - ) -} - -// -// snake_case target -// - -#[test] -fn test_dual_supports_interface() { - let (dispatcher, _) = setup_snake(); - let supports_iaccesscontrol = dispatcher.supports_interface(IACCESSCONTROL_ID); - assert!(supports_iaccesscontrol); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_supports_interface() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.supports_interface(IACCESSCONTROL_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_supports_interface_exists_and_panics() { - let (snake_dispatcher, _) = setup_accesscontrol_panic(); - snake_dispatcher.supports_interface(IACCESSCONTROL_ID); -} - -#[test] -fn test_dual_has_role() { - let (snake_dispatcher, _) = setup_snake(); - let has_role = snake_dispatcher.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); - assert!(has_role); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_has_role() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_has_role_exists_and_panics() { - let (dispatcher, _) = setup_accesscontrol_panic(); - dispatcher.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} - -#[test] -fn test_dual_get_role_admin() { - let (dispatcher, _) = setup_snake(); - let current_admin_role = dispatcher.get_role_admin(ROLE); - assert_eq!(current_admin_role, DEFAULT_ADMIN_ROLE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_get_role_admin() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.get_role_admin(ROLE); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_get_role_admin_exists_and_panics() { - let (snake_dispatcher, _) = setup_accesscontrol_panic(); - snake_dispatcher.get_role_admin(ROLE); -} - -#[test] -fn test_dual_grant_role() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.grant_role(ROLE, AUTHORIZED()); - - let has_role = target.has_role(ROLE, AUTHORIZED()); - assert!(has_role); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_grant_role() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.grant_role(ROLE, AUTHORIZED()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_grant_role_exists_and_panics() { - let (snake_dispatcher, _) = setup_accesscontrol_panic(); - snake_dispatcher.grant_role(ROLE, AUTHORIZED()); -} - -#[test] -fn test_dual_revoke_role() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.revoke_role(ROLE, AUTHORIZED()); - - let has_not_role = !target.has_role(ROLE, AUTHORIZED()); - assert!(has_not_role); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_revoke_role() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.revoke_role(ROLE, AUTHORIZED()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_revoke_role_exists_and_panics() { - let (snake_dispatcher, _) = setup_accesscontrol_panic(); - snake_dispatcher.revoke_role(ROLE, AUTHORIZED()); -} - -#[test] -fn test_dual_renounce_role() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN()); - - let has_not_role = !target.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); - assert!(has_not_role); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_renounce_role() { - let dispatcher = setup_non_accesscontrol(); - dispatcher.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_renounce_role_exists_and_panics() { - let (snake_dispatcher, _) = setup_accesscontrol_panic(); - snake_dispatcher.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_hasRole() { - let (dispatcher, _) = setup_camel(); - - let has_role = dispatcher.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); - assert!(has_role); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_hasRole_exists_and_panics() { - let (_, camel_dispatcher) = setup_accesscontrol_panic(); - camel_dispatcher.has_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_getRoleAdmin() { - let (dispatcher, _) = setup_camel(); - - let current_admin_role = dispatcher.get_role_admin(ROLE); - assert_eq!(current_admin_role, DEFAULT_ADMIN_ROLE); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_getRoleAdmin_exists_and_panics() { - let (_, camel_dispatcher) = setup_accesscontrol_panic(); - camel_dispatcher.get_role_admin(ROLE); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_grantRole() { - let (dispatcher, target) = setup_camel(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.grant_role(ROLE, AUTHORIZED()); - - let has_role = target.hasRole(ROLE, AUTHORIZED()); - assert!(has_role); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_grantRole_exists_and_panics() { - let (_, camel_dispatcher) = setup_accesscontrol_panic(); - camel_dispatcher.grant_role(ROLE, AUTHORIZED()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_revokeRole() { - let (dispatcher, target) = setup_camel(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.grant_role(ROLE, AUTHORIZED()); - dispatcher.revoke_role(ROLE, AUTHORIZED()); - - let has_not_role = !target.hasRole(ROLE, AUTHORIZED()); - assert!(has_not_role); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_revokeRole_exists_and_panics() { - let (_, camel_dispatcher) = setup_accesscontrol_panic(); - camel_dispatcher.revoke_role(ROLE, AUTHORIZED()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_renounceRole() { - let (dispatcher, target) = setup_camel(); - start_cheat_caller_address(target.contract_address, ADMIN()); - dispatcher.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN()); - - let has_not_role = !target.hasRole(DEFAULT_ADMIN_ROLE, ADMIN()); - assert!(has_not_role); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_renounceRole_exists_and_panics() { - let (_, camel_dispatcher) = setup_accesscontrol_panic(); - camel_dispatcher.renounce_role(DEFAULT_ADMIN_ROLE, ADMIN()); -} diff --git a/packages/access/src/tests/test_dual_ownable.cairo b/packages/access/src/tests/test_dual_ownable.cairo deleted file mode 100644 index 97d737032..000000000 --- a/packages/access/src/tests/test_dual_ownable.cairo +++ /dev/null @@ -1,173 +0,0 @@ -use core::num::traits::Zero; -use crate::ownable::dual_ownable::{DualCaseOwnable, DualCaseOwnableTrait}; -use crate::ownable::interface::{ - IOwnableDispatcher, IOwnableCamelOnlyDispatcher, IOwnableDispatcherTrait -}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{OWNER, NEW_OWNER}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::start_cheat_caller_address; - -// -// Setup -// - -fn setup_snake() -> (DualCaseOwnable, IOwnableDispatcher) { - let mut calldata = array![]; - calldata.append_serde(OWNER()); - let target = utils::declare_and_deploy("SnakeOwnableMock", calldata); - (DualCaseOwnable { contract_address: target }, IOwnableDispatcher { contract_address: target }) -} - -fn setup_camel() -> (DualCaseOwnable, IOwnableCamelOnlyDispatcher) { - let mut calldata = array![]; - calldata.append_serde(OWNER()); - let target = utils::declare_and_deploy("CamelOwnableMock", calldata); - ( - DualCaseOwnable { contract_address: target }, - IOwnableCamelOnlyDispatcher { contract_address: target } - ) -} - -fn setup_non_ownable() -> DualCaseOwnable { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseOwnable { contract_address: target } -} - -fn setup_ownable_panic() -> (DualCaseOwnable, DualCaseOwnable) { - let snake_target = utils::declare_and_deploy("SnakeOwnablePanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelOwnablePanicMock", array![]); - ( - DualCaseOwnable { contract_address: snake_target }, - DualCaseOwnable { contract_address: camel_target } - ) -} - -// -// Case agnostic methods -// - -#[test] -fn test_dual_owner() { - let (snake_dispatcher, _) = setup_snake(); - let (camel_dispatcher, _) = setup_camel(); - - let snake_owner = snake_dispatcher.owner(); - assert_eq!(snake_owner, OWNER()); - - let camel_owner = camel_dispatcher.owner(); - assert_eq!(camel_owner, OWNER()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_owner() { - let dispatcher = setup_non_ownable(); - dispatcher.owner(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_owner_exists_and_panics() { - let (dispatcher, _) = setup_ownable_panic(); - dispatcher.owner(); -} - -// -// snake_case target -// - -#[test] -fn test_dual_transfer_ownership() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.transfer_ownership(NEW_OWNER()); - - let current_owner = target.owner(); - assert_eq!(current_owner, NEW_OWNER()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer_ownership() { - let dispatcher = setup_non_ownable(); - dispatcher.transfer_ownership(NEW_OWNER()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_transfer_ownership_exists_and_panics() { - let (dispatcher, _) = setup_ownable_panic(); - dispatcher.transfer_ownership(NEW_OWNER()); -} - -#[test] -fn test_dual_renounce_ownership() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.renounce_ownership(); - - let current_owner = target.owner(); - assert!(current_owner.is_zero()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_renounce_ownership() { - let dispatcher = setup_non_ownable(); - dispatcher.renounce_ownership(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_renounce_ownership_exists_and_panics() { - let (dispatcher, _) = setup_ownable_panic(); - dispatcher.renounce_ownership(); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_transferOwnership() { - let (dispatcher, _) = setup_camel(); - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.transfer_ownership(NEW_OWNER()); - - let current_owner = dispatcher.owner(); - assert_eq!(current_owner, NEW_OWNER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_transferOwnership_exists_and_panics() { - let (_, camel_dispatcher) = setup_ownable_panic(); - camel_dispatcher.transfer_ownership(NEW_OWNER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_renounceOwnership() { - let (dispatcher, _) = setup_camel(); - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.renounce_ownership(); - - let current_owner = dispatcher.owner(); - assert!(current_owner.is_zero()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_renounceOwnership_exists_and_panics() { - let (_, camel_dispatcher) = setup_ownable_panic(); - camel_dispatcher.renounce_ownership(); -} - From 485512f7cb331bb391e36a6e9559ce8cd9ad915b Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 26 Sep 2024 13:35:23 +0200 Subject: [PATCH 03/76] feat: bump scarb --- CHANGELOG.md | 4 ++++ Scarb.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4724fb45..4fb2e5813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Bump scarb to v2.8.3 (#) + ## 0.17.0 (2024-09-23) ### Added diff --git a/Scarb.toml b/Scarb.toml index c5d76c606..7fb7c15a4 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -25,7 +25,7 @@ keywords.workspace = true version = "0.17.0" edition = "2024_07" cairo-version = "2.8.2" -scarb-version = "2.8.2" +scarb-version = "2.8.3" authors = ["OpenZeppelin Community "] description = "OpenZeppelin Contracts written in Cairo for Starknet, a decentralized ZK Rollup" documentation = "https://docs.openzeppelin.com/contracts-cairo" From e9cd67d201e857abfc160b9af45dc2e37f53731a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 26 Sep 2024 13:36:28 +0200 Subject: [PATCH 04/76] feat: update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb2e5813..0312236cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump scarb to v2.8.3 (#) +- Bump scarb to v2.8.3 (#1166) ## 0.17.0 (2024-09-23) From 21b31d9ee248110f8cd3ca82ba71ff13e0d320cf Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 30 Sep 2024 12:26:53 -0400 Subject: [PATCH 05/76] feat: add features --- Scarb.toml | 3 --- packages/access/Scarb.toml | 6 ++++-- packages/access/src/tests/mocks/accesscontrol_mocks.cairo | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Scarb.toml b/Scarb.toml index 635406eff..7fb7c15a4 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -81,6 +81,3 @@ fmt.workspace = true unstable-add-statements-functions-debug-info = true unstable-add-statements-code-locations-debug-info = true inlining-strategy= "avoid" - -[features] -mock_contract = [] diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 35db86e78..6d4008583 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -32,6 +32,10 @@ snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } +[features] +default = ["mock_contract"] +mock_contract = [] + [lib] [[target.starknet-contract]] @@ -39,5 +43,3 @@ allowed-libfuncs-list.name = "experimental" sierra = true casm = false -[features] -mock_contract = [] diff --git a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo b/packages/access/src/tests/mocks/accesscontrol_mocks.cairo index 09eee34b1..8dffc2e69 100644 --- a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo +++ b/packages/access/src/tests/mocks/accesscontrol_mocks.cairo @@ -1,4 +1,5 @@ #[starknet::contract] +#[cfg(feature: 'mock_contract')] pub(crate) mod DualCaseAccessControlMock { use crate::accesscontrol::AccessControlComponent; use crate::accesscontrol::DEFAULT_ADMIN_ROLE; From b8a3ed5eb19643a27872f6133f55dff7505a4cff Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 2 Oct 2024 16:35:55 -0400 Subject: [PATCH 06/76] feat: remove account dual dispatchers (#1168) --- packages/account/src/dual_account.cairo | 67 ------ packages/account/src/dual_eth_account.cairo | 73 ------ .../src/extensions/src9/snip12_utils.cairo | 2 +- .../account/src/extensions/src9/src9.cairo | 4 +- packages/account/src/lib.cairo | 2 - packages/account/src/tests.cairo | 4 - packages/account/src/tests/mocks.cairo | 1 - .../src/tests/mocks/account_mocks.cairo | 178 -------------- .../src/tests/mocks/eth_account_mocks.cairo | 185 --------------- .../tests/mocks/non_implementing_mock.cairo | 10 - .../account/src/tests/test_dual_account.cairo | 220 ------------------ .../src/tests/test_dual_eth_account.cairo | 220 ------------------ .../src/erc20/extensions/erc20_votes.cairo | 4 +- 13 files changed, 5 insertions(+), 965 deletions(-) delete mode 100644 packages/account/src/dual_account.cairo delete mode 100644 packages/account/src/dual_eth_account.cairo delete mode 100644 packages/account/src/tests/mocks/non_implementing_mock.cairo delete mode 100644 packages/account/src/tests/test_dual_account.cairo delete mode 100644 packages/account/src/tests/test_dual_eth_account.cairo diff --git a/packages/account/src/dual_account.cairo b/packages/account/src/dual_account.cairo deleted file mode 100644 index 5872df0ff..000000000 --- a/packages/account/src/dual_account.cairo +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (account/dual_account.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseAccount { - pub contract_address: ContractAddress -} - -pub trait DualCaseAccountTrait { - fn set_public_key(self: @DualCaseAccount, new_public_key: felt252, signature: Span); - fn get_public_key(self: @DualCaseAccount) -> felt252; - fn is_valid_signature( - self: @DualCaseAccount, hash: felt252, signature: Array - ) -> felt252; - fn supports_interface(self: @DualCaseAccount, interface_id: felt252) -> bool; -} - -impl DualCaseAccountImpl of DualCaseAccountTrait { - fn set_public_key(self: @DualCaseAccount, new_public_key: felt252, signature: Span) { - let mut args = array![new_public_key]; - args.append_serde(signature); - - try_selector_with_fallback( - *self.contract_address, selectors::set_public_key, selectors::setPublicKey, args.span() - ) - .unwrap_syscall(); - } - - fn get_public_key(self: @DualCaseAccount) -> felt252 { - let args = array![]; - - try_selector_with_fallback( - *self.contract_address, selectors::get_public_key, selectors::getPublicKey, args.span() - ) - .unwrap_and_cast() - } - - fn is_valid_signature( - self: @DualCaseAccount, hash: felt252, signature: Array - ) -> felt252 { - let mut args = array![hash]; - args.append_serde(signature); - - try_selector_with_fallback( - *self.contract_address, - selectors::is_valid_signature, - selectors::isValidSignature, - args.span() - ) - .unwrap_and_cast() - } - - fn supports_interface(self: @DualCaseAccount, interface_id: felt252) -> bool { - let args = array![interface_id]; - - call_contract_syscall(*self.contract_address, selectors::supports_interface, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/account/src/dual_eth_account.cairo b/packages/account/src/dual_eth_account.cairo deleted file mode 100644 index f305aa62d..000000000 --- a/packages/account/src/dual_eth_account.cairo +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (account/dual_eth_account.cairo) - -use crate::interface::EthPublicKey; -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseEthAccount { - pub contract_address: ContractAddress -} - -pub trait DualCaseEthAccountTrait { - fn set_public_key( - self: @DualCaseEthAccount, new_public_key: EthPublicKey, signature: Span - ); - fn get_public_key(self: @DualCaseEthAccount) -> EthPublicKey; - fn is_valid_signature( - self: @DualCaseEthAccount, hash: felt252, signature: Array - ) -> felt252; - fn supports_interface(self: @DualCaseEthAccount, interface_id: felt252) -> bool; -} - -impl DualCaseEthAccountImpl of DualCaseEthAccountTrait { - fn set_public_key( - self: @DualCaseEthAccount, new_public_key: EthPublicKey, signature: Span - ) { - let mut args = array![]; - args.append_serde(new_public_key); - args.append_serde(signature); - - try_selector_with_fallback( - *self.contract_address, selectors::set_public_key, selectors::setPublicKey, args.span() - ) - .unwrap_syscall(); - } - - fn get_public_key(self: @DualCaseEthAccount) -> EthPublicKey { - let args = array![]; - - try_selector_with_fallback( - *self.contract_address, selectors::get_public_key, selectors::getPublicKey, args.span() - ) - .unwrap_and_cast() - } - - fn is_valid_signature( - self: @DualCaseEthAccount, hash: felt252, signature: Array - ) -> felt252 { - let mut args = array![hash]; - args.append_serde(signature); - - try_selector_with_fallback( - *self.contract_address, - selectors::is_valid_signature, - selectors::isValidSignature, - args.span() - ) - .unwrap_and_cast() - } - - fn supports_interface(self: @DualCaseEthAccount, interface_id: felt252) -> bool { - let args = array![interface_id]; - - call_contract_syscall(*self.contract_address, selectors::supports_interface, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/account/src/extensions/src9/snip12_utils.cairo b/packages/account/src/extensions/src9/snip12_utils.cairo index 6a397dfe5..b2d81132c 100644 --- a/packages/account/src/extensions/src9/snip12_utils.cairo +++ b/packages/account/src/extensions/src9/snip12_utils.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (account/extensions/src9/snip12.cairo) +// OpenZeppelin Contracts for Cairo v0.17.0 (account/extensions/src9/snip12_utils.cairo) use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::{PoseidonTrait, poseidon_hash_span}; diff --git a/packages/account/src/extensions/src9/src9.cairo b/packages/account/src/extensions/src9/src9.cairo index e9b7b3afc..468e1475f 100644 --- a/packages/account/src/extensions/src9/src9.cairo +++ b/packages/account/src/extensions/src9/src9.cairo @@ -14,7 +14,7 @@ pub mod SRC9Component { use crate::extensions::src9::interface; use crate::extensions::src9::snip12_utils::OutsideExecutionStructHash; use crate::utils::execute_calls; - use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; + use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; @@ -105,7 +105,7 @@ pub mod SRC9Component { // Make this component agnostic to the account implementation, as long // as the contract implements the SRC6 interface. - let is_valid_signature_felt = DualCaseAccount { contract_address: this } + let is_valid_signature_felt = ISRC6Dispatcher { contract_address: this } .is_valid_signature(outside_tx_hash, signature.into()); // Check either 'VALID' or true for backwards compatibility. diff --git a/packages/account/src/lib.cairo b/packages/account/src/lib.cairo index 6e4ed8a32..366b3eb8f 100644 --- a/packages/account/src/lib.cairo +++ b/packages/account/src/lib.cairo @@ -1,6 +1,4 @@ pub mod account; -pub mod dual_account; -pub mod dual_eth_account; pub mod eth_account; pub mod extensions; pub mod interface; diff --git a/packages/account/src/tests.cairo b/packages/account/src/tests.cairo index c02171f01..660465d02 100644 --- a/packages/account/src/tests.cairo +++ b/packages/account/src/tests.cairo @@ -4,10 +4,6 @@ pub(crate) mod mocks; #[cfg(test)] mod test_account; #[cfg(test)] -mod test_dual_account; -#[cfg(test)] -mod test_dual_eth_account; -#[cfg(test)] mod test_eth_account; #[cfg(test)] mod test_secp256k1; diff --git a/packages/account/src/tests/mocks.cairo b/packages/account/src/tests/mocks.cairo index 3d873c28b..e9e44bf5d 100644 --- a/packages/account/src/tests/mocks.cairo +++ b/packages/account/src/tests/mocks.cairo @@ -1,5 +1,4 @@ pub(crate) mod account_mocks; pub(crate) mod eth_account_mocks; -pub(crate) mod non_implementing_mock; pub(crate) mod simple_mock; pub(crate) mod src9_mocks; diff --git a/packages/account/src/tests/mocks/account_mocks.cairo b/packages/account/src/tests/mocks/account_mocks.cairo index faddfdf9f..893a339c1 100644 --- a/packages/account/src/tests/mocks/account_mocks.cairo +++ b/packages/account/src/tests/mocks/account_mocks.cairo @@ -43,181 +43,3 @@ pub(crate) mod DualCaseAccountMock { self.account.initializer(public_key); } } - -#[starknet::contract(account)] -pub(crate) mod SnakeAccountMock { - use crate::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 crate::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) - } - } -} - -// Although these modules are designed to panic, functions -// still need a valid return value. We chose: -// -// 3 for felt252 -// false for bool - -#[starknet::contract] -pub(crate) mod SnakeAccountPanicMock { - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn set_public_key( - ref self: ContractState, new_public_key: felt252, signature: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn get_public_key(self: @ContractState) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn is_valid_signature( - self: @ContractState, hash: felt252, signature: Array - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelAccountPanicMock { - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn setPublicKey(ref self: ContractState, newPublicKey: felt252, signature: Span) { - panic!("Some error"); - } - - #[external(v0)] - fn getPublicKey(self: @ContractState) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn isValidSignature( - self: @ContractState, hash: felt252, signature: Array - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { - panic!("Some error"); - false - } - } -} diff --git a/packages/account/src/tests/mocks/eth_account_mocks.cairo b/packages/account/src/tests/mocks/eth_account_mocks.cairo index 2787e76d1..71ddeb787 100644 --- a/packages/account/src/tests/mocks/eth_account_mocks.cairo +++ b/packages/account/src/tests/mocks/eth_account_mocks.cairo @@ -41,188 +41,3 @@ pub(crate) mod DualCaseEthAccountMock { self.eth_account.initializer(public_key); } } - -#[starknet::contract(account)] -pub(crate) mod SnakeEthAccountMock { - use crate::EthAccountComponent; - use crate::interface::EthPublicKey; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC6Impl = EthAccountComponent::SRC6Impl; - #[abi(embed_v0)] - impl PublicKeyImpl = EthAccountComponent::PublicKeyImpl; - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub eth_account: EthAccountComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - EthAccountEvent: EthAccountComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, public_key: EthPublicKey) { - self.eth_account.initializer(public_key); - } -} - -#[starknet::contract(account)] -pub(crate) mod CamelEthAccountMock { - use crate::EthAccountComponent; - use crate::interface::EthPublicKey; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::account::Call; - - component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC6CamelOnlyImpl = EthAccountComponent::SRC6CamelOnlyImpl; - #[abi(embed_v0)] - impl PublicKeyCamelImpl = - EthAccountComponent::PublicKeyCamelImpl; - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - impl SRC6Impl = EthAccountComponent::SRC6Impl; - impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub eth_account: EthAccountComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - EthAccountEvent: EthAccountComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, publicKey: EthPublicKey) { - self.eth_account.initializer(publicKey); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn __execute__(self: @ContractState, mut calls: Array) -> Array> { - self.eth_account.__execute__(calls) - } - - #[external(v0)] - fn __validate__(self: @ContractState, mut calls: Array) -> felt252 { - self.eth_account.__validate__(calls) - } - } -} - -// Although these modules are designed to panic, functions -// still need a valid return value. We chose: -// -// 3 for felt252 -// false for bool - -#[starknet::contract] -pub(crate) mod SnakeEthAccountPanicMock { - use crate::interface::EthPublicKey; - use starknet::SyscallResultTrait; - use starknet::secp256_trait::Secp256Trait; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn set_public_key( - ref self: ContractState, new_public_key: EthPublicKey, signature: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn get_public_key(self: @ContractState) -> EthPublicKey { - panic!("Some error"); - Secp256Trait::secp256_ec_new_syscall(3, 3).unwrap_syscall().unwrap() - } - - #[external(v0)] - fn is_valid_signature( - self: @ContractState, hash: felt252, signature: Array - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelEthAccountPanicMock { - use crate::interface::EthPublicKey; - use starknet::SyscallResultTrait; - use starknet::secp256_trait::Secp256Trait; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn setPublicKey( - ref self: ContractState, newPublicKey: EthPublicKey, signature: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn getPublicKey(self: @ContractState) -> EthPublicKey { - panic!("Some error"); - Secp256Trait::secp256_ec_new_syscall(3, 3).unwrap_syscall().unwrap() - } - - #[external(v0)] - fn isValidSignature( - self: @ContractState, hash: felt252, signature: Array - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { - panic!("Some error"); - false - } - } -} diff --git a/packages/account/src/tests/mocks/non_implementing_mock.cairo b/packages/account/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/account/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} diff --git a/packages/account/src/tests/test_dual_account.cairo b/packages/account/src/tests/test_dual_account.cairo deleted file mode 100644 index 467f0878e..000000000 --- a/packages/account/src/tests/test_dual_account.cairo +++ /dev/null @@ -1,220 +0,0 @@ -use crate::dual_account::{DualCaseAccountTrait, DualCaseAccount}; -use crate::interface::{AccountABIDispatcherTrait, AccountABIDispatcher}; -use openzeppelin_introspection::interface::ISRC5_ID; - -use openzeppelin_test_common::account::{get_accept_ownership_signature}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::TRANSACTION_HASH; -use openzeppelin_testing::constants::stark::{KEY_PAIR, KEY_PAIR_2}; -use openzeppelin_testing::signing::{StarkKeyPair, StarkSerializedSigning}; -use snforge_std::{start_cheat_caller_address}; - -// -// Setup -// - -fn setup_snake(key_pair: StarkKeyPair) -> (DualCaseAccount, AccountABIDispatcher) { - let calldata = array![key_pair.public_key]; - let contract_address = utils::declare_and_deploy("SnakeAccountMock", calldata); - (DualCaseAccount { contract_address }, AccountABIDispatcher { contract_address }) -} - -fn setup_camel(key_pair: StarkKeyPair) -> (DualCaseAccount, AccountABIDispatcher) { - let calldata = array![key_pair.public_key]; - let contract_address = utils::declare_and_deploy("CamelAccountMock", calldata); - (DualCaseAccount { contract_address }, AccountABIDispatcher { contract_address }) -} - -fn setup_non_account() -> DualCaseAccount { - let calldata = array![]; - let contract_address = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseAccount { contract_address } -} - -fn setup_account_panic() -> (DualCaseAccount, DualCaseAccount) { - let snake_target = utils::declare_and_deploy("SnakeAccountPanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelAccountPanicMock", array![]); - ( - DualCaseAccount { contract_address: snake_target }, - DualCaseAccount { contract_address: camel_target } - ) -} - -// -// snake_case target -// - -#[test] -fn test_dual_set_public_key() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, target) = setup_snake(key_pair); - let new_key_pair = KEY_PAIR_2(); - let signature = get_accept_ownership_signature( - snake_dispatcher.contract_address, key_pair.public_key, new_key_pair - ); - start_cheat_caller_address(target.contract_address, target.contract_address); - - snake_dispatcher.set_public_key(new_key_pair.public_key, signature); - - assert_eq!(target.get_public_key(), new_key_pair.public_key); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_set_public_key() { - let dispatcher = setup_non_account(); - let new_public_key = KEY_PAIR_2().public_key; - dispatcher.set_public_key(new_public_key, array![].span()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_set_public_key_exists_and_panics() { - let (snake_dispatcher, _) = setup_account_panic(); - let new_public_key = KEY_PAIR_2().public_key; - snake_dispatcher.set_public_key(new_public_key, array![].span()); -} - -#[test] -fn test_dual_get_public_key() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, _) = setup_snake(key_pair); - let expected_public_key = key_pair.public_key; - assert_eq!(snake_dispatcher.get_public_key(), expected_public_key); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_get_public_key() { - let dispatcher = setup_non_account(); - dispatcher.get_public_key(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_get_public_key_exists_and_panics() { - let (snake_dispatcher, _) = setup_account_panic(); - snake_dispatcher.get_public_key(); -} - -#[test] -fn test_dual_is_valid_signature() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, _) = setup_snake(key_pair); - let tx_hash = TRANSACTION_HASH; - let serialized_signature = key_pair.serialized_sign(tx_hash); - - let is_valid = snake_dispatcher.is_valid_signature(tx_hash, serialized_signature); - assert_eq!(is_valid, starknet::VALIDATED); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_is_valid_signature() { - let signature = array![]; - - let dispatcher = setup_non_account(); - dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_is_valid_signature_exists_and_panics() { - let signature = array![]; - let (snake_dispatcher, _) = setup_account_panic(); - - snake_dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} - -#[test] -fn test_dual_supports_interface() { - let (snake_dispatcher, _) = setup_snake(KEY_PAIR()); - let supports_isrc5 = snake_dispatcher.supports_interface(ISRC5_ID); - assert!(supports_isrc5); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_supports_interface() { - let dispatcher = setup_non_account(); - dispatcher.supports_interface(ISRC5_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_supports_interface_exists_and_panics() { - let (snake_dispatcher, _) = setup_account_panic(); - snake_dispatcher.supports_interface(ISRC5_ID); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_setPublicKey() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, target) = setup_camel(key_pair); - let new_key_pair = KEY_PAIR_2(); - let signature = get_accept_ownership_signature( - camel_dispatcher.contract_address, key_pair.public_key, new_key_pair - ); - start_cheat_caller_address(target.contract_address, target.contract_address); - - camel_dispatcher.set_public_key(new_key_pair.public_key, signature); - - assert_eq!(target.getPublicKey(), new_key_pair.public_key); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_setPublicKey_exists_and_panics() { - let (_, camel_dispatcher) = setup_account_panic(); - let new_public_key = KEY_PAIR_2().public_key; - camel_dispatcher.set_public_key(new_public_key, array![].span()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_getPublicKey() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, _) = setup_camel(key_pair); - let expected_public_key = key_pair.public_key; - assert_eq!(camel_dispatcher.get_public_key(), expected_public_key); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_getPublicKey_exists_and_panics() { - let (_, camel_dispatcher) = setup_account_panic(); - camel_dispatcher.get_public_key(); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_isValidSignature() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, _) = setup_camel(key_pair); - let tx_hash = TRANSACTION_HASH; - let serialized_signature = key_pair.serialized_sign(tx_hash); - - let is_valid = camel_dispatcher.is_valid_signature(tx_hash, serialized_signature); - assert_eq!(is_valid, starknet::VALIDATED); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_isValidSignature_exists_and_panics() { - let signature = array![]; - - let (_, camel_dispatcher) = setup_account_panic(); - camel_dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} diff --git a/packages/account/src/tests/test_dual_eth_account.cairo b/packages/account/src/tests/test_dual_eth_account.cairo deleted file mode 100644 index c62495448..000000000 --- a/packages/account/src/tests/test_dual_eth_account.cairo +++ /dev/null @@ -1,220 +0,0 @@ -use crate::dual_eth_account::{DualCaseEthAccountTrait, DualCaseEthAccount}; -use crate::interface::{EthAccountABIDispatcherTrait, EthAccountABIDispatcher}; -use crate::utils::secp256k1::{DebugSecp256k1Point, Secp256k1PointPartialEq}; -use openzeppelin_introspection::interface::ISRC5_ID; - -use openzeppelin_test_common::eth_account::get_accept_ownership_signature; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::TRANSACTION_HASH; -use openzeppelin_testing::constants::secp256k1::{KEY_PAIR, KEY_PAIR_2}; -use openzeppelin_testing::signing::{Secp256k1KeyPair, Secp256k1SerializedSigning}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::start_cheat_caller_address; - -// -// Setup -// - -fn setup_snake(key_pair: Secp256k1KeyPair) -> (DualCaseEthAccount, EthAccountABIDispatcher) { - let mut calldata = array![]; - calldata.append_serde(key_pair.public_key); - - let contract_address = utils::declare_and_deploy("SnakeEthAccountMock", calldata); - (DualCaseEthAccount { contract_address }, EthAccountABIDispatcher { contract_address }) -} - -fn setup_camel(key_pair: Secp256k1KeyPair) -> (DualCaseEthAccount, EthAccountABIDispatcher) { - let mut calldata = array![]; - calldata.append_serde(key_pair.public_key); - - let contract_address = utils::declare_and_deploy("CamelEthAccountMock", calldata); - (DualCaseEthAccount { contract_address }, EthAccountABIDispatcher { contract_address }) -} - -fn setup_non_account() -> DualCaseEthAccount { - let calldata = array![]; - let contract_address = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseEthAccount { contract_address } -} - -fn setup_account_panic() -> (DualCaseEthAccount, DualCaseEthAccount) { - let snake_target = utils::declare_and_deploy("SnakeEthAccountPanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelEthAccountPanicMock", array![]); - ( - DualCaseEthAccount { contract_address: snake_target }, - DualCaseEthAccount { contract_address: camel_target } - ) -} - -// -// snake_case target -// - -#[test] -fn test_dual_set_public_key() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, target) = setup_snake(key_pair); - let contract_address = snake_dispatcher.contract_address; - - let new_key_pair = KEY_PAIR_2(); - start_cheat_caller_address(contract_address, contract_address); - let signature = get_accept_ownership_signature( - contract_address, key_pair.public_key, new_key_pair - ); - snake_dispatcher.set_public_key(new_key_pair.public_key, signature); - - assert_eq!(target.get_public_key(), new_key_pair.public_key); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_set_public_key() { - let dispatcher = setup_non_account(); - dispatcher.set_public_key(KEY_PAIR().public_key, array![].span()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_set_public_key_exists_and_panics() { - let (dispatcher, _) = setup_account_panic(); - dispatcher.set_public_key(KEY_PAIR().public_key, array![].span()); -} - -#[test] -fn test_dual_get_public_key() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, _) = setup_snake(key_pair); - assert_eq!(snake_dispatcher.get_public_key(), key_pair.public_key); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_get_public_key() { - let dispatcher = setup_non_account(); - dispatcher.get_public_key(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_get_public_key_exists_and_panics() { - let (dispatcher, _) = setup_account_panic(); - dispatcher.get_public_key(); -} - -#[test] -fn test_dual_is_valid_signature() { - let key_pair = KEY_PAIR(); - let (snake_dispatcher, _) = setup_snake(key_pair); - - let serialized_signature = key_pair.serialized_sign(TRANSACTION_HASH.into()); - let is_valid = snake_dispatcher.is_valid_signature(TRANSACTION_HASH, serialized_signature); - assert_eq!(is_valid, starknet::VALIDATED); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_is_valid_signature() { - let signature = array![]; - - let dispatcher = setup_non_account(); - dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_is_valid_signature_exists_and_panics() { - let signature = array![]; - - let (dispatcher, _) = setup_account_panic(); - dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} - -#[test] -fn test_dual_supports_interface() { - let (snake_dispatcher, _) = setup_snake(KEY_PAIR()); - assert!(snake_dispatcher.supports_interface(ISRC5_ID), "Should implement ISRC5"); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_supports_interface() { - let dispatcher = setup_non_account(); - dispatcher.supports_interface(ISRC5_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_supports_interface_exists_and_panics() { - let (dispatcher, _) = setup_account_panic(); - dispatcher.supports_interface(ISRC5_ID); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_setPublicKey() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, target) = setup_camel(key_pair); - let contract_address = camel_dispatcher.contract_address; - - start_cheat_caller_address(contract_address, contract_address); - let new_key_pair = KEY_PAIR_2(); - let signature = get_accept_ownership_signature( - contract_address, key_pair.public_key, new_key_pair - ); - - camel_dispatcher.set_public_key(new_key_pair.public_key, signature); - assert_eq!(target.getPublicKey(), new_key_pair.public_key); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_setPublicKey_exists_and_panics() { - let (_, dispatcher) = setup_account_panic(); - dispatcher.set_public_key(KEY_PAIR_2().public_key, array![].span()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_getPublicKey() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, _) = setup_camel(key_pair); - assert_eq!(camel_dispatcher.get_public_key(), key_pair.public_key); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_getPublicKey_exists_and_panics() { - let (_, dispatcher) = setup_account_panic(); - dispatcher.get_public_key(); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_isValidSignature() { - let key_pair = KEY_PAIR(); - let (camel_dispatcher, _) = setup_camel(key_pair); - - let serialized_signature = key_pair.serialized_sign(TRANSACTION_HASH.into()); - let is_valid = camel_dispatcher.is_valid_signature(TRANSACTION_HASH, serialized_signature); - assert_eq!(is_valid, starknet::VALIDATED); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_isValidSignature_exists_and_panics() { - let signature = array![]; - - let (_, dispatcher) = setup_account_panic(); - dispatcher.is_valid_signature(TRANSACTION_HASH, signature); -} diff --git a/packages/token/src/erc20/extensions/erc20_votes.cairo b/packages/token/src/erc20/extensions/erc20_votes.cairo index a41e6b811..b1dfa1fcb 100644 --- a/packages/token/src/erc20/extensions/erc20_votes.cairo +++ b/packages/token/src/erc20/extensions/erc20_votes.cairo @@ -19,7 +19,7 @@ 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_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_governance::utils::interfaces::IVotes; use openzeppelin_utils::nonces::NoncesComponent::InternalTrait as NoncesInternalTrait; use openzeppelin_utils::nonces::NoncesComponent; @@ -162,7 +162,7 @@ pub mod ERC20VotesComponent { let delegation = Delegation { delegatee, nonce, expiry }; let hash = delegation.get_message_hash(delegator); - let is_valid_signature_felt = DualCaseAccount { contract_address: delegator } + let is_valid_signature_felt = ISRC6Dispatcher { contract_address: delegator } .is_valid_signature(hash, signature); // Check either 'VALID' or true for backwards compatibility. From d44754c4e45b0aaa1afd51bc91d9eb7017130fda Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 19:29:21 -0400 Subject: [PATCH 07/76] feat: move mocks to test_common --- CHANGELOG.md | 4 +++ Scarb.lock | 8 +++--- Scarb.toml | 2 +- packages/access/Scarb.toml | 16 ++++++++---- packages/access/src/lib.cairo | 3 ++- packages/access/src/tests.cairo | 7 ----- packages/access/src/tests/mocks.cairo | 3 --- .../tests/mocks/non_implementing_mock.cairo | 10 ------- .../access/src/tests/test_accesscontrol.cairo | 2 +- packages/access/src/tests/test_ownable.cairo | 3 +-- .../src/tests/test_ownable_twostep.cairo | 2 +- packages/account/Scarb.toml | 13 ++++++++++ packages/account/src/lib.cairo | 3 ++- packages/account/src/tests.cairo | 7 ----- packages/account/src/tests/extensions.cairo | 2 -- .../src/tests/extensions/test_src9.cairo | 2 +- packages/account/src/tests/mocks.cairo | 5 ---- packages/account/src/tests/test_account.cairo | 4 +-- .../account/src/tests/test_eth_account.cairo | 4 +-- packages/test_common/src/lib.cairo | 1 + packages/test_common/src/mocks.cairo | 7 +++++ .../src/mocks/accesscontrol.cairo} | 23 ++++++++-------- .../src/mocks/account.cairo} | 16 ++++++------ .../src/mocks/eth_account.cairo} | 26 +++++++++---------- .../src/mocks/non_implementing.cairo} | 2 +- .../src/mocks/ownable.cairo} | 22 ++++++++-------- .../src/mocks/simple.cairo} | 5 ++-- .../src/mocks/src9.cairo} | 6 ++--- 28 files changed, 103 insertions(+), 105 deletions(-) delete mode 100644 packages/access/src/tests/mocks.cairo delete mode 100644 packages/access/src/tests/mocks/non_implementing_mock.cairo delete mode 100644 packages/account/src/tests/mocks.cairo create mode 100644 packages/test_common/src/mocks.cairo rename packages/{access/src/tests/mocks/accesscontrol_mocks.cairo => test_common/src/mocks/accesscontrol.cairo} (91%) rename packages/{account/src/tests/mocks/account_mocks.cairo => test_common/src/mocks/account.cairo} (95%) rename packages/{account/src/tests/mocks/eth_account_mocks.cairo => test_common/src/mocks/eth_account.cairo} (91%) rename packages/{account/src/tests/mocks/non_implementing_mock.cairo => test_common/src/mocks/non_implementing.cairo} (80%) rename packages/{access/src/tests/mocks/ownable_mocks.cairo => test_common/src/mocks/ownable.cairo} (90%) rename packages/{account/src/tests/mocks/simple_mock.cairo => test_common/src/mocks/simple.cairo} (90%) rename packages/{account/src/tests/mocks/src9_mocks.cairo => test_common/src/mocks/src9.cairo} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0312236cf..f2a97f3fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump scarb to v2.8.3 (#1166) +### Changed (Breaking) + +- Bump snforge to 0.31.0 + ## 0.17.0 (2024-09-23) ### Added diff --git a/Scarb.lock b/Scarb.lock index 12e157aae..0c51b589b 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -155,15 +155,15 @@ dependencies = [ [[package]] name = "snforge_scarb_plugin" -version = "0.2.0" +version = "0.31.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:2e4ce3ebe3f49548bd26908391b5d78537a765d827df0d96c32aeb88941d0d67" +checksum = "sha256:1fce075fcbf7fce1b0935f6f9a034549704837fb221da212d3b6e9134cebfdaa" [[package]] name = "snforge_std" -version = "0.30.0" +version = "0.31.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:2f3c4846881813ac0f5d1460981249c9f5e2a6831e752beedf9b70975495b4ec" +checksum = "sha256:60ac980b297281f9a59a5f1668cb56bdea1b28fd2f8008008270f9a3c91ad3ba" dependencies = [ "snforge_scarb_plugin", ] diff --git a/Scarb.toml b/Scarb.toml index 7fb7c15a4..50c76abd1 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -42,7 +42,7 @@ keywords = [ [workspace.dependencies] assert_macros = "2.8.2" starknet = "2.8.2" -snforge_std = "0.30.0" +snforge_std = "0.31.0" [dependencies] starknet.workspace = true diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 6d4008583..5ea06fd7e 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -32,14 +32,20 @@ snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } -[features] -default = ["mock_contract"] -mock_contract = [] - [lib] [[target.starknet-contract]] allowed-libfuncs-list.name = "experimental" sierra = true casm = false - +build-external-contracts = [ + "openzeppelin_test_common::mocks::accesscontrol::SnakeAccessControlMock", + "openzeppelin_test_common::mocks::accesscontrol::CamelAccessControlMock", + "openzeppelin_test_common::mocks::accesscontrol::SnakeAccessControlPanicMock", + "openzeppelin_test_common::mocks::accesscontrol::CamelAccessControlPanicMock", + "openzeppelin_test_common::mocks::ownable::SnakeOwnableMock", + "openzeppelin_test_common::mocks::ownable::CamelOwnableMock", + "openzeppelin_test_common::mocks::ownable::SnakeOwnablePanicMock", + "openzeppelin_test_common::mocks::ownable::CamelOwnablePanicMock", + "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", +] diff --git a/packages/access/src/lib.cairo b/packages/access/src/lib.cairo index b9ab604d4..5c758695e 100644 --- a/packages/access/src/lib.cairo +++ b/packages/access/src/lib.cairo @@ -1,4 +1,5 @@ pub mod accesscontrol; pub mod ownable; -pub mod tests; +#[cfg(test)] +mod tests; diff --git a/packages/access/src/tests.cairo b/packages/access/src/tests.cairo index a7b4dadfe..078ca4837 100644 --- a/packages/access/src/tests.cairo +++ b/packages/access/src/tests.cairo @@ -1,12 +1,5 @@ -pub(crate) mod mocks; - -#[cfg(test)] mod test_accesscontrol; -#[cfg(test)] mod test_dual_accesscontrol; -#[cfg(test)] mod test_dual_ownable; -#[cfg(test)] mod test_ownable; -#[cfg(test)] mod test_ownable_twostep; diff --git a/packages/access/src/tests/mocks.cairo b/packages/access/src/tests/mocks.cairo deleted file mode 100644 index f3eca4c97..000000000 --- a/packages/access/src/tests/mocks.cairo +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod accesscontrol_mocks; -pub(crate) mod non_implementing_mock; -pub(crate) mod ownable_mocks; diff --git a/packages/access/src/tests/mocks/non_implementing_mock.cairo b/packages/access/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/access/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} diff --git a/packages/access/src/tests/test_accesscontrol.cairo b/packages/access/src/tests/test_accesscontrol.cairo index 9f06a54da..3c4550016 100644 --- a/packages/access/src/tests/test_accesscontrol.cairo +++ b/packages/access/src/tests/test_accesscontrol.cairo @@ -3,8 +3,8 @@ use crate::accesscontrol::AccessControlComponent::{ }; use crate::accesscontrol::interface::{IAccessControl, IAccessControlCamel, IACCESSCONTROL_ID}; use crate::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE}; -use crate::tests::mocks::accesscontrol_mocks::DualCaseAccessControlMock; use openzeppelin_introspection::interface::ISRC5; +use openzeppelin_test_common::mocks::accesscontrol::DualCaseAccessControlMock; use openzeppelin_testing::constants::{ ADMIN, AUTHORIZED, OTHER, OTHER_ADMIN, ROLE, OTHER_ROLE, ZERO }; diff --git a/packages/access/src/tests/test_ownable.cairo b/packages/access/src/tests/test_ownable.cairo index 0c7fed3aa..47457cbc5 100644 --- a/packages/access/src/tests/test_ownable.cairo +++ b/packages/access/src/tests/test_ownable.cairo @@ -2,8 +2,7 @@ use core::num::traits::Zero; use crate::ownable::OwnableComponent::InternalTrait; use crate::ownable::OwnableComponent; use crate::ownable::interface::{IOwnable, IOwnableCamelOnly}; -use crate::tests::mocks::ownable_mocks::DualCaseOwnableMock; - +use openzeppelin_test_common::mocks::ownable::DualCaseOwnableMock; use openzeppelin_test_common::ownable::OwnableSpyHelpers; use openzeppelin_testing::constants::{ZERO, OTHER, OWNER, RECIPIENT}; use snforge_std::{spy_events, test_address, start_cheat_caller_address}; diff --git a/packages/access/src/tests/test_ownable_twostep.cairo b/packages/access/src/tests/test_ownable_twostep.cairo index eff125e82..5965bd926 100644 --- a/packages/access/src/tests/test_ownable_twostep.cairo +++ b/packages/access/src/tests/test_ownable_twostep.cairo @@ -2,7 +2,7 @@ use core::num::traits::Zero; use crate::ownable::OwnableComponent::{InternalTrait, OwnershipTransferStarted}; use crate::ownable::OwnableComponent; use crate::ownable::interface::{IOwnableTwoStep, IOwnableTwoStepCamelOnly}; -use crate::tests::mocks::ownable_mocks::DualCaseTwoStepOwnableMock; +use openzeppelin_test_common::mocks::ownable::DualCaseTwoStepOwnableMock; use openzeppelin_test_common::ownable::OwnableSpyHelpers; use openzeppelin_testing::constants::{ZERO, OWNER, OTHER, NEW_OWNER}; use openzeppelin_testing::events::EventSpyExt; diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index df108ed9f..de7a83b55 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -38,3 +38,16 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::account::DualCaseAccountMock", + "openzeppelin_test_common::mocks::account::SnakeAccountMock", + "openzeppelin_test_common::mocks::account::SnakeAccountPanicMock", + "openzeppelin_test_common::mocks::account::CamelAccountPanicMock", + "openzeppelin_test_common::mocks::eth_account::SnakeEthAccountMock", + "openzeppelin_test_common::mocks::eth_account::SnakeEthAccountPanicMock", + "openzeppelin_test_common::mocks::eth_account::CamelEthAccountPanicMock", + "openzeppelin_test_common::mocks::eth_account::DualCaseEthAccountMock", + "openzeppelin_test_common::mocks::src9::SRC9AccountMock", + "openzeppelin_test_common::mocks::simple::SimpleMock", + "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", +] diff --git a/packages/account/src/lib.cairo b/packages/account/src/lib.cairo index 6e4ed8a32..05713c6a0 100644 --- a/packages/account/src/lib.cairo +++ b/packages/account/src/lib.cairo @@ -5,7 +5,8 @@ pub mod eth_account; pub mod extensions; pub mod interface; -pub mod tests; +#[cfg(test)] +mod tests; pub mod utils; pub use account::AccountComponent; diff --git a/packages/account/src/tests.cairo b/packages/account/src/tests.cairo index c02171f01..89661e07d 100644 --- a/packages/account/src/tests.cairo +++ b/packages/account/src/tests.cairo @@ -1,15 +1,8 @@ mod extensions; -pub(crate) mod mocks; -#[cfg(test)] mod test_account; -#[cfg(test)] mod test_dual_account; -#[cfg(test)] mod test_dual_eth_account; -#[cfg(test)] mod test_eth_account; -#[cfg(test)] mod test_secp256k1; -#[cfg(test)] mod test_signature; diff --git a/packages/account/src/tests/extensions.cairo b/packages/account/src/tests/extensions.cairo index ea5d3f334..b3ecde616 100644 --- a/packages/account/src/tests/extensions.cairo +++ b/packages/account/src/tests/extensions.cairo @@ -1,4 +1,2 @@ -#[cfg(test)] mod test_snip12_utils; -#[cfg(test)] mod test_src9; diff --git a/packages/account/src/tests/extensions/test_src9.cairo b/packages/account/src/tests/extensions/test_src9.cairo index 74f098489..9547ce586 100644 --- a/packages/account/src/tests/extensions/test_src9.cairo +++ b/packages/account/src/tests/extensions/test_src9.cairo @@ -3,8 +3,8 @@ use crate::extensions::SRC9Component; use crate::extensions::src9::interface::{ISRC9_V2Dispatcher, ISRC9_V2DispatcherTrait}; use crate::extensions::src9::interface::{OutsideExecution, ISRC9_V2_ID}; use crate::extensions::src9::snip12_utils::OutsideExecutionStructHash; -use crate::tests::mocks::src9_mocks::SRC9AccountMock; use openzeppelin_introspection::interface::{ISRC5, ISRC5_ID}; +use openzeppelin_test_common::mocks::src9::SRC9AccountMock; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{RECIPIENT, OWNER, OTHER, FELT_VALUE}; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; diff --git a/packages/account/src/tests/mocks.cairo b/packages/account/src/tests/mocks.cairo deleted file mode 100644 index 3d873c28b..000000000 --- a/packages/account/src/tests/mocks.cairo +++ /dev/null @@ -1,5 +0,0 @@ -pub(crate) mod account_mocks; -pub(crate) mod eth_account_mocks; -pub(crate) mod non_implementing_mock; -pub(crate) mod simple_mock; -pub(crate) mod src9_mocks; diff --git a/packages/account/src/tests/test_account.cairo b/packages/account/src/tests/test_account.cairo index 9b906e756..4924f3ab4 100644 --- a/packages/account/src/tests/test_account.cairo +++ b/packages/account/src/tests/test_account.cairo @@ -4,11 +4,11 @@ use crate::AccountComponent::{PublicKeyCamelImpl, PublicKeyImpl}; use crate::AccountComponent; use crate::interface::{AccountABIDispatcherTrait, AccountABIDispatcher}; use crate::interface::{ISRC6, ISRC6_ID}; -use crate::tests::mocks::account_mocks::DualCaseAccountMock; -use crate::tests::mocks::simple_mock::{ISimpleMockDispatcher, ISimpleMockDispatcherTrait}; use openzeppelin_introspection::interface::{ISRC5, ISRC5_ID}; use openzeppelin_test_common::account::{AccountSpyHelpers, SignedTransactionData}; use openzeppelin_test_common::account::{SIGNED_TX_DATA, get_accept_ownership_signature}; +use openzeppelin_test_common::mocks::account::DualCaseAccountMock; +use openzeppelin_test_common::mocks::simple::{ISimpleMockDispatcher, ISimpleMockDispatcherTrait}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::stark::{KEY_PAIR, KEY_PAIR_2}; use openzeppelin_testing::constants::{ diff --git a/packages/account/src/tests/test_eth_account.cairo b/packages/account/src/tests/test_eth_account.cairo index cae546168..1e62f5bb3 100644 --- a/packages/account/src/tests/test_eth_account.cairo +++ b/packages/account/src/tests/test_eth_account.cairo @@ -3,8 +3,6 @@ use crate::EthAccountComponent::{PublicKeyCamelImpl, PublicKeyImpl}; use crate::EthAccountComponent; use crate::interface::{EthAccountABIDispatcherTrait, EthAccountABIDispatcher}; use crate::interface::{ISRC6, ISRC6_ID}; -use crate::tests::mocks::eth_account_mocks::DualCaseEthAccountMock; -use crate::tests::mocks::simple_mock::{ISimpleMockDispatcher, ISimpleMockDispatcherTrait}; use crate::utils::secp256k1::{DebugSecp256k1Point, Secp256k1PointPartialEq}; use crate::utils::signature::EthSignature; use openzeppelin_introspection::interface::{ISRC5, ISRC5_ID}; @@ -12,6 +10,8 @@ use openzeppelin_test_common::eth_account::EthAccountSpyHelpers; use openzeppelin_test_common::eth_account::{ SIGNED_TX_DATA, SignedTransactionData, get_accept_ownership_signature }; +use openzeppelin_test_common::mocks::eth_account::DualCaseEthAccountMock; +use openzeppelin_test_common::mocks::simple::{ISimpleMockDispatcher, ISimpleMockDispatcherTrait}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::secp256k1::{KEY_PAIR, KEY_PAIR_2}; use openzeppelin_testing::constants::{ diff --git a/packages/test_common/src/lib.cairo b/packages/test_common/src/lib.cairo index 8013cedc4..55dcb574e 100644 --- a/packages/test_common/src/lib.cairo +++ b/packages/test_common/src/lib.cairo @@ -3,6 +3,7 @@ pub mod erc1155; pub mod erc20; pub mod erc721; pub mod eth_account; +pub mod mocks; pub mod ownable; pub mod upgrades; pub mod vesting; diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo new file mode 100644 index 000000000..9d79b8e03 --- /dev/null +++ b/packages/test_common/src/mocks.cairo @@ -0,0 +1,7 @@ +pub mod accesscontrol; +pub mod account; +pub mod eth_account; +pub mod non_implementing; +pub mod ownable; +pub mod simple; +pub mod src9; diff --git a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo b/packages/test_common/src/mocks/accesscontrol.cairo similarity index 91% rename from packages/access/src/tests/mocks/accesscontrol_mocks.cairo rename to packages/test_common/src/mocks/accesscontrol.cairo index 8dffc2e69..6fbb95b62 100644 --- a/packages/access/src/tests/mocks/accesscontrol_mocks.cairo +++ b/packages/test_common/src/mocks/accesscontrol.cairo @@ -1,8 +1,7 @@ #[starknet::contract] -#[cfg(feature: 'mock_contract')] -pub(crate) mod DualCaseAccessControlMock { - use crate::accesscontrol::AccessControlComponent; - use crate::accesscontrol::DEFAULT_ADMIN_ROLE; +pub mod DualCaseAccessControlMock { + use openzeppelin_access::accesscontrol::AccessControlComponent; + use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; @@ -40,9 +39,9 @@ pub(crate) mod DualCaseAccessControlMock { } #[starknet::contract] -pub(crate) mod SnakeAccessControlMock { - use crate::accesscontrol::AccessControlComponent; - use crate::accesscontrol::DEFAULT_ADMIN_ROLE; +pub mod SnakeAccessControlMock { + use openzeppelin_access::accesscontrol::AccessControlComponent; + use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; @@ -84,9 +83,9 @@ pub(crate) mod SnakeAccessControlMock { } #[starknet::contract] -pub(crate) mod CamelAccessControlMock { - use crate::accesscontrol::AccessControlComponent; - use crate::accesscontrol::DEFAULT_ADMIN_ROLE; +pub mod CamelAccessControlMock { + use openzeppelin_access::accesscontrol::AccessControlComponent; + use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; @@ -135,7 +134,7 @@ pub(crate) mod CamelAccessControlMock { // false for bool #[starknet::contract] -pub(crate) mod SnakeAccessControlPanicMock { +pub mod SnakeAccessControlPanicMock { use starknet::ContractAddress; #[storage] @@ -180,7 +179,7 @@ pub(crate) mod SnakeAccessControlPanicMock { } #[starknet::contract] -pub(crate) mod CamelAccessControlPanicMock { +pub mod CamelAccessControlPanicMock { use starknet::ContractAddress; #[storage] diff --git a/packages/account/src/tests/mocks/account_mocks.cairo b/packages/test_common/src/mocks/account.cairo similarity index 95% rename from packages/account/src/tests/mocks/account_mocks.cairo rename to packages/test_common/src/mocks/account.cairo index faddfdf9f..3fc0e2435 100644 --- a/packages/account/src/tests/mocks/account_mocks.cairo +++ b/packages/test_common/src/mocks/account.cairo @@ -1,6 +1,6 @@ #[starknet::contract(account)] -pub(crate) mod DualCaseAccountMock { - use crate::AccountComponent; +pub mod DualCaseAccountMock { + use openzeppelin_account::AccountComponent; use openzeppelin_introspection::src5::SRC5Component; component!(path: AccountComponent, storage: account, event: AccountEvent); @@ -45,8 +45,8 @@ pub(crate) mod DualCaseAccountMock { } #[starknet::contract(account)] -pub(crate) mod SnakeAccountMock { - use crate::AccountComponent; +pub mod SnakeAccountMock { + use openzeppelin_account::AccountComponent; use openzeppelin_introspection::src5::SRC5Component; component!(path: AccountComponent, storage: account, event: AccountEvent); @@ -87,8 +87,8 @@ pub(crate) mod SnakeAccountMock { } #[starknet::contract(account)] -pub(crate) mod CamelAccountMock { - use crate::AccountComponent; +pub mod CamelAccountMock { + use openzeppelin_account::AccountComponent; use openzeppelin_introspection::src5::SRC5Component; use starknet::account::Call; @@ -151,7 +151,7 @@ pub(crate) mod CamelAccountMock { // false for bool #[starknet::contract] -pub(crate) mod SnakeAccountPanicMock { +pub mod SnakeAccountPanicMock { #[storage] pub struct Storage {} @@ -188,7 +188,7 @@ pub(crate) mod SnakeAccountPanicMock { } #[starknet::contract] -pub(crate) mod CamelAccountPanicMock { +pub mod CamelAccountPanicMock { #[storage] pub struct Storage {} diff --git a/packages/account/src/tests/mocks/eth_account_mocks.cairo b/packages/test_common/src/mocks/eth_account.cairo similarity index 91% rename from packages/account/src/tests/mocks/eth_account_mocks.cairo rename to packages/test_common/src/mocks/eth_account.cairo index 2787e76d1..e1724e7f6 100644 --- a/packages/account/src/tests/mocks/eth_account_mocks.cairo +++ b/packages/test_common/src/mocks/eth_account.cairo @@ -1,7 +1,7 @@ #[starknet::contract(account)] -pub(crate) mod DualCaseEthAccountMock { - use crate::EthAccountComponent; - use crate::interface::EthPublicKey; +pub mod DualCaseEthAccountMock { + use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::interface::EthPublicKey; use openzeppelin_introspection::src5::SRC5Component; component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); @@ -43,9 +43,9 @@ pub(crate) mod DualCaseEthAccountMock { } #[starknet::contract(account)] -pub(crate) mod SnakeEthAccountMock { - use crate::EthAccountComponent; - use crate::interface::EthPublicKey; +pub mod SnakeEthAccountMock { + use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::interface::EthPublicKey; use openzeppelin_introspection::src5::SRC5Component; component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); @@ -83,9 +83,9 @@ pub(crate) mod SnakeEthAccountMock { } #[starknet::contract(account)] -pub(crate) mod CamelEthAccountMock { - use crate::EthAccountComponent; - use crate::interface::EthPublicKey; +pub mod CamelEthAccountMock { + use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::interface::EthPublicKey; use openzeppelin_introspection::src5::SRC5Component; use starknet::account::Call; @@ -146,8 +146,8 @@ pub(crate) mod CamelEthAccountMock { // false for bool #[starknet::contract] -pub(crate) mod SnakeEthAccountPanicMock { - use crate::interface::EthPublicKey; +pub mod SnakeEthAccountPanicMock { + use openzeppelin_account::interface::EthPublicKey; use starknet::SyscallResultTrait; use starknet::secp256_trait::Secp256Trait; @@ -187,8 +187,8 @@ pub(crate) mod SnakeEthAccountPanicMock { } #[starknet::contract] -pub(crate) mod CamelEthAccountPanicMock { - use crate::interface::EthPublicKey; +pub mod CamelEthAccountPanicMock { + use openzeppelin_account::interface::EthPublicKey; use starknet::SyscallResultTrait; use starknet::secp256_trait::Secp256Trait; diff --git a/packages/account/src/tests/mocks/non_implementing_mock.cairo b/packages/test_common/src/mocks/non_implementing.cairo similarity index 80% rename from packages/account/src/tests/mocks/non_implementing_mock.cairo rename to packages/test_common/src/mocks/non_implementing.cairo index 67d632c72..4f54b0fd3 100644 --- a/packages/account/src/tests/mocks/non_implementing_mock.cairo +++ b/packages/test_common/src/mocks/non_implementing.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub(crate) mod NonImplementingMock { +pub mod NonImplementingMock { #[storage] pub struct Storage {} diff --git a/packages/access/src/tests/mocks/ownable_mocks.cairo b/packages/test_common/src/mocks/ownable.cairo similarity index 90% rename from packages/access/src/tests/mocks/ownable_mocks.cairo rename to packages/test_common/src/mocks/ownable.cairo index f3456c68a..5c5ba55b3 100644 --- a/packages/access/src/tests/mocks/ownable_mocks.cairo +++ b/packages/test_common/src/mocks/ownable.cairo @@ -1,6 +1,6 @@ #[starknet::contract] -pub(crate) mod DualCaseOwnableMock { - use crate::ownable::OwnableComponent; +pub mod DualCaseOwnableMock { + use openzeppelin_access::ownable::OwnableComponent; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -29,8 +29,8 @@ pub(crate) mod DualCaseOwnableMock { } #[starknet::contract] -pub(crate) mod SnakeOwnableMock { - use crate::ownable::OwnableComponent; +pub mod SnakeOwnableMock { + use openzeppelin_access::ownable::OwnableComponent; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -59,9 +59,9 @@ pub(crate) mod SnakeOwnableMock { } #[starknet::contract] -pub(crate) mod CamelOwnableMock { - use crate::ownable::OwnableComponent; - use crate::ownable::interface::IOwnable; +pub mod CamelOwnableMock { + use openzeppelin_access::ownable::OwnableComponent; + use openzeppelin_access::ownable::interface::IOwnable; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -100,7 +100,7 @@ pub(crate) mod CamelOwnableMock { } #[starknet::contract] -pub(crate) mod SnakeOwnablePanicMock { +pub mod SnakeOwnablePanicMock { use core::num::traits::Zero; use starknet::ContractAddress; @@ -129,7 +129,7 @@ pub(crate) mod SnakeOwnablePanicMock { } #[starknet::contract] -pub(crate) mod CamelOwnablePanicMock { +pub mod CamelOwnablePanicMock { use core::num::traits::Zero; use starknet::ContractAddress; @@ -158,8 +158,8 @@ pub(crate) mod CamelOwnablePanicMock { } #[starknet::contract] -pub(crate) mod DualCaseTwoStepOwnableMock { - use crate::ownable::OwnableComponent; +pub mod DualCaseTwoStepOwnableMock { + use openzeppelin_access::ownable::OwnableComponent; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); diff --git a/packages/account/src/tests/mocks/simple_mock.cairo b/packages/test_common/src/mocks/simple.cairo similarity index 90% rename from packages/account/src/tests/mocks/simple_mock.cairo rename to packages/test_common/src/mocks/simple.cairo index f16386735..d1eab519a 100644 --- a/packages/account/src/tests/mocks/simple_mock.cairo +++ b/packages/test_common/src/mocks/simple.cairo @@ -1,12 +1,13 @@ #[starknet::interface] -pub(crate) trait ISimpleMock { +pub trait ISimpleMock { fn increase_balance(ref self: TContractState, amount: felt252) -> bool; fn get_balance(self: @TContractState) -> felt252; } #[starknet::contract] -pub(crate) mod SimpleMock { +pub mod SimpleMock { use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + #[storage] pub struct Storage { pub balance: felt252, diff --git a/packages/account/src/tests/mocks/src9_mocks.cairo b/packages/test_common/src/mocks/src9.cairo similarity index 93% rename from packages/account/src/tests/mocks/src9_mocks.cairo rename to packages/test_common/src/mocks/src9.cairo index 0f41818f2..a7a7bcafb 100644 --- a/packages/account/src/tests/mocks/src9_mocks.cairo +++ b/packages/test_common/src/mocks/src9.cairo @@ -1,7 +1,7 @@ #[starknet::contract(account)] -pub(crate) mod SRC9AccountMock { - use crate::AccountComponent; - use crate::extensions::SRC9Component; +pub mod SRC9AccountMock { + use openzeppelin_account::AccountComponent; + use openzeppelin_account::extensions::SRC9Component; use openzeppelin_introspection::src5::SRC5Component; use starknet::storage::StoragePointerWriteAccess; From 094081c8b7d2bc6f3b9f31d2df88e84acfc52ff1 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 19:31:28 -0400 Subject: [PATCH 08/76] Remove token dual dispatchers (#1175) * feat: remove modules * fix: mock * fix: linter * fix: tests * fix: mock * feat: apply review suggestions --- .../src/tests/mocks/account_mocks.cairo | 58 -- .../src/tests/mocks/erc1155_mocks.cairo | 51 -- .../tests/mocks/erc1155_receiver_mocks.cairo | 12 +- .../tests/mocks/erc721_receiver_mocks.cairo | 52 +- packages/presets/src/tests/test_erc1155.cairo | 64 +-- packages/presets/src/tests/test_erc721.cairo | 217 +------- packages/test_common/src/erc1155.cairo | 8 +- packages/token/src/erc1155.cairo | 2 - packages/token/src/erc1155/dual1155.cairo | 166 ------ .../token/src/erc1155/dual1155_receiver.cairo | 83 --- packages/token/src/erc1155/erc1155.cairo | 6 +- packages/token/src/erc20.cairo | 1 - packages/token/src/erc20/dual20.cairo | 107 ---- packages/token/src/erc721.cairo | 2 - packages/token/src/erc721/dual721.cairo | 174 ------ .../token/src/erc721/dual721_receiver.cairo | 47 -- packages/token/src/erc721/erc721.cairo | 4 +- packages/token/src/tests/erc1155.cairo | 2 - .../src/tests/erc1155/test_dual1155.cairo | 412 -------------- .../erc1155/test_dual1155_receiver.cairo | 148 ----- .../src/tests/erc1155/test_erc1155.cairo | 149 +---- packages/token/src/tests/erc20.cairo | 1 - .../token/src/tests/erc20/test_dual20.cairo | 309 ----------- packages/token/src/tests/erc721.cairo | 2 - .../token/src/tests/erc721/test_dual721.cairo | 507 ------------------ .../tests/erc721/test_dual721_receiver.cairo | 93 ---- .../token/src/tests/erc721/test_erc721.cairo | 288 +--------- packages/token/src/tests/mocks.cairo | 1 - .../token/src/tests/mocks/account_mocks.cairo | 100 ---- .../token/src/tests/mocks/erc1155_mocks.cairo | 254 --------- .../tests/mocks/erc1155_receiver_mocks.cairo | 154 ------ .../token/src/tests/mocks/erc20_mocks.cairo | 213 -------- .../tests/mocks/erc721_enumerable_mocks.cairo | 74 --- .../token/src/tests/mocks/erc721_mocks.cairo | 292 ---------- .../tests/mocks/erc721_receiver_mocks.cairo | 166 ------ .../tests/mocks/non_implementing_mock.cairo | 10 - 36 files changed, 17 insertions(+), 4212 deletions(-) delete mode 100644 packages/token/src/erc1155/dual1155.cairo delete mode 100644 packages/token/src/erc1155/dual1155_receiver.cairo delete mode 100644 packages/token/src/erc20/dual20.cairo delete mode 100644 packages/token/src/erc721/dual721.cairo delete mode 100644 packages/token/src/erc721/dual721_receiver.cairo delete mode 100644 packages/token/src/tests/erc1155/test_dual1155.cairo delete mode 100644 packages/token/src/tests/erc1155/test_dual1155_receiver.cairo delete mode 100644 packages/token/src/tests/erc20/test_dual20.cairo delete mode 100644 packages/token/src/tests/erc721/test_dual721.cairo delete mode 100644 packages/token/src/tests/erc721/test_dual721_receiver.cairo delete mode 100644 packages/token/src/tests/mocks/non_implementing_mock.cairo diff --git a/packages/presets/src/tests/mocks/account_mocks.cairo b/packages/presets/src/tests/mocks/account_mocks.cairo index f8f222dad..c92013f39 100644 --- a/packages/presets/src/tests/mocks/account_mocks.cairo +++ b/packages/presets/src/tests/mocks/account_mocks.cairo @@ -85,61 +85,3 @@ pub(crate) mod SnakeAccountMock { 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/presets/src/tests/mocks/erc1155_mocks.cairo b/packages/presets/src/tests/mocks/erc1155_mocks.cairo index ffa4a3ec4..8ebd48000 100644 --- a/packages/presets/src/tests/mocks/erc1155_mocks.cairo +++ b/packages/presets/src/tests/mocks/erc1155_mocks.cairo @@ -48,54 +48,3 @@ pub(crate) mod SnakeERC1155Mock { self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); } } - -#[starknet::contract] -pub(crate) mod CamelERC1155Mock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC1155 - #[abi(embed_v0)] - impl ERC1155Camel = ERC1155Component::ERC1155CamelImpl; - #[abi(embed_v0)] - impl ERC1155MetadataURIImpl = - ERC1155Component::ERC1155MetadataURIImpl; - impl ERC1155InternalImpl = ERC1155Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155: ERC1155Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155Event: ERC1155Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256, - value: u256 - ) { - self.erc1155.initializer(base_uri); - self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); - } -} diff --git a/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo b/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo index cb9f8ac25..30e722593 100644 --- a/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo +++ b/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub(crate) mod SnakeERC1155ReceiverMock { +pub(crate) mod DualCaseERC1155ReceiverMock { use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::ERC1155ReceiverComponent; @@ -8,16 +8,12 @@ pub(crate) mod SnakeERC1155ReceiverMock { path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent ); - // ERC1155Receiver + // ERC1155Receiver Mixin #[abi(embed_v0)] - impl ERC1155ReceiverImpl = - ERC1155ReceiverComponent::ERC1155ReceiverImpl; + impl ERC1155ReceiverMixinImpl = + ERC1155ReceiverComponent::ERC1155ReceiverMixinImpl; impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - #[storage] pub struct Storage { #[substorage(v0)] diff --git a/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo b/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo index 71154e21b..b18b1d647 100644 --- a/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo +++ b/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo @@ -1,7 +1,7 @@ const SUCCESS: felt252 = 'SUCCESS'; #[starknet::contract] -pub(crate) mod SnakeERC721ReceiverMock { +pub(crate) mod DualCaseERC721ReceiverMock { use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc721::ERC721ReceiverComponent; use starknet::ContractAddress; @@ -56,51 +56,7 @@ pub(crate) mod SnakeERC721ReceiverMock { 0 } } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC721ReceiverMock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc721::ERC721ReceiverComponent; - use starknet::ContractAddress; - - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721Receiver - impl ERC721ReceiverCamelImpl = ERC721ReceiverComponent::ERC721ReceiverCamelImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - #[constructor] - fn constructor(ref self: ContractState) { - self.erc721_receiver.initializer(); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { #[external(v0)] fn onERC721Received( self: @ContractState, @@ -109,11 +65,7 @@ pub(crate) mod CamelERC721ReceiverMock { tokenId: u256, data: Span ) -> felt252 { - if *data.at(0) == super::SUCCESS { - self.erc721_receiver.onERC721Received(operator, from, tokenId, data) - } else { - 0 - } + Self::on_erc721_received(self, operator, from, tokenId, data) } } } diff --git a/packages/presets/src/tests/test_erc1155.cairo b/packages/presets/src/tests/test_erc1155.cairo index 01c0518c8..7cfb30f97 100644 --- a/packages/presets/src/tests/test_erc1155.cairo +++ b/packages/presets/src/tests/test_erc1155.cairo @@ -2,7 +2,7 @@ use core::num::traits::Zero; use crate::interfaces::{ERC1155UpgradeableABIDispatcher, ERC1155UpgradeableABIDispatcherTrait}; use openzeppelin_test_common::erc1155::ERC1155SpyHelpers; use openzeppelin_test_common::erc1155::{ - setup_account, setup_receiver, setup_camel_receiver, deploy_another_account_at, setup_src5 + setup_account, setup_receiver, deploy_another_account_at, setup_src5 }; use openzeppelin_test_common::erc1155::{get_ids_and_values, get_ids_and_split_values}; use openzeppelin_test_common::ownable::OwnableSpyHelpers; @@ -168,21 +168,6 @@ fn test_safe_transfer_from_to_receiver() { assert_state_after_transfer_single(dispatcher, owner, recipient, TOKEN_ID); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_to_camel_receiver() { - let (mut spy, dispatcher, owner) = setup_dispatcher(); - let contract = dispatcher.contract_address; - let recipient = setup_camel_receiver(); - - assert_state_before_transfer_single(dispatcher, owner, recipient, TOKEN_ID); - - dispatcher.safe_transfer_from(owner, recipient, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - spy.assert_only_event_transfer_single(contract, owner, owner, recipient, TOKEN_ID, TOKEN_VALUE); - - assert_state_after_transfer_single(dispatcher, owner, recipient, TOKEN_ID); -} - #[test] fn test_safeTransferFrom_to_receiver() { let (mut spy, dispatcher, owner) = setup_dispatcher(); @@ -197,21 +182,6 @@ fn test_safeTransferFrom_to_receiver() { assert_state_after_transfer_single(dispatcher, owner, recipient, TOKEN_ID); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_to_camel_receiver() { - let (mut spy, dispatcher, owner) = setup_dispatcher(); - let contract = dispatcher.contract_address; - let recipient = setup_camel_receiver(); - - assert_state_before_transfer_single(dispatcher, owner, recipient, TOKEN_ID); - - dispatcher.safeTransferFrom(owner, recipient, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - spy.assert_only_event_transfer_single(contract, owner, owner, recipient, TOKEN_ID, TOKEN_VALUE); - - assert_state_after_transfer_single(dispatcher, owner, recipient, TOKEN_ID); -} - #[test] fn test_safe_transfer_from_to_account() { let (mut spy, dispatcher, owner) = setup_dispatcher(); @@ -393,22 +363,6 @@ fn test_safe_batch_transfer_from_to_receiver() { assert_state_after_transfer_batch(dispatcher, owner, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_batch_transfer_from_to_camel_receiver() { - let (mut spy, dispatcher, owner) = setup_dispatcher(); - let contract = dispatcher.contract_address; - let recipient = setup_camel_receiver(); - let (token_ids, values) = get_ids_and_values(); - - assert_state_before_transfer_batch(dispatcher, owner, recipient, token_ids, values); - - dispatcher.safe_batch_transfer_from(owner, recipient, token_ids, values, EMPTY_DATA()); - spy.assert_only_event_transfer_batch(contract, owner, owner, recipient, token_ids, values); - - assert_state_after_transfer_batch(dispatcher, owner, recipient, token_ids, values); -} - #[test] fn test_safeBatchTransferFrom_to_receiver() { let (mut spy, dispatcher, owner) = setup_dispatcher(); @@ -424,22 +378,6 @@ fn test_safeBatchTransferFrom_to_receiver() { assert_state_after_transfer_batch(dispatcher, owner, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeBatchTransferFrom_to_camel_receiver() { - let (mut spy, dispatcher, owner) = setup_dispatcher(); - let contract = dispatcher.contract_address; - let recipient = setup_camel_receiver(); - let (token_ids, values) = get_ids_and_values(); - - assert_state_before_transfer_batch(dispatcher, owner, recipient, token_ids, values); - - dispatcher.safeBatchTransferFrom(owner, recipient, token_ids, values, EMPTY_DATA()); - spy.assert_only_event_transfer_batch(contract, owner, owner, recipient, token_ids, values); - - assert_state_after_transfer_batch(dispatcher, owner, recipient, token_ids, values); -} - #[test] fn test_safe_batch_transfer_from_to_account() { let (mut spy, dispatcher, owner) = setup_dispatcher(); diff --git a/packages/presets/src/tests/test_erc721.cairo b/packages/presets/src/tests/test_erc721.cairo index 2aecede3a..6800e55b8 100644 --- a/packages/presets/src/tests/test_erc721.cairo +++ b/packages/presets/src/tests/test_erc721.cairo @@ -64,11 +64,7 @@ fn setup_dispatcher() -> (EventSpy, ERC721UpgradeableABIDispatcher) { } fn setup_receiver() -> ContractAddress { - utils::declare_and_deploy("SnakeERC721ReceiverMock", array![]) -} - -fn setup_camel_receiver() -> ContractAddress { - utils::declare_and_deploy("CamelERC721ReceiverMock", array![]) + utils::declare_and_deploy("DualCaseERC721ReceiverMock", array![]) } fn setup_account() -> ContractAddress { @@ -76,11 +72,6 @@ fn setup_account() -> ContractAddress { utils::declare_and_deploy("DualCaseAccountMock", calldata) } -fn setup_camel_account() -> ContractAddress { - let mut calldata = array![PUBKEY]; - utils::declare_and_deploy("CamelAccountMock", calldata) -} - // // mint_assets // @@ -513,38 +504,6 @@ fn test_safeTransferFrom_to_account() { assert_state_after_transfer(dispatcher, owner, account, token_id); } -#[test] -fn test_safe_transfer_from_to_account_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let account = setup_camel_account(); - let token_id = TOKEN_1; - let owner = OWNER(); - spy.drop_all_events(); - - assert_state_before_transfer(dispatcher, owner, account, token_id); - - dispatcher.safe_transfer_from(owner, account, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); - - assert_state_after_transfer(dispatcher, owner, account, token_id); -} - -#[test] -fn test_safeTransferFrom_to_account_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let account = setup_camel_account(); - let token_id = TOKEN_1; - let owner = OWNER(); - spy.drop_all_events(); - - assert_state_before_transfer(dispatcher, owner, account, token_id); - - dispatcher.safeTransferFrom(owner, account, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, account, token_id); - - assert_state_after_transfer(dispatcher, owner, account, token_id); -} - #[test] fn test_safe_transfer_from_to_receiver() { let (mut spy, dispatcher) = setup_dispatcher(); @@ -575,38 +534,6 @@ fn test_safeTransferFrom_to_receiver() { assert_state_after_transfer(dispatcher, owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_to_receiver_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_to_receiver_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - #[test] #[should_panic(expected: ('ERC721: safe transfer failed',))] fn test_safe_transfer_from_to_receiver_failure() { @@ -629,30 +556,6 @@ fn test_safeTransferFrom_to_receiver_failure() { dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(false)); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: ('ERC721: safe transfer failed',))] -fn test_safe_transfer_from_to_receiver_failure_camel() { - let (_, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(false)); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: ('ERC721: safe transfer failed',))] -fn test_safeTransferFrom_to_receiver_failure_camel() { - let (_, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(false)); -} - #[test] #[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] @@ -741,44 +644,6 @@ fn test_safeTransferFrom_to_owner() { assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_to_owner_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let token_id = TOKEN_1; - let receiver = setup_camel_receiver(); - - dispatcher.transfer_from(OWNER(), receiver, token_id); - spy.drop_event(); - - assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); - - start_cheat_caller_address(dispatcher.contract_address, receiver); - dispatcher.safe_transfer_from(receiver, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); - - assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_to_owner_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let token_id = TOKEN_1; - let receiver = setup_camel_receiver(); - - dispatcher.transfer_from(OWNER(), receiver, token_id); - spy.drop_event(); - - assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); - - start_cheat_caller_address(dispatcher.contract_address, receiver); - dispatcher.safeTransferFrom(receiver, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, receiver, receiver, token_id); - - assert_state_transfer_to_self(dispatcher, receiver, token_id, 1); -} - #[test] fn test_safe_transfer_from_approved() { let (mut spy, dispatcher) = setup_dispatcher(); @@ -817,46 +682,6 @@ fn test_safeTransferFrom_approved() { assert_state_after_transfer(dispatcher, owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_approved_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.approve(OPERATOR(), token_id); - spy.drop_event(); - - start_cheat_caller_address(dispatcher.contract_address, OPERATOR()); - dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_approved_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.approve(OPERATOR(), token_id); - spy.drop_event(); - - start_cheat_caller_address(dispatcher.contract_address, OPERATOR()); - dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - #[test] fn test_safe_transfer_from_approved_for_all() { let (mut spy, dispatcher) = setup_dispatcher(); @@ -895,46 +720,6 @@ fn test_safeTransferFrom_approved_for_all() { assert_state_after_transfer(dispatcher, owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_approved_for_all_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.set_approval_for_all(OPERATOR(), true); - spy.drop_event(); - - start_cheat_caller_address(dispatcher.contract_address, OPERATOR()); - dispatcher.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_approved_for_all_camel() { - let (mut spy, dispatcher) = setup_dispatcher(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_1; - let owner = OWNER(); - - assert_state_before_transfer(dispatcher, owner, receiver, token_id); - - dispatcher.set_approval_for_all(OPERATOR(), true); - spy.drop_event(); - - start_cheat_caller_address(dispatcher.contract_address, OPERATOR()); - dispatcher.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(dispatcher.contract_address, owner, receiver, token_id); - - assert_state_after_transfer(dispatcher, owner, receiver, token_id); -} - #[test] #[should_panic(expected: ('ERC721: unauthorized caller',))] fn test_safe_transfer_from_unauthorized() { diff --git a/packages/test_common/src/erc1155.cairo b/packages/test_common/src/erc1155.cairo index 75350ec22..569c1c781 100644 --- a/packages/test_common/src/erc1155.cairo +++ b/packages/test_common/src/erc1155.cairo @@ -7,16 +7,12 @@ use snforge_std::EventSpy; use starknet::ContractAddress; pub fn setup_receiver() -> ContractAddress { - utils::declare_and_deploy("SnakeERC1155ReceiverMock", array![]) -} - -pub fn setup_camel_receiver() -> ContractAddress { - utils::declare_and_deploy("CamelERC1155ReceiverMock", array![]) + utils::declare_and_deploy("DualCaseERC1155ReceiverMock", array![]) } pub fn setup_account() -> ContractAddress { let calldata = array![PUBKEY]; - utils::declare_and_deploy("SnakeAccountMock", calldata) + utils::declare_and_deploy("DualCaseAccountMock", calldata) } pub fn deploy_another_account_at(existing: ContractAddress, target_address: ContractAddress) { diff --git a/packages/token/src/erc1155.cairo b/packages/token/src/erc1155.cairo index 68a61043a..8d92872cf 100644 --- a/packages/token/src/erc1155.cairo +++ b/packages/token/src/erc1155.cairo @@ -1,5 +1,3 @@ -pub mod dual1155; -pub mod dual1155_receiver; pub mod erc1155; pub mod erc1155_receiver; pub mod interface; diff --git a/packages/token/src/erc1155/dual1155.cairo b/packages/token/src/erc1155/dual1155.cairo deleted file mode 100644 index 95da3bfa2..000000000 --- a/packages/token/src/erc1155/dual1155.cairo +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc1155/dual1155.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseERC1155 { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC1155Trait { - fn uri(self: @DualCaseERC1155, token_id: u256) -> ByteArray; - fn balance_of(self: @DualCaseERC1155, account: ContractAddress, token_id: u256) -> u256; - fn balance_of_batch( - self: @DualCaseERC1155, accounts: Span, token_ids: Span - ) -> Span; - fn safe_transfer_from( - self: @DualCaseERC1155, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - value: u256, - data: Span - ); - fn safe_batch_transfer_from( - self: @DualCaseERC1155, - from: ContractAddress, - to: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ); - fn is_approved_for_all( - self: @DualCaseERC1155, owner: ContractAddress, operator: ContractAddress - ) -> bool; - fn set_approval_for_all(self: @DualCaseERC1155, operator: ContractAddress, approved: bool); - fn supports_interface(self: @DualCaseERC1155, interface_id: felt252) -> bool; -} - -impl DualCaseERC1155Impl of DualCaseERC1155Trait { - fn uri(self: @DualCaseERC1155, token_id: u256) -> ByteArray { - let mut args = array![]; - args.append_serde(token_id); - - call_contract_syscall(*self.contract_address, selectors::uri, args.span()).unwrap_and_cast() - } - - fn balance_of(self: @DualCaseERC1155, account: ContractAddress, token_id: u256) -> u256 { - let mut args = array![]; - args.append_serde(account); - args.append_serde(token_id); - - try_selector_with_fallback( - *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() - ) - .unwrap_and_cast() - } - - fn balance_of_batch( - self: @DualCaseERC1155, accounts: Span, token_ids: Span - ) -> Span { - let mut args = array![]; - args.append_serde(accounts); - args.append_serde(token_ids); - - try_selector_with_fallback( - *self.contract_address, - selectors::balance_of_batch, - selectors::balanceOfBatch, - args.span() - ) - .unwrap_and_cast() - } - - fn safe_transfer_from( - self: @DualCaseERC1155, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) { - let mut args = array![]; - args.append_serde(from); - args.append_serde(to); - args.append_serde(token_id); - args.append_serde(value); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::safe_transfer_from, - selectors::safeTransferFrom, - args.span() - ) - .unwrap_syscall(); - } - - fn safe_batch_transfer_from( - self: @DualCaseERC1155, - from: ContractAddress, - to: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) { - let mut args = array![]; - args.append_serde(from); - args.append_serde(to); - args.append_serde(token_ids); - args.append_serde(values); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::safe_batch_transfer_from, - selectors::safeBatchTransferFrom, - args.span() - ) - .unwrap_syscall(); - } - - fn is_approved_for_all( - self: @DualCaseERC1155, owner: ContractAddress, operator: ContractAddress - ) -> bool { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(operator); - - try_selector_with_fallback( - *self.contract_address, - selectors::is_approved_for_all, - selectors::isApprovedForAll, - args.span() - ) - .unwrap_and_cast() - } - - fn set_approval_for_all(self: @DualCaseERC1155, operator: ContractAddress, approved: bool) { - let mut args = array![]; - args.append_serde(operator); - args.append_serde(approved); - - try_selector_with_fallback( - *self.contract_address, - selectors::set_approval_for_all, - selectors::setApprovalForAll, - args.span() - ) - .unwrap_syscall(); - } - - fn supports_interface(self: @DualCaseERC1155, interface_id: felt252) -> bool { - let mut args = array![]; - args.append_serde(interface_id); - - call_contract_syscall(*self.contract_address, selectors::supports_interface, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc1155/dual1155_receiver.cairo b/packages/token/src/erc1155/dual1155_receiver.cairo deleted file mode 100644 index d123224e5..000000000 --- a/packages/token/src/erc1155/dual1155_receiver.cairo +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc1155/dual1155_receiver.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; - -#[derive(Copy, Drop)] -pub struct DualCaseERC1155Receiver { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC1155ReceiverTrait { - fn on_erc1155_received( - self: @DualCaseERC1155Receiver, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) -> felt252; - - fn on_erc1155_batch_received( - self: @DualCaseERC1155Receiver, - operator: ContractAddress, - from: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) -> felt252; -} - -impl DualCaseERC1155ReceiverImpl of DualCaseERC1155ReceiverTrait { - fn on_erc1155_received( - self: @DualCaseERC1155Receiver, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) -> felt252 { - let mut args = array![]; - args.append_serde(operator); - args.append_serde(from); - args.append_serde(token_id); - args.append_serde(value); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::on_erc1155_received, - selectors::onERC1155Received, - args.span() - ) - .unwrap_and_cast() - } - - fn on_erc1155_batch_received( - self: @DualCaseERC1155Receiver, - operator: ContractAddress, - from: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) -> felt252 { - let mut args = array![]; - args.append_serde(operator); - args.append_serde(from); - args.append_serde(token_ids); - args.append_serde(values); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::on_erc1155_batch_received, - selectors::onERC1155BatchReceived, - args.span() - ) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc1155/erc1155.cairo b/packages/token/src/erc1155/erc1155.cairo index 30128dd42..84dc32580 100644 --- a/packages/token/src/erc1155/erc1155.cairo +++ b/packages/token/src/erc1155/erc1155.cairo @@ -8,7 +8,7 @@ #[starknet::component] pub mod ERC1155Component { use core::num::traits::Zero; - use crate::erc1155::dual1155_receiver::{DualCaseERC1155Receiver, DualCaseERC1155ReceiverTrait}; + use crate::erc1155::interface::{IERC1155ReceiverDispatcher, IERC1155ReceiverDispatcherTrait}; use crate::erc1155::interface; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; @@ -684,7 +684,7 @@ pub mod ERC1155Component { let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; if src5_dispatcher.supports_interface(interface::IERC1155_RECEIVER_ID) { - DualCaseERC1155Receiver { contract_address: to } + IERC1155ReceiverDispatcher { contract_address: to } .on_erc1155_received( get_caller_address(), from, token_id, value, data ) == interface::IERC1155_RECEIVER_ID @@ -705,7 +705,7 @@ pub mod ERC1155Component { let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; if src5_dispatcher.supports_interface(interface::IERC1155_RECEIVER_ID) { - DualCaseERC1155Receiver { contract_address: to } + IERC1155ReceiverDispatcher { contract_address: to } .on_erc1155_batch_received( get_caller_address(), from, token_ids, values, data ) == interface::IERC1155_RECEIVER_ID diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index e40fbb2bb..0a44a41dd 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -1,4 +1,3 @@ -pub mod dual20; pub mod erc20; pub mod extensions; pub mod interface; diff --git a/packages/token/src/erc20/dual20.cairo b/packages/token/src/erc20/dual20.cairo deleted file mode 100644 index 20cefb030..000000000 --- a/packages/token/src/erc20/dual20.cairo +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/dual20.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseERC20 { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC20Trait { - fn name(self: @DualCaseERC20) -> ByteArray; - fn symbol(self: @DualCaseERC20) -> ByteArray; - fn decimals(self: @DualCaseERC20) -> u8; - fn total_supply(self: @DualCaseERC20) -> u256; - fn balance_of(self: @DualCaseERC20, account: ContractAddress) -> u256; - fn allowance(self: @DualCaseERC20, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(self: @DualCaseERC20, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - self: @DualCaseERC20, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(self: @DualCaseERC20, spender: ContractAddress, amount: u256) -> bool; -} - -impl DualCaseERC20Impl of DualCaseERC20Trait { - fn name(self: @DualCaseERC20) -> ByteArray { - let args = array![]; - call_contract_syscall(*self.contract_address, selectors::name, args.span()) - .unwrap_and_cast() - } - - fn symbol(self: @DualCaseERC20) -> ByteArray { - let args = array![]; - call_contract_syscall(*self.contract_address, selectors::symbol, args.span()) - .unwrap_and_cast() - } - - fn decimals(self: @DualCaseERC20) -> u8 { - let args = array![]; - call_contract_syscall(*self.contract_address, selectors::decimals, args.span()) - .unwrap_and_cast() - } - - fn total_supply(self: @DualCaseERC20) -> u256 { - let args = array![]; - try_selector_with_fallback( - *self.contract_address, selectors::total_supply, selectors::totalSupply, args.span() - ) - .unwrap_and_cast() - } - - fn balance_of(self: @DualCaseERC20, account: ContractAddress) -> u256 { - let mut args = array![]; - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() - ) - .unwrap_and_cast() - } - - fn allowance(self: @DualCaseERC20, owner: ContractAddress, spender: ContractAddress) -> u256 { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(spender); - - call_contract_syscall(*self.contract_address, selectors::allowance, args.span()) - .unwrap_and_cast() - } - - fn transfer(self: @DualCaseERC20, recipient: ContractAddress, amount: u256) -> bool { - let mut args = array![]; - args.append_serde(recipient); - args.append_serde(amount); - - call_contract_syscall(*self.contract_address, selectors::transfer, args.span()) - .unwrap_and_cast() - } - - fn transfer_from( - self: @DualCaseERC20, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool { - let mut args = array![]; - args.append_serde(sender); - args.append_serde(recipient); - args.append_serde(amount); - - try_selector_with_fallback( - *self.contract_address, selectors::transfer_from, selectors::transferFrom, args.span() - ) - .unwrap_and_cast() - } - - fn approve(self: @DualCaseERC20, spender: ContractAddress, amount: u256) -> bool { - let mut args = array![]; - args.append_serde(spender); - args.append_serde(amount); - - call_contract_syscall(*self.contract_address, selectors::approve, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc721.cairo b/packages/token/src/erc721.cairo index 3a08d821a..59fa1d854 100644 --- a/packages/token/src/erc721.cairo +++ b/packages/token/src/erc721.cairo @@ -1,5 +1,3 @@ -pub mod dual721; -pub mod dual721_receiver; pub mod erc721; pub mod erc721_receiver; pub mod extensions; diff --git a/packages/token/src/erc721/dual721.cairo b/packages/token/src/erc721/dual721.cairo deleted file mode 100644 index 7932cd559..000000000 --- a/packages/token/src/erc721/dual721.cairo +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc721/dual721.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; -use starknet::SyscallResultTrait; -use starknet::syscalls::call_contract_syscall; - -#[derive(Copy, Drop)] -pub struct DualCaseERC721 { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC721Trait { - fn name(self: @DualCaseERC721) -> ByteArray; - fn symbol(self: @DualCaseERC721) -> ByteArray; - fn token_uri(self: @DualCaseERC721, token_id: u256) -> ByteArray; - fn balance_of(self: @DualCaseERC721, account: ContractAddress) -> u256; - fn owner_of(self: @DualCaseERC721, token_id: u256) -> ContractAddress; - fn get_approved(self: @DualCaseERC721, token_id: u256) -> ContractAddress; - fn approve(self: @DualCaseERC721, to: ContractAddress, token_id: u256); - fn set_approval_for_all(self: @DualCaseERC721, operator: ContractAddress, approved: bool); - fn transfer_from( - self: @DualCaseERC721, from: ContractAddress, to: ContractAddress, token_id: u256 - ); - fn is_approved_for_all( - self: @DualCaseERC721, owner: ContractAddress, operator: ContractAddress - ) -> bool; - fn safe_transfer_from( - self: @DualCaseERC721, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ); - fn supports_interface(self: @DualCaseERC721, interface_id: felt252) -> bool; -} - -impl DualCaseERC721Impl of DualCaseERC721Trait { - fn name(self: @DualCaseERC721) -> ByteArray { - call_contract_syscall(*self.contract_address, selectors::name, array![].span()) - .unwrap_and_cast() - } - - fn symbol(self: @DualCaseERC721) -> ByteArray { - call_contract_syscall(*self.contract_address, selectors::symbol, array![].span()) - .unwrap_and_cast() - } - - fn token_uri(self: @DualCaseERC721, token_id: u256) -> ByteArray { - let mut args = array![]; - args.append_serde(token_id); - - try_selector_with_fallback( - *self.contract_address, selectors::token_uri, selectors::tokenURI, args.span() - ) - .unwrap_and_cast() - } - - fn balance_of(self: @DualCaseERC721, account: ContractAddress) -> u256 { - let mut args = array![]; - args.append_serde(account); - - try_selector_with_fallback( - *self.contract_address, selectors::balance_of, selectors::balanceOf, args.span() - ) - .unwrap_and_cast() - } - - fn owner_of(self: @DualCaseERC721, token_id: u256) -> ContractAddress { - let mut args = array![]; - args.append_serde(token_id); - - try_selector_with_fallback( - *self.contract_address, selectors::owner_of, selectors::ownerOf, args.span() - ) - .unwrap_and_cast() - } - - fn get_approved(self: @DualCaseERC721, token_id: u256) -> ContractAddress { - let mut args = array![]; - args.append_serde(token_id); - - try_selector_with_fallback( - *self.contract_address, selectors::get_approved, selectors::getApproved, args.span() - ) - .unwrap_and_cast() - } - - fn is_approved_for_all( - self: @DualCaseERC721, owner: ContractAddress, operator: ContractAddress - ) -> bool { - let mut args = array![]; - args.append_serde(owner); - args.append_serde(operator); - - try_selector_with_fallback( - *self.contract_address, - selectors::is_approved_for_all, - selectors::isApprovedForAll, - args.span() - ) - .unwrap_and_cast() - } - - fn approve(self: @DualCaseERC721, to: ContractAddress, token_id: u256) { - let mut args = array![]; - args.append_serde(to); - args.append_serde(token_id); - call_contract_syscall(*self.contract_address, selectors::approve, args.span()) - .unwrap_syscall(); - } - - fn set_approval_for_all(self: @DualCaseERC721, operator: ContractAddress, approved: bool) { - let mut args = array![]; - args.append_serde(operator); - args.append_serde(approved); - - try_selector_with_fallback( - *self.contract_address, - selectors::set_approval_for_all, - selectors::setApprovalForAll, - args.span() - ) - .unwrap_syscall(); - } - - fn transfer_from( - self: @DualCaseERC721, from: ContractAddress, to: ContractAddress, token_id: u256 - ) { - let mut args = array![]; - args.append_serde(from); - args.append_serde(to); - args.append_serde(token_id); - - try_selector_with_fallback( - *self.contract_address, selectors::transfer_from, selectors::transferFrom, args.span() - ) - .unwrap_syscall(); - } - - fn safe_transfer_from( - self: @DualCaseERC721, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - mut data: Span - ) { - let mut args = array![]; - args.append_serde(from); - args.append_serde(to); - args.append_serde(token_id); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::safe_transfer_from, - selectors::safeTransferFrom, - args.span() - ) - .unwrap_syscall(); - } - - fn supports_interface(self: @DualCaseERC721, interface_id: felt252) -> bool { - let mut args = array![]; - args.append_serde(interface_id); - - call_contract_syscall(*self.contract_address, selectors::supports_interface, args.span()) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc721/dual721_receiver.cairo b/packages/token/src/erc721/dual721_receiver.cairo deleted file mode 100644 index 1e616c5b6..000000000 --- a/packages/token/src/erc721/dual721_receiver.cairo +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc721/dual721_receiver.cairo) - -use openzeppelin_utils::UnwrapAndCast; -use openzeppelin_utils::selectors; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::try_selector_with_fallback; -use starknet::ContractAddress; - -#[derive(Copy, Drop)] -pub struct DualCaseERC721Receiver { - pub contract_address: ContractAddress -} - -pub trait DualCaseERC721ReceiverTrait { - fn on_erc721_received( - self: @DualCaseERC721Receiver, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252; -} - -impl DualCaseERC721ReceiverImpl of DualCaseERC721ReceiverTrait { - fn on_erc721_received( - self: @DualCaseERC721Receiver, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - let mut args = array![]; - args.append_serde(operator); - args.append_serde(from); - args.append_serde(token_id); - args.append_serde(data); - - try_selector_with_fallback( - *self.contract_address, - selectors::on_erc721_received, - selectors::onERC721Received, - args.span() - ) - .unwrap_and_cast() - } -} diff --git a/packages/token/src/erc721/erc721.cairo b/packages/token/src/erc721/erc721.cairo index d19a82cbd..a47874d9a 100644 --- a/packages/token/src/erc721/erc721.cairo +++ b/packages/token/src/erc721/erc721.cairo @@ -8,7 +8,7 @@ #[starknet::component] pub mod ERC721Component { use core::num::traits::Zero; - use crate::erc721::dual721_receiver::{DualCaseERC721Receiver, DualCaseERC721ReceiverTrait}; + use crate::erc721::interface::{IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait}; use crate::erc721::interface; use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; @@ -813,7 +813,7 @@ pub mod ERC721Component { let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; if src5_dispatcher.supports_interface(interface::IERC721_RECEIVER_ID) { - DualCaseERC721Receiver { contract_address: to } + IERC721ReceiverDispatcher { contract_address: to } .on_erc721_received( get_caller_address(), from, token_id, data ) == interface::IERC721_RECEIVER_ID diff --git a/packages/token/src/tests/erc1155.cairo b/packages/token/src/tests/erc1155.cairo index 799272ec1..e9bbde0ba 100644 --- a/packages/token/src/tests/erc1155.cairo +++ b/packages/token/src/tests/erc1155.cairo @@ -1,4 +1,2 @@ -mod test_dual1155; -mod test_dual1155_receiver; mod test_erc1155; mod test_erc1155_receiver; diff --git a/packages/token/src/tests/erc1155/test_dual1155.cairo b/packages/token/src/tests/erc1155/test_dual1155.cairo deleted file mode 100644 index cc15d9822..000000000 --- a/packages/token/src/tests/erc1155/test_dual1155.cairo +++ /dev/null @@ -1,412 +0,0 @@ -use core::num::traits::Zero; -use crate::erc1155::dual1155::{DualCaseERC1155, DualCaseERC1155Trait}; -use crate::erc1155::interface::IERC1155_ID; -use crate::erc1155::interface::{IERC1155CamelDispatcher, IERC1155CamelDispatcherTrait}; -use crate::erc1155::interface::{IERC1155Dispatcher, IERC1155DispatcherTrait}; -use openzeppelin_test_common::erc1155::{setup_account, setup_receiver}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{ - EMPTY_DATA, OWNER, RECIPIENT, OPERATOR, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE -}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::start_cheat_caller_address; -use starknet::ContractAddress; - -// -// Setup -// - -fn setup_snake() -> (DualCaseERC1155, IERC1155Dispatcher, ContractAddress) { - let base_uri: ByteArray = "URI"; - let owner = setup_account(); - let mut calldata = array![]; - calldata.append_serde(base_uri); - calldata.append_serde(owner); - calldata.append_serde(TOKEN_ID); - calldata.append_serde(TOKEN_VALUE); - let target = utils::declare_and_deploy("SnakeERC1155Mock", calldata); - ( - DualCaseERC1155 { contract_address: target }, - IERC1155Dispatcher { contract_address: target }, - owner - ) -} - -fn setup_camel() -> (DualCaseERC1155, IERC1155CamelDispatcher, ContractAddress) { - let base_uri: ByteArray = "URI"; - let owner = setup_account(); - let mut calldata = array![]; - calldata.append_serde(base_uri); - calldata.append_serde(owner); - calldata.append_serde(TOKEN_ID); - calldata.append_serde(TOKEN_VALUE); - let target = utils::declare_and_deploy("CamelERC1155Mock", calldata); - ( - DualCaseERC1155 { contract_address: target }, - IERC1155CamelDispatcher { contract_address: target }, - owner - ) -} - -fn setup_non_erc1155() -> DualCaseERC1155 { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseERC1155 { contract_address: target } -} - -fn setup_erc1155_panic() -> (DualCaseERC1155, DualCaseERC1155) { - let snake_target = utils::declare_and_deploy("SnakeERC1155PanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelERC1155PanicMock", array![]); - ( - DualCaseERC1155 { contract_address: snake_target }, - DualCaseERC1155 { contract_address: camel_target } - ) -} - -// -// Case agnostic methods -// - -#[test] -fn test_dual_uri_snake() { - let (snake_dispatcher, _, _) = setup_snake(); - assert_eq!(snake_dispatcher.uri(TOKEN_ID), "URI"); -} - -#[test] -fn test_dual_uri_camel() { - let (camel_dispatcher, _, _) = setup_camel(); - assert_eq!(camel_dispatcher.uri(TOKEN_ID), "URI"); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_uri() { - let dispatcher = setup_non_erc1155(); - dispatcher.uri(TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_uri_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.uri(TOKEN_ID); -} - -// -// snake_case target -// - -#[test] -fn test_dual_balance_of() { - let (dispatcher, _, owner) = setup_snake(); - assert_eq!(dispatcher.balance_of(owner, TOKEN_ID), TOKEN_VALUE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_balance_of() { - let dispatcher = setup_non_erc1155(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_balance_of_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -fn test_dual_balance_of_batch() { - let (dispatcher, _, owner) = setup_snake(); - let accounts = array![owner, RECIPIENT()].span(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - - let balances = dispatcher.balance_of_batch(accounts, token_ids); - assert_eq!(*balances.at(0), TOKEN_VALUE); - assert!((*balances.at(1)).is_zero()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_balance_of_batch() { - let dispatcher = setup_non_erc1155(); - let (accounts, token_ids) = get_accounts_and_ids(); - dispatcher.balance_of_batch(accounts, token_ids); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_balance_of_batch_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - let (accounts, token_ids) = get_accounts_and_ids(); - dispatcher.balance_of_batch(accounts, token_ids); -} - -#[test] -fn test_dual_safe_transfer_from() { - let (dispatcher, target, owner) = setup_snake(); - let receiver = setup_receiver(); - - start_cheat_caller_address(dispatcher.contract_address, owner); - dispatcher.safe_transfer_from(owner, receiver, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - assert_eq!(target.balance_of(receiver, TOKEN_ID), TOKEN_VALUE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_safe_transfer_from() { - let dispatcher = setup_non_erc1155(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_safe_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -fn test_dual_safe_batch_transfer_from() { - let (dispatcher, target, owner) = setup_snake(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - let values = array![TOKEN_VALUE, 0].span(); - let receiver = setup_receiver(); - - start_cheat_caller_address(dispatcher.contract_address, owner); - dispatcher.safe_batch_transfer_from(owner, receiver, token_ids, values, EMPTY_DATA()); - assert_eq!(target.balance_of(receiver, TOKEN_ID), TOKEN_VALUE); - assert!(target.balance_of(receiver, TOKEN_ID_2).is_zero()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_safe_batch_transfer_from() { - let dispatcher = setup_non_erc1155(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - let values = array![TOKEN_VALUE, 0].span(); - dispatcher.safe_batch_transfer_from(OWNER(), RECIPIENT(), token_ids, values, EMPTY_DATA()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_safe_batch_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - let values = array![TOKEN_VALUE, 0].span(); - dispatcher.safe_batch_transfer_from(OWNER(), RECIPIENT(), token_ids, values, EMPTY_DATA()); -} - -#[test] -fn test_dual_is_approved_for_all() { - let (dispatcher, target, _) = setup_snake(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = dispatcher.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_is_approved_for_all() { - let dispatcher = setup_non_erc1155(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_is_approved_for_all_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -fn test_dual_set_approval_for_all() { - let (dispatcher, target, _) = setup_snake(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = target.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_set_approval_for_all() { - let dispatcher = setup_non_erc1155(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_set_approval_for_all_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -#[test] -fn test_dual_supports_interface() { - let (dispatcher, _, _) = setup_snake(); - let supports_ierc1155 = dispatcher.supports_interface(IERC1155_ID); - assert!(supports_ierc1155); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_supports_interface() { - let dispatcher = setup_non_erc1155(); - dispatcher.supports_interface(IERC1155_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_supports_interface_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_panic(); - dispatcher.supports_interface(IERC1155_ID); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_balanceOf() { - let (dispatcher, _, owner) = setup_camel(); - assert_eq!(dispatcher.balance_of(owner, TOKEN_ID), TOKEN_VALUE); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_balanceOf_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - dispatcher.balance_of(OWNER(), TOKEN_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_balanceOfBatch() { - let (dispatcher, _, owner) = setup_camel(); - let accounts = array![owner, RECIPIENT()].span(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - - let balances = dispatcher.balance_of_batch(accounts, token_ids); - assert_eq!(*balances.at(0), TOKEN_VALUE); - assert!((*balances.at(1)).is_zero()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_balanceOfBatch_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - let (accounts, token_ids) = get_accounts_and_ids(); - dispatcher.balance_of_batch(accounts, token_ids); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_safeTransferFrom() { - let (dispatcher, target, owner) = setup_camel(); - let receiver = setup_receiver(); - - start_cheat_caller_address(dispatcher.contract_address, owner); - dispatcher.safe_transfer_from(owner, receiver, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - assert_eq!(target.balanceOf(receiver, TOKEN_ID), TOKEN_VALUE); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_safeTransferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_safeBatchTransferFrom() { - let (dispatcher, target, owner) = setup_camel(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - let values = array![TOKEN_VALUE, 0].span(); - let receiver = setup_receiver(); - - start_cheat_caller_address(dispatcher.contract_address, owner); - dispatcher.safe_batch_transfer_from(owner, receiver, token_ids, values, EMPTY_DATA()); - assert_eq!(target.balanceOf(receiver, TOKEN_ID), TOKEN_VALUE); - assert!(target.balanceOf(receiver, TOKEN_ID_2).is_zero()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_safeBatchTransferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - let token_ids = array![TOKEN_ID, TOKEN_ID_2].span(); - let values = array![TOKEN_VALUE, 0].span(); - dispatcher.safe_batch_transfer_from(OWNER(), RECIPIENT(), token_ids, values, EMPTY_DATA()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_isApprovedForAll() { - let (dispatcher, target, _) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.setApprovalForAll(OPERATOR(), true); - - let is_approved_for_all = dispatcher.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_isApprovedForAll_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_setApprovalForAll() { - let (dispatcher, target, _) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = target.isApprovedForAll(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_setApprovalForAll_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_panic(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -// -// Helpers -// - -fn get_accounts_and_ids() -> (Span, Span) { - let accounts = array![OWNER(), RECIPIENT()].span(); - let ids = array![TOKEN_ID, TOKEN_ID_2].span(); - (accounts, ids) -} diff --git a/packages/token/src/tests/erc1155/test_dual1155_receiver.cairo b/packages/token/src/tests/erc1155/test_dual1155_receiver.cairo deleted file mode 100644 index dc6c56108..000000000 --- a/packages/token/src/tests/erc1155/test_dual1155_receiver.cairo +++ /dev/null @@ -1,148 +0,0 @@ -use crate::erc1155::dual1155_receiver::{DualCaseERC1155Receiver, DualCaseERC1155ReceiverTrait}; -use crate::erc1155::interface::IERC1155_RECEIVER_ID; -use crate::erc1155::interface::{IERC1155ReceiverCamelDispatcher}; -use crate::erc1155::interface::{IERC1155ReceiverDispatcher}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{EMPTY_DATA, OPERATOR, OWNER, TOKEN_ID, TOKEN_VALUE}; - -// -// Setup -// - -fn setup_snake() -> (DualCaseERC1155Receiver, IERC1155ReceiverDispatcher) { - let mut calldata = array![]; - let target = utils::declare_and_deploy("SnakeERC1155ReceiverMock", calldata); - ( - DualCaseERC1155Receiver { contract_address: target }, - IERC1155ReceiverDispatcher { contract_address: target } - ) -} - -fn setup_camel() -> (DualCaseERC1155Receiver, IERC1155ReceiverCamelDispatcher) { - let mut calldata = array![]; - let target = utils::declare_and_deploy("CamelERC1155ReceiverMock", calldata); - ( - DualCaseERC1155Receiver { contract_address: target }, - IERC1155ReceiverCamelDispatcher { contract_address: target } - ) -} - -fn setup_non_erc1155_receiver() -> DualCaseERC1155Receiver { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseERC1155Receiver { contract_address: target } -} - -fn setup_erc1155_receiver_panic() -> (DualCaseERC1155Receiver, DualCaseERC1155Receiver) { - let snake_target = utils::declare_and_deploy("SnakeERC1155ReceiverPanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelERC1155ReceiverPanicMock", array![]); - ( - DualCaseERC1155Receiver { contract_address: snake_target }, - DualCaseERC1155Receiver { contract_address: camel_target } - ) -} - -// -// snake_case target -// - -#[test] -fn test_dual_on_erc1155_received() { - let (dispatcher, _) = setup_snake(); - let result = dispatcher - .on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - assert_eq!(result, IERC1155_RECEIVER_ID,); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_on_erc1155_received() { - let dispatcher = setup_non_erc1155_receiver(); - dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_on_erc1155_received_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_receiver_panic(); - dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -fn test_dual_on_erc1155_batch_received() { - let (dispatcher, _) = setup_snake(); - let (token_ids, values) = get_ids_and_values(); - - let result = dispatcher - .on_erc1155_batch_received(OPERATOR(), OWNER(), token_ids, values, EMPTY_DATA()); - assert_eq!(result, IERC1155_RECEIVER_ID); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_on_erc1155_batch_received() { - let dispatcher = setup_non_erc1155_receiver(); - let (token_ids, values) = get_ids_and_values(); - dispatcher.on_erc1155_batch_received(OPERATOR(), OWNER(), token_ids, values, EMPTY_DATA()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_on_erc1155_batch_received_exists_and_panics() { - let (dispatcher, _) = setup_erc1155_receiver_panic(); - let (token_ids, values) = get_ids_and_values(); - dispatcher.on_erc1155_batch_received(OPERATOR(), OWNER(), token_ids, values, EMPTY_DATA()); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_onERC1155Received() { - let (dispatcher, _) = setup_camel(); - let result = dispatcher - .on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - assert_eq!(result, IERC1155_RECEIVER_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_onERC1155Received_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_receiver_panic(); - dispatcher.on_erc1155_received(OPERATOR(), OWNER(), TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_onERC1155BatchReceived() { - let (dispatcher, _) = setup_camel(); - let (token_ids, values) = get_ids_and_values(); - - let result = dispatcher - .on_erc1155_batch_received(OPERATOR(), OWNER(), token_ids, values, EMPTY_DATA()); - assert_eq!(result, IERC1155_RECEIVER_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_onERC1155BatchReceived_exists_and_panics() { - let (_, dispatcher) = setup_erc1155_receiver_panic(); - let (token_ids, values) = get_ids_and_values(); - dispatcher.on_erc1155_batch_received(OPERATOR(), OWNER(), token_ids, values, EMPTY_DATA()); -} - -// -// Helpers -// - -fn get_ids_and_values() -> (Span, Span) { - let token_ids = array![TOKEN_ID, TOKEN_ID].span(); - let values = array![TOKEN_VALUE, TOKEN_VALUE].span(); - (token_ids, values) -} diff --git a/packages/token/src/tests/erc1155/test_erc1155.cairo b/packages/token/src/tests/erc1155/test_erc1155.cairo index 750fdcefd..ffdc620d3 100644 --- a/packages/token/src/tests/erc1155/test_erc1155.cairo +++ b/packages/token/src/tests/erc1155/test_erc1155.cairo @@ -9,7 +9,7 @@ use openzeppelin_test_common::erc1155::{ ERC1155SpyHelpers, get_ids_and_values, get_ids_and_split_values }; use openzeppelin_test_common::erc1155::{ - setup_account, deploy_another_account_at, setup_src5, setup_receiver, setup_camel_receiver + setup_account, deploy_another_account_at, setup_src5, setup_receiver }; use openzeppelin_testing::constants::{ EMPTY_DATA, ZERO, OWNER, RECIPIENT, OPERATOR, OTHER, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE, @@ -158,26 +158,6 @@ fn test_safe_transfer_from_owner_to_receiver() { assert_state_after_transfer_single(owner, recipient, TOKEN_ID); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_owner_to_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_single(owner, recipient, TOKEN_ID); - state.safe_transfer_from(owner, recipient, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - spy - .assert_only_event_transfer_single( - contract_address, owner, owner, recipient, TOKEN_ID, TOKEN_VALUE - ); - - assert_state_after_transfer_single(owner, recipient, TOKEN_ID); -} - #[test] fn test_safeTransferFrom_owner_to_receiver() { let (mut state, owner) = setup(); @@ -197,26 +177,6 @@ fn test_safeTransferFrom_owner_to_receiver() { assert_state_after_transfer_single(owner, recipient, TOKEN_ID); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_owner_to_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_single(owner, recipient, TOKEN_ID); - state.safeTransferFrom(owner, recipient, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA()); - spy - .assert_only_event_transfer_single( - contract_address, owner, owner, recipient, TOKEN_ID, TOKEN_VALUE - ); - - assert_state_after_transfer_single(owner, recipient, TOKEN_ID); -} - #[test] fn test_safe_transfer_from_owner_to_account() { let (mut state, owner) = setup(); @@ -423,27 +383,6 @@ fn test_safe_batch_transfer_from_owner_to_receiver() { assert_state_after_transfer_batch(owner, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_batch_transfer_from_owner_to_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let (token_ids, values) = get_ids_and_values(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_batch(owner, recipient, token_ids, values); - state.safe_batch_transfer_from(owner, recipient, token_ids, values, EMPTY_DATA()); - spy - .assert_only_event_transfer_batch( - contract_address, owner, owner, recipient, token_ids, values - ); - - assert_state_after_transfer_batch(owner, recipient, token_ids, values); -} - #[test] fn test_safeBatchTransferFrom_owner_to_receiver() { let (mut state, owner) = setup(); @@ -464,27 +403,6 @@ fn test_safeBatchTransferFrom_owner_to_receiver() { assert_state_after_transfer_batch(owner, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeBatchTransferFrom_owner_to_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let (token_ids, values) = get_ids_and_values(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_batch(owner, recipient, token_ids, values); - state.safeBatchTransferFrom(owner, recipient, token_ids, values, EMPTY_DATA()); - spy - .assert_only_event_transfer_batch( - contract_address, owner, owner, recipient, token_ids, values - ); - - assert_state_after_transfer_batch(owner, recipient, token_ids, values); -} - #[test] fn test_safe_batch_transfer_from_owner_to_account() { let (mut state, owner) = setup(); @@ -919,28 +837,6 @@ fn test_update_wac_single_from_non_zero_to_non_zero() { assert_state_after_transfer_single(owner, recipient, TOKEN_ID); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_update_wac_single_from_non_zero_to_non_zero_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let token_ids = array![TOKEN_ID].span(); - let values = array![TOKEN_VALUE].span(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_single(owner, recipient, TOKEN_ID); - state.update_with_acceptance_check(owner, recipient, token_ids, values, EMPTY_DATA()); - spy - .assert_only_event_transfer_single( - contract_address, owner, owner, recipient, TOKEN_ID, TOKEN_VALUE - ); - - assert_state_after_transfer_single(owner, recipient, TOKEN_ID); -} - #[test] fn test_update_wac_single_from_non_zero_to_non_zero_account() { let (mut state, owner) = setup(); @@ -983,27 +879,6 @@ fn test_update_wac_batch_from_non_zero_to_non_zero() { assert_state_after_transfer_batch(owner, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_update_wac_batch_from_non_zero_to_non_zero_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let (token_ids, values) = get_ids_and_values(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_batch(owner, recipient, token_ids, values); - state.update_with_acceptance_check(owner, recipient, token_ids, values, EMPTY_DATA()); - spy - .assert_only_event_transfer_batch( - contract_address, owner, owner, recipient, token_ids, values - ); - - assert_state_after_transfer_batch(owner, recipient, token_ids, values); -} - #[test] fn test_update_wac_batch_from_non_zero_to_non_zero_account() { let (mut state, owner) = setup(); @@ -1057,28 +932,6 @@ fn test_update_wac_from_zero_to_non_zero() { assert_state_after_transfer_from_zero_batch(sender, recipient, token_ids, values); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_update_wac_from_zero_to_non_zero_camel_receiver() { - let (mut state, owner) = setup(); - let recipient = setup_camel_receiver(); - let sender = ZERO(); - let (token_ids, values) = get_ids_and_values(); - let mut spy = spy_events(); - let contract_address = test_address(); - - start_cheat_caller_address(contract_address, owner); - - assert_state_before_transfer_from_zero_batch(sender, recipient, token_ids); - state.update_with_acceptance_check(sender, recipient, token_ids, values, EMPTY_DATA()); - spy - .assert_only_event_transfer_batch( - contract_address, owner, sender, recipient, token_ids, values - ); - - assert_state_after_transfer_from_zero_batch(sender, recipient, token_ids, values); -} - #[test] fn test_update_wac_from_zero_to_non_zero_account() { let (mut state, owner) = setup(); diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index 213861a92..4e62efeae 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_dual20.cairo b/packages/token/src/tests/erc20/test_dual20.cairo deleted file mode 100644 index d1b926deb..000000000 --- a/packages/token/src/tests/erc20/test_dual20.cairo +++ /dev/null @@ -1,309 +0,0 @@ -use crate::erc20::dual20::{DualCaseERC20, DualCaseERC20Trait}; -use crate::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; -use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{ - OWNER, RECIPIENT, SPENDER, OPERATOR, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE -}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::{test_address, start_cheat_caller_address}; - -// -// Setup -// - -fn setup_snake() -> (DualCaseERC20, IERC20Dispatcher) { - let mut calldata = array![]; - calldata.append_serde(NAME()); - calldata.append_serde(SYMBOL()); - calldata.append_serde(SUPPLY); - calldata.append_serde(OWNER()); - let target = utils::declare_and_deploy("SnakeERC20Mock", calldata); - (DualCaseERC20 { contract_address: target }, IERC20Dispatcher { contract_address: target }) -} - -fn setup_camel() -> (DualCaseERC20, IERC20CamelDispatcher) { - let mut calldata = array![]; - calldata.append_serde(NAME()); - calldata.append_serde(SYMBOL()); - calldata.append_serde(SUPPLY); - calldata.append_serde(OWNER()); - let target = utils::declare_and_deploy("CamelERC20Mock", calldata); - (DualCaseERC20 { contract_address: target }, IERC20CamelDispatcher { contract_address: target }) -} - -fn setup_non_erc20() -> DualCaseERC20 { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseERC20 { contract_address: target } -} - -fn setup_erc20_panic() -> (DualCaseERC20, DualCaseERC20) { - let snake_target = utils::declare_and_deploy("SnakeERC20Panic", array![]); - let camel_target = utils::declare_and_deploy("CamelERC20Panic", array![]); - ( - DualCaseERC20 { contract_address: snake_target }, - DualCaseERC20 { contract_address: camel_target } - ) -} - -// -// Case agnostic methods -// - -#[test] -fn test_dual_name() { - let (snake_dispatcher, _) = setup_snake(); - assert_eq!(snake_dispatcher.name(), NAME()); - - let (camel_dispatcher, _) = setup_camel(); - assert_eq!(camel_dispatcher.name(), NAME()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_name() { - let dispatcher = setup_non_erc20(); - dispatcher.name(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_name_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.name(); -} - -#[test] -fn test_dual_symbol() { - let (snake_dispatcher, _) = setup_snake(); - let (camel_dispatcher, _) = setup_camel(); - assert_eq!(snake_dispatcher.symbol(), SYMBOL()); - assert_eq!(camel_dispatcher.symbol(), SYMBOL()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_symbol() { - let dispatcher = setup_non_erc20(); - dispatcher.symbol(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_symbol_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.symbol(); -} - -#[test] -fn test_dual_decimals() { - let (snake_dispatcher, _) = setup_snake(); - let (camel_dispatcher, _) = setup_camel(); - assert_eq!(snake_dispatcher.decimals(), DECIMALS); - assert_eq!(camel_dispatcher.decimals(), DECIMALS); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_decimals() { - let dispatcher = setup_non_erc20(); - dispatcher.decimals(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_decimals_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.decimals(); -} - -#[test] -fn test_dual_transfer() { - let (snake_dispatcher, snake_target) = setup_snake(); - - start_cheat_caller_address(snake_dispatcher.contract_address, OWNER()); - assert!(snake_dispatcher.transfer(RECIPIENT(), VALUE)); - assert_eq!(snake_target.balance_of(RECIPIENT()), VALUE); - - let (camel_dispatcher, camel_target) = setup_camel(); - start_cheat_caller_address(camel_dispatcher.contract_address, OWNER()); - assert!(camel_dispatcher.transfer(RECIPIENT(), VALUE)); - assert_eq!(camel_target.balanceOf(RECIPIENT()), VALUE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer() { - let dispatcher = setup_non_erc20(); - dispatcher.transfer(RECIPIENT(), VALUE); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_transfer_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.transfer(RECIPIENT(), VALUE); -} - -#[test] -fn test_dual_approve() { - let (snake_dispatcher, snake_target) = setup_snake(); - start_cheat_caller_address(snake_dispatcher.contract_address, OWNER()); - assert!(snake_dispatcher.approve(SPENDER(), VALUE)); - - let snake_allowance = snake_target.allowance(OWNER(), SPENDER()); - assert_eq!(snake_allowance, VALUE); - - let (camel_dispatcher, camel_target) = setup_camel(); - start_cheat_caller_address(camel_dispatcher.contract_address, OWNER()); - assert!(camel_dispatcher.approve(SPENDER(), VALUE)); - - let camel_allowance = camel_target.allowance(OWNER(), SPENDER()); - assert_eq!(camel_allowance, VALUE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_approve() { - let dispatcher = setup_non_erc20(); - dispatcher.approve(SPENDER(), VALUE); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_approve_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.approve(SPENDER(), VALUE); -} - -// -// snake_case target -// - -#[test] -fn test_dual_total_supply() { - let (dispatcher, _) = setup_snake(); - assert_eq!(dispatcher.total_supply(), SUPPLY); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_total_supply() { - let dispatcher = setup_non_erc20(); - dispatcher.total_supply(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_total_supply_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.total_supply(); -} - -#[test] -fn test_dual_balance_of() { - let (dispatcher, _) = setup_snake(); - assert_eq!(dispatcher.balance_of(OWNER()), SUPPLY); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_balance_of() { - let dispatcher = setup_non_erc20(); - dispatcher.balance_of(OWNER()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_balance_of_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.balance_of(OWNER()); -} - -#[test] -fn test_dual_transfer_from() { - let (dispatcher, target) = setup_snake(); - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.approve(OPERATOR(), VALUE); - - start_cheat_caller_address(dispatcher.contract_address, OPERATOR()); - dispatcher.transfer_from(OWNER(), RECIPIENT(), VALUE); - assert_eq!(target.balance_of(RECIPIENT()), VALUE); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer_from() { - let dispatcher = setup_non_erc20(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), VALUE); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc20_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), VALUE); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_totalSupply() { - let (dispatcher, _) = setup_camel(); - assert_eq!(dispatcher.total_supply(), SUPPLY); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_totalSupply_exists_and_panics() { - let (_, dispatcher) = setup_erc20_panic(); - dispatcher.total_supply(); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_balanceOf() { - let (dispatcher, _) = setup_camel(); - assert_eq!(dispatcher.balance_of(OWNER()), SUPPLY); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_balanceOf_exists_and_panics() { - let (_, dispatcher) = setup_erc20_panic(); - dispatcher.balance_of(OWNER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_transferFrom() { - let (dispatcher, target) = setup_camel(); - start_cheat_caller_address(test_address(), OWNER()); - target.approve(OPERATOR(), VALUE); - - start_cheat_caller_address(test_address(), OPERATOR()); - dispatcher.transfer_from(OWNER(), RECIPIENT(), VALUE); - assert_eq!(target.balanceOf(RECIPIENT()), VALUE); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_transferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc20_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), VALUE); -} diff --git a/packages/token/src/tests/erc721.cairo b/packages/token/src/tests/erc721.cairo index 38933c7cb..d42d6412a 100644 --- a/packages/token/src/tests/erc721.cairo +++ b/packages/token/src/tests/erc721.cairo @@ -1,5 +1,3 @@ -mod test_dual721; -mod test_dual721_receiver; mod test_erc721; mod test_erc721_enumerable; mod test_erc721_receiver; diff --git a/packages/token/src/tests/erc721/test_dual721.cairo b/packages/token/src/tests/erc721/test_dual721.cairo deleted file mode 100644 index 8fabac436..000000000 --- a/packages/token/src/tests/erc721/test_dual721.cairo +++ /dev/null @@ -1,507 +0,0 @@ -use crate::erc721::dual721::{DualCaseERC721, DualCaseERC721Trait}; -use crate::erc721::interface::IERC721_ID; -use crate::erc721::interface::{IERC721CamelOnlyDispatcher, IERC721CamelOnlyDispatcherTrait}; -use crate::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{ - DATA, OWNER, RECIPIENT, SPENDER, OPERATOR, NAME, SYMBOL, BASE_URI, TOKEN_ID -}; -use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::{start_cheat_caller_address}; -use starknet::ContractAddress; - -// -// Setup -// - -fn setup_snake() -> (DualCaseERC721, IERC721Dispatcher) { - let mut calldata = array![]; - calldata.append_serde(NAME()); - calldata.append_serde(SYMBOL()); - calldata.append_serde(BASE_URI()); - calldata.append_serde(OWNER()); - calldata.append_serde(TOKEN_ID); - - let target = utils::declare_and_deploy("SnakeERC721Mock", calldata); - start_cheat_caller_address(target, OWNER()); - (DualCaseERC721 { contract_address: target }, IERC721Dispatcher { contract_address: target }) -} - -fn setup_camel() -> (DualCaseERC721, IERC721CamelOnlyDispatcher) { - let mut calldata = array![]; - calldata.append_serde(NAME()); - calldata.append_serde(SYMBOL()); - calldata.append_serde(BASE_URI()); - calldata.append_serde(OWNER()); - calldata.append_serde(TOKEN_ID); - - let target = utils::declare_and_deploy("CamelERC721Mock", calldata); - start_cheat_caller_address(target, OWNER()); - ( - DualCaseERC721 { contract_address: target }, - IERC721CamelOnlyDispatcher { contract_address: target } - ) -} - -fn setup_non_erc721() -> DualCaseERC721 { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseERC721 { contract_address: target } -} - -fn setup_erc721_panic() -> (DualCaseERC721, DualCaseERC721) { - let snake_target = utils::declare_and_deploy("SnakeERC721PanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelERC721PanicMock", array![]); - ( - DualCaseERC721 { contract_address: snake_target }, - DualCaseERC721 { contract_address: camel_target } - ) -} - -fn setup_receiver() -> ContractAddress { - utils::declare_and_deploy("DualCaseERC721ReceiverMock", array![]) -} - -// -// Case agnostic methods -// - -#[test] -fn test_dual_name() { - let (snake_dispatcher, _) = setup_snake(); - assert_eq!(snake_dispatcher.name(), NAME()); - - let (camel_dispatcher, _) = setup_camel(); - assert_eq!(camel_dispatcher.name(), NAME()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_name() { - let dispatcher = setup_non_erc721(); - dispatcher.name(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_name_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.name(); -} - -#[test] -fn test_dual_symbol() { - let (snake_dispatcher, _) = setup_snake(); - let (camel_dispatcher, _) = setup_camel(); - assert_eq!(snake_dispatcher.symbol(), SYMBOL()); - assert_eq!(camel_dispatcher.symbol(), SYMBOL()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_symbol() { - let dispatcher = setup_non_erc721(); - dispatcher.symbol(); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_symbol_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.symbol(); -} - -#[test] -fn test_dual_approve() { - let (snake_dispatcher, snake_target) = setup_snake(); - - start_cheat_caller_address(snake_dispatcher.contract_address, OWNER()); - snake_dispatcher.approve(SPENDER(), TOKEN_ID); - assert_eq!(snake_target.get_approved(TOKEN_ID), SPENDER()); - - let (camel_dispatcher, camel_target) = setup_camel(); - - start_cheat_caller_address(camel_dispatcher.contract_address, OWNER()); - camel_dispatcher.approve(SPENDER(), TOKEN_ID); - assert_eq!(camel_target.getApproved(TOKEN_ID), SPENDER()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_approve() { - let dispatcher = setup_non_erc721(); - dispatcher.approve(SPENDER(), TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_approve_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.approve(SPENDER(), TOKEN_ID); -} - -// -// snake_case target -// - -#[test] -fn test_dual_balance_of() { - let (dispatcher, _) = setup_snake(); - assert_eq!(dispatcher.balance_of(OWNER()), 1); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_balance_of() { - let dispatcher = setup_non_erc721(); - dispatcher.balance_of(OWNER()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_balance_of_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.balance_of(OWNER()); -} - -#[test] -fn test_dual_owner_of() { - let (dispatcher, _) = setup_snake(); - assert_eq!(dispatcher.owner_of(TOKEN_ID), OWNER()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_owner_of() { - let dispatcher = setup_non_erc721(); - dispatcher.owner_of(TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_owner_of_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.owner_of(TOKEN_ID); -} - -#[test] -fn test_dual_transfer_from() { - let (dispatcher, target) = setup_snake(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); - assert_eq!(target.owner_of(TOKEN_ID), RECIPIENT()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_transfer_from() { - let dispatcher = setup_non_erc721(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); -} - -#[test] -fn test_dual_safe_transfer_from() { - let (dispatcher, target) = setup_snake(); - let receiver = setup_receiver(); - dispatcher.safe_transfer_from(OWNER(), receiver, TOKEN_ID, DATA(true)); - assert_eq!(target.owner_of(TOKEN_ID), receiver); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_safe_transfer_from() { - let dispatcher = setup_non_erc721(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_safe_transfer_from_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); -} - -#[test] -fn test_dual_get_approved() { - let (dispatcher, target) = setup_snake(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.approve(SPENDER(), TOKEN_ID); - assert_eq!(dispatcher.get_approved(TOKEN_ID), SPENDER()); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_get_approved() { - let dispatcher = setup_non_erc721(); - dispatcher.get_approved(TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_get_approved_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.get_approved(TOKEN_ID); -} - -#[test] -fn test_dual_set_approval_for_all() { - let (dispatcher, target) = setup_snake(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = target.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_set_approval_for_all() { - let dispatcher = setup_non_erc721(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_set_approval_for_all_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -#[test] -fn test_dual_is_approved_for_all() { - let (dispatcher, target) = setup_snake(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = dispatcher.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_is_approved_for_all() { - let dispatcher = setup_non_erc721(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_is_approved_for_all_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -fn test_dual_token_uri() { - let (dispatcher, _) = setup_snake(); - let uri = dispatcher.token_uri(TOKEN_ID); - let expected = format!("{}{}", BASE_URI(), TOKEN_ID); - assert_eq!(uri, expected); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_token_uri() { - let dispatcher = setup_non_erc721(); - dispatcher.token_uri(TOKEN_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_token_uri_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.token_uri(TOKEN_ID); -} - -#[test] -fn test_dual_supports_interface() { - let (dispatcher, _) = setup_snake(); - let supports_ierc721 = dispatcher.supports_interface(IERC721_ID); - assert!(supports_ierc721); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_supports_interface() { - let dispatcher = setup_non_erc721(); - dispatcher.supports_interface(IERC721_ID); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_supports_interface_exists_and_panics() { - let (dispatcher, _) = setup_erc721_panic(); - dispatcher.supports_interface(IERC721_ID); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_balanceOf() { - let (dispatcher, _) = setup_camel(); - assert_eq!(dispatcher.balance_of(OWNER()), 1); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_balanceOf_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.balance_of(OWNER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_ownerOf() { - let (dispatcher, _) = setup_camel(); - let current_owner = dispatcher.owner_of(TOKEN_ID); - assert_eq!(current_owner, OWNER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_ownerOf_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.owner_of(TOKEN_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_transferFrom() { - let (dispatcher, target) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); - - let current_owner = target.ownerOf(TOKEN_ID); - assert_eq!(current_owner, RECIPIENT()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_transferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_safeTransferFrom() { - let (dispatcher, target) = setup_camel(); - let receiver = setup_receiver(); - dispatcher.safe_transfer_from(OWNER(), receiver, TOKEN_ID, DATA(true)); - - let current_owner = target.ownerOf(TOKEN_ID); - assert_eq!(current_owner, receiver); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_safeTransferFrom_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_getApproved() { - let (dispatcher, _) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.approve(SPENDER(), TOKEN_ID); - - let approved = dispatcher.get_approved(TOKEN_ID); - assert_eq!(approved, SPENDER()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_getApproved_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.get_approved(TOKEN_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_setApprovalForAll() { - let (dispatcher, target) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - dispatcher.set_approval_for_all(OPERATOR(), true); - - let is_approved_for_all = target.isApprovedForAll(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_setApprovalForAll_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.set_approval_for_all(OPERATOR(), true); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_isApprovedForAll() { - let (dispatcher, target) = setup_camel(); - - start_cheat_caller_address(dispatcher.contract_address, OWNER()); - target.setApprovalForAll(OPERATOR(), true); - - let is_approved_for_all = dispatcher.is_approved_for_all(OWNER(), OPERATOR()); - assert!(is_approved_for_all); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_isApprovedForAll_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.is_approved_for_all(OWNER(), OPERATOR()); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_tokenURI() { - let (dispatcher, _) = setup_camel(); - let uri = dispatcher.token_uri(TOKEN_ID); - let expected = format!("{}{}", BASE_URI(), TOKEN_ID); - assert_eq!(uri, expected); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_tokenURI_exists_and_panics() { - let (_, dispatcher) = setup_erc721_panic(); - dispatcher.token_uri(TOKEN_ID); -} diff --git a/packages/token/src/tests/erc721/test_dual721_receiver.cairo b/packages/token/src/tests/erc721/test_dual721_receiver.cairo deleted file mode 100644 index bab5645c8..000000000 --- a/packages/token/src/tests/erc721/test_dual721_receiver.cairo +++ /dev/null @@ -1,93 +0,0 @@ -use crate::erc721::dual721_receiver::{DualCaseERC721Receiver, DualCaseERC721ReceiverTrait}; -use crate::erc721::interface::{ - IERC721ReceiverDispatcher, IERC721ReceiverCamelDispatcher, IERC721_RECEIVER_ID -}; -use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{DATA, OPERATOR, OWNER, TOKEN_ID}; - -// -// Setup -// - -fn setup_snake() -> (DualCaseERC721Receiver, IERC721ReceiverDispatcher) { - let calldata = array![]; - let target = utils::declare_and_deploy("SnakeERC721ReceiverMock", calldata); - ( - DualCaseERC721Receiver { contract_address: target }, - IERC721ReceiverDispatcher { contract_address: target } - ) -} - -fn setup_camel() -> (DualCaseERC721Receiver, IERC721ReceiverCamelDispatcher) { - let calldata = array![]; - let target = utils::declare_and_deploy("CamelERC721ReceiverMock", calldata); - ( - DualCaseERC721Receiver { contract_address: target }, - IERC721ReceiverCamelDispatcher { contract_address: target } - ) -} - -fn setup_non_erc721_receiver() -> DualCaseERC721Receiver { - let calldata = array![]; - let target = utils::declare_and_deploy("NonImplementingMock", calldata); - DualCaseERC721Receiver { contract_address: target } -} - -fn setup_erc721_receiver_panic() -> (DualCaseERC721Receiver, DualCaseERC721Receiver) { - let snake_target = utils::declare_and_deploy("SnakeERC721ReceiverPanicMock", array![]); - let camel_target = utils::declare_and_deploy("CamelERC721ReceiverPanicMock", array![]); - ( - DualCaseERC721Receiver { contract_address: snake_target }, - DualCaseERC721Receiver { contract_address: camel_target } - ) -} - -// -// snake_case target -// - -#[test] -fn test_dual_on_erc721_received() { - let (dispatcher, _) = setup_snake(); - - let on_erc721_received = dispatcher - .on_erc721_received(OPERATOR(), OWNER(), TOKEN_ID, DATA(true)); - assert_eq!(on_erc721_received, IERC721_RECEIVER_ID); -} - -#[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] -fn test_dual_no_on_erc721_received() { - let dispatcher = setup_non_erc721_receiver(); - dispatcher.on_erc721_received(OPERATOR(), OWNER(), TOKEN_ID, DATA(true)); -} - -#[test] -#[should_panic(expected: "Some error")] -fn test_dual_on_erc721_received_exists_and_panics() { - let (dispatcher, _) = setup_erc721_receiver_panic(); - dispatcher.on_erc721_received(OPERATOR(), OWNER(), TOKEN_ID, DATA(true)); -} - -// -// camelCase target -// - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_dual_onERC721Received() { - let (dispatcher, _) = setup_camel(); - - let on_erc721_received = dispatcher - .on_erc721_received(OPERATOR(), OWNER(), TOKEN_ID, DATA(true)); - assert_eq!(on_erc721_received, IERC721_RECEIVER_ID); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: "Some error")] -fn test_dual_onERC721Received_exists_and_panics() { - let (_, dispatcher) = setup_erc721_receiver_panic(); - dispatcher.on_erc721_received(OPERATOR(), OWNER(), TOKEN_ID, DATA(true)); -} diff --git a/packages/token/src/tests/erc721/test_erc721.cairo b/packages/token/src/tests/erc721/test_erc721.cairo index ebf9c5bd5..d28f9d189 100644 --- a/packages/token/src/tests/erc721/test_erc721.cairo +++ b/packages/token/src/tests/erc721/test_erc721.cairo @@ -37,11 +37,7 @@ fn setup() -> ComponentState { } fn setup_receiver() -> ContractAddress { - utils::declare_and_deploy("SnakeERC721ReceiverMock", array![]) -} - -fn setup_camel_receiver() -> ContractAddress { - utils::declare_and_deploy("CamelERC721ReceiverMock", array![]) + utils::declare_and_deploy("DualCaseERC721ReceiverMock", array![]) } fn setup_account() -> ContractAddress { @@ -49,11 +45,6 @@ fn setup_account() -> ContractAddress { utils::declare_and_deploy("DualCaseAccountMock", calldata) } -fn setup_camel_account() -> ContractAddress { - let calldata = array![PUBKEY]; - utils::declare_and_deploy("CamelAccountMock", calldata) -} - // // Initializers // @@ -595,42 +586,6 @@ fn test_safeTransferFrom_to_account() { assert_state_after_transfer(owner, account, token_id); } -#[test] -fn test_safe_transfer_from_to_account_camel() { - let mut state = setup(); - let contract_address = test_address(); - let account = setup_camel_account(); - let mut spy = spy_events(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, account, token_id); - - start_cheat_caller_address(contract_address, owner); - state.safe_transfer_from(owner, account, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, account, token_id); - - assert_state_after_transfer(owner, account, token_id); -} - -#[test] -fn test_safeTransferFrom_to_account_camel() { - let mut state = setup(); - let contract_address = test_address(); - let account = setup_camel_account(); - let mut spy = spy_events(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, account, token_id); - - start_cheat_caller_address(contract_address, owner); - state.safeTransferFrom(owner, account, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, account, token_id); - - assert_state_after_transfer(owner, account, token_id); -} - #[test] fn test_safe_transfer_from_to_receiver() { let mut state = setup(); @@ -667,44 +622,6 @@ fn test_safeTransferFrom_to_receiver() { assert_state_after_transfer(owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_to_receiver_camel() { - let mut state = setup(); - let receiver = setup_camel_receiver(); - let contract_address = test_address(); - let mut spy = spy_events(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_to_receiver_camel() { - let mut state = setup(); - let receiver = setup_camel_receiver(); - let mut spy = spy_events(); - let contract_address = test_address(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - #[test] #[should_panic(expected: ('ERC721: safe transfer failed',))] fn test_safe_transfer_from_to_receiver_failure() { @@ -729,32 +646,6 @@ fn test_safeTransferFrom_to_receiver_failure() { state.safeTransferFrom(owner, receiver, token_id, DATA(false)); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: ('ERC721: safe transfer failed',))] -fn test_safe_transfer_from_to_receiver_failure_camel() { - let mut state = setup(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - start_cheat_caller_address(test_address(), owner); - state.safe_transfer_from(owner, receiver, token_id, DATA(false)); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: ('ERC721: safe transfer failed',))] -fn test_safeTransferFrom_to_receiver_failure_camel() { - let mut state = setup(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - start_cheat_caller_address(test_address(), owner); - state.safeTransferFrom(owner, receiver, token_id, DATA(false)); -} - #[test] #[ignore] // REASON: should_panic attribute not fit for complex panic messages. #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] @@ -855,54 +746,6 @@ fn test_safeTransferFrom_to_owner() { assert_eq!(state.balance_of(owner), 1); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_to_owner_camel() { - let mut state = COMPONENT_STATE(); - let token_id = TOKEN_ID; - let owner = setup_camel_receiver(); - let contract_address = test_address(); - - state.initializer(NAME(), SYMBOL(), BASE_URI()); - state.mint(owner, token_id); - - let mut spy = spy_events(); - - assert_eq!(state.owner_of(token_id), owner); - assert_eq!(state.balance_of(owner), 1); - - start_cheat_caller_address(contract_address, owner); - state.safe_transfer_from(owner, owner, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, owner, token_id); - - assert_eq!(state.owner_of(token_id), owner); - assert_eq!(state.balance_of(owner), 1); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_to_owner_camel() { - let mut state = COMPONENT_STATE(); - let contract_address = test_address(); - let token_id = TOKEN_ID; - let owner = setup_camel_receiver(); - - state.initializer(NAME(), SYMBOL(), BASE_URI()); - state.mint(owner, token_id); - - assert_eq!(state.owner_of(token_id), owner); - assert_eq!(state.balance_of(owner), 1); - - let mut spy = spy_events(); - - start_cheat_caller_address(contract_address, owner); - state.safeTransferFrom(owner, owner, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, owner, token_id); - - assert_eq!(state.owner_of(token_id), owner); - assert_eq!(state.balance_of(owner), 1); -} - #[test] fn test_safe_transfer_from_approved() { let mut state = setup(); @@ -947,49 +790,6 @@ fn test_safeTransferFrom_approved() { assert_state_after_transfer(owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_approved_camel() { - let mut state = setup(); - let contract_address = test_address(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.approve(OPERATOR(), token_id); - - let mut spy = spy_events(); - - start_cheat_caller_address(contract_address, OPERATOR()); - state.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_approved_camel() { - let mut state = setup(); - let contract_address = test_address(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - let mut spy = spy_events(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.approve(OPERATOR(), token_id); - state.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - #[test] fn test_safe_transfer_from_approved_for_all() { let mut state = setup(); @@ -1034,48 +834,6 @@ fn test_safeTransferFrom_approved_for_all() { assert_state_after_transfer(owner, receiver, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safe_transfer_from_approved_for_all_camel() { - let mut state = setup(); - let contract_address = test_address(); - let mut spy = spy_events(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.set_approval_for_all(OPERATOR(), true); - - start_cheat_caller_address(contract_address, OPERATOR()); - state.safe_transfer_from(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test_safeTransferFrom_approved_for_all_camel() { - let mut state = setup(); - let contract_address = test_address(); - let receiver = setup_camel_receiver(); - let token_id = TOKEN_ID; - let owner = OWNER(); - let mut spy = spy_events(); - - assert_state_before_transfer(owner, receiver, token_id); - - start_cheat_caller_address(contract_address, owner); - state.set_approval_for_all(OPERATOR(), true); - state.safeTransferFrom(owner, receiver, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, owner, receiver, token_id); - - assert_state_after_transfer(owner, receiver, token_id); -} - #[test] #[should_panic(expected: ('ERC721: unauthorized caller',))] fn test_safe_transfer_from_unauthorized() { @@ -1186,22 +944,6 @@ fn test__safe_mint_to_receiver() { assert_state_after_mint(recipient, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -fn test__safe_mint_to_receiver_camel() { - let mut state = COMPONENT_STATE(); - let contract_address = test_address(); - let mut spy = spy_events(); - let recipient = setup_camel_receiver(); - let token_id = TOKEN_ID; - - assert_state_before_mint(recipient); - state.safe_mint(recipient, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, ZERO(), recipient, token_id); - - assert_state_after_mint(recipient, token_id); -} - #[test] fn test__safe_mint_to_account() { let mut state = COMPONENT_STATE(); @@ -1217,21 +959,6 @@ fn test__safe_mint_to_account() { assert_state_after_mint(account, token_id); } -#[test] -fn test__safe_mint_to_account_camel() { - let mut state = COMPONENT_STATE(); - let contract_address = test_address(); - let account = setup_camel_account(); - let mut spy = spy_events(); - let token_id = TOKEN_ID; - - assert_state_before_mint(account); - state.safe_mint(account, token_id, DATA(true)); - spy.assert_only_event_transfer(contract_address, ZERO(), account, token_id); - - assert_state_after_mint(account, token_id); -} - #[test] #[ignore] // REASON: should_panic attribute not fit for complex panic messages. #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] @@ -1257,19 +984,6 @@ fn test__safe_mint_to_receiver_failure() { assert_state_after_mint(recipient, token_id); } -#[test] -#[ignore] // REASON: foundry entrypoint_not_found error message inconsistent with mainnet. -#[should_panic(expected: ('ERC721: safe mint failed',))] -fn test__safe_mint_to_receiver_failure_camel() { - let mut state = COMPONENT_STATE(); - let recipient = setup_camel_receiver(); - let token_id = TOKEN_ID; - - assert_state_before_mint(recipient); - state.safe_mint(recipient, token_id, DATA(false)); - assert_state_after_mint(recipient, token_id); -} - #[test] #[should_panic(expected: ('ERC721: invalid receiver',))] fn test__safe_mint_to_zero() { diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 9574a4b7a..35ad0586e 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -7,5 +7,4 @@ pub(crate) mod erc2981_mocks; pub(crate) mod erc721_enumerable_mocks; pub(crate) mod erc721_mocks; pub(crate) mod erc721_receiver_mocks; -pub(crate) mod non_implementing_mock; pub(crate) mod src5_mocks; diff --git a/packages/token/src/tests/mocks/account_mocks.cairo b/packages/token/src/tests/mocks/account_mocks.cairo index f8f222dad..e09f53328 100644 --- a/packages/token/src/tests/mocks/account_mocks.cairo +++ b/packages/token/src/tests/mocks/account_mocks.cairo @@ -43,103 +43,3 @@ pub(crate) mod DualCaseAccountMock { 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/token/src/tests/mocks/erc1155_mocks.cairo b/packages/token/src/tests/mocks/erc1155_mocks.cairo index cbf968c76..a7442f8c0 100644 --- a/packages/token/src/tests/mocks/erc1155_mocks.cairo +++ b/packages/token/src/tests/mocks/erc1155_mocks.cairo @@ -50,257 +50,3 @@ pub(crate) mod DualCaseERC1155Mock { self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); } } - -#[starknet::contract] -pub(crate) mod SnakeERC1155Mock { - use crate::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC1155 - #[abi(embed_v0)] - impl ERC1155Impl = ERC1155Component::ERC1155Impl; - #[abi(embed_v0)] - impl ERC1155MetadataURIImpl = - ERC1155Component::ERC1155MetadataURIImpl; - impl ERC1155InternalImpl = ERC1155Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155: ERC1155Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155Event: ERC1155Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256, - value: u256 - ) { - self.erc1155.initializer(base_uri); - self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); - } -} - -#[starknet::contract] -pub(crate) mod CamelERC1155Mock { - use crate::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC1155 - #[abi(embed_v0)] - impl ERC1155Camel = ERC1155Component::ERC1155CamelImpl; - #[abi(embed_v0)] - impl ERC1155MetadataURIImpl = - ERC1155Component::ERC1155MetadataURIImpl; - impl ERC1155InternalImpl = ERC1155Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155: ERC1155Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155Event: ERC1155Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256, - value: u256 - ) { - self.erc1155.initializer(base_uri); - self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC1155PanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn uri(self: @ContractState, token_id: u256) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn balance_of(self: @ContractState, account: ContractAddress, token_id: u256) -> u256 { - panic!("Some error"); - u256 { low: 3, high: 3 } - } - - #[external(v0)] - fn balance_of_batch( - self: @ContractState, accounts: Span, token_ids: Span - ) -> Span { - panic!("Some error"); - array![u256 { low: 3, high: 3 }].span() - } - - #[external(v0)] - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn safe_batch_transfer_from( - ref self: ContractState, - from: starknet::ContractAddress, - to: starknet::ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool - ) { - panic!("Some error"); - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC1155PanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn uri(self: @ContractState, tokenId: u256) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn balanceOf(self: @ContractState, account: ContractAddress, tokenId: u256) -> u256 { - panic!("Some error"); - u256 { low: 3, high: 3 } - } - - #[external(v0)] - fn balanceOfBatch( - self: @ContractState, accounts: Span, token_ids: Span - ) -> Span { - panic!("Some error"); - array![u256 { low: 3, high: 3 }].span() - } - - #[external(v0)] - fn safeTransferFrom( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - tokenId: u256, - value: u256, - data: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn safeBatchTransferFrom( - ref self: ContractState, - from: starknet::ContractAddress, - to: starknet::ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) { - panic!("Some error"); - } - - #[external(v0)] - fn isApprovedForAll( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { - panic!("Some error"); - } - - #[external(v0)] - fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { - panic!("Some error"); - false - } - } -} diff --git a/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo b/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo index d5018e877..06443f5f5 100644 --- a/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo +++ b/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo @@ -38,157 +38,3 @@ pub(crate) mod DualCaseERC1155ReceiverMock { self.erc1155_receiver.initializer(); } } - -#[starknet::contract] -pub(crate) mod SnakeERC1155ReceiverMock { - use crate::erc1155::ERC1155ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - component!( - path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent - ); - - // ERC1155Receiver - #[abi(embed_v0)] - impl ERC1155ReceiverImpl = - ERC1155ReceiverComponent::ERC1155ReceiverImpl; - impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155_receiver: ERC1155ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc1155_receiver.initializer(); - } -} - -#[starknet::contract] -pub(crate) mod CamelERC1155ReceiverMock { - use crate::erc1155::ERC1155ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - component!( - path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent - ); - - // ERC1155Receiver - #[abi(embed_v0)] - impl ERC1155ReceiverCamelImpl = - ERC1155ReceiverComponent::ERC1155ReceiverCamelImpl; - impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155_receiver: ERC1155ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc1155_receiver.initializer(); - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC1155ReceiverPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[external(v0)] - fn on_erc1155_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn on_erc1155_batch_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenIds: Span, - values: Span, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } -} - -#[starknet::contract] -pub(crate) mod CamelERC1155ReceiverPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[external(v0)] - fn onERC1155Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - value: u256, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn onERC1155BatchReceived( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenIds: Span, - values: Span, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } -} diff --git a/packages/token/src/tests/mocks/erc20_mocks.cairo b/packages/token/src/tests/mocks/erc20_mocks.cairo index bd53190b8..d6b0a799b 100644 --- a/packages/token/src/tests/mocks/erc20_mocks.cairo +++ b/packages/token/src/tests/mocks/erc20_mocks.cairo @@ -38,216 +38,3 @@ pub(crate) mod DualCaseERC20Mock { self.erc20.mint(recipient, initial_supply); } } - -#[starknet::contract] -pub(crate) mod SnakeERC20Mock { - use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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); - } -} - -#[starknet::contract] -pub(crate) mod CamelERC20Mock { - use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - #[abi(embed_v0)] - impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; - - // `ERC20Impl` is not embedded because it would defeat the purpose of the - // mock. The `ERC20Impl` case-agnostic methods are manually exposed. - impl ERC20Impl = ERC20Component::ERC20Impl; - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> u256 { - self.erc20.allowance(owner, spender) - } - - #[external(v0)] - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { - self.erc20.transfer(recipient, amount) - } - - #[external(v0)] - fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { - self.erc20.approve(spender, amount) - } - } -} - -/// Although these modules are designed to panic, functions -/// still need a valid return value. We chose: -/// -/// 3 for felt252, u8, and u256 -/// zero for ContractAddress -/// false for bool -#[starknet::contract] -pub(crate) mod SnakeERC20Panic { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn name(self: @ContractState) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn symbol(self: @ContractState) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn decimals(self: @ContractState) -> u8 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn total_supply(self: @ContractState) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256 - ) -> bool { - panic!("Some error"); - false - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC20Panic { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn totalSupply(self: @ContractState) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn transferFrom( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) { - panic!("Some error"); - } - } -} diff --git a/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo b/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo index 117bf0237..096dea067 100644 --- a/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo +++ b/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo @@ -73,77 +73,3 @@ pub(crate) mod ERC721EnumerableMock { self.erc721.mint(recipient, token_id); } } - -#[starknet::contract] -pub(crate) mod SnakeERC721EnumerableMock { - use crate::erc721::ERC721Component; - use crate::erc721::extensions::ERC721EnumerableComponent; - use openzeppelin_introspection::src5::SRC5Component; - - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!( - path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent - ); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // ERC721Enumerable - #[abi(embed_v0)] - impl ERC721EnumerableImpl = - ERC721EnumerableComponent::ERC721EnumerableImpl; - impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; - - // SRC5 - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub erc721_enumerable: ERC721EnumerableComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - ERC721EnumerableEvent: ERC721EnumerableComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait { - fn before_update( - ref self: ERC721Component::ComponentState, - to: ContractAddress, - token_id: u256, - auth: ContractAddress - ) { - let mut contract_state = ERC721Component::HasComponent::get_contract_mut(ref self); - contract_state.erc721_enumerable.before_update(to, token_id); - } - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721_enumerable.initializer(); - self.erc721.mint(recipient, token_id); - } -} diff --git a/packages/token/src/tests/mocks/erc721_mocks.cairo b/packages/token/src/tests/mocks/erc721_mocks.cairo index 0d44f6944..432c72da9 100644 --- a/packages/token/src/tests/mocks/erc721_mocks.cairo +++ b/packages/token/src/tests/mocks/erc721_mocks.cairo @@ -53,295 +53,3 @@ pub(crate) mod DualCaseERC721Mock { self.erc721.mint(recipient, token_id); } } - -#[starknet::contract] -pub(crate) mod SnakeERC721Mock { - use crate::erc721::{ERC721Component, ERC721HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - #[abi(embed_v0)] - impl ERC721Impl = ERC721Component::ERC721Impl; - #[abi(embed_v0)] - impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721.mint(recipient, token_id); - } -} - -#[starknet::contract] -pub(crate) mod CamelERC721Mock { - use crate::erc721::ERC721Component::{ERC721Impl, ERC721MetadataImpl}; - use crate::erc721::{ERC721Component, ERC721HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - #[abi(embed_v0)] - impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; - #[abi(embed_v0)] - impl ERC721MetadataCamelOnly = - ERC721Component::ERC721MetadataCamelOnlyImpl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721.mint(recipient, token_id); - } - - /// The following external methods are included because they are case-agnostic - /// and this contract should not embed the snake_case impl. - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn approve(ref self: ContractState, to: ContractAddress, tokenId: u256) { - self.erc721.approve(to, tokenId); - } - - #[external(v0)] - fn name(self: @ContractState) -> ByteArray { - self.erc721.name() - } - - #[external(v0)] - fn symbol(self: @ContractState) -> ByteArray { - self.erc721.symbol() - } - } -} - -/// Although these modules are designed to panic, functions -/// still need a valid return value. We chose: -/// -/// 3 for felt252 -/// zero for ContractAddress -/// u256 { 3, 3 } for u256 -#[starknet::contract] -pub(crate) mod SnakeERC721PanicMock { - use core::num::traits::Zero; - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn name(self: @ContractState) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn symbol(self: @ContractState) -> ByteArray { - panic!("Some error"); - "3" - } - - #[external(v0)] - fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { - panic!("Some error"); - } - - #[external(v0)] - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn token_uri(self: @ContractState, token_id: u256) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - panic!("Some error"); - u256 { low: 3, high: 3 } - } - - #[external(v0)] - fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool - ) { - panic!("Some error"); - } - - #[external(v0)] - fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 - ) { - panic!("Some error"); - } - - #[external(v0)] - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ) { - panic!("Some error"); - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC721PanicMock { - use core::num::traits::Zero; - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn tokenURI(self: @ContractState, tokenId: u256) -> felt252 { - panic!("Some error"); - 3 - } - - #[external(v0)] - fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { - panic!("Some error"); - u256 { low: 3, high: 3 } - } - - #[external(v0)] - fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { - panic!("Some error"); - Zero::zero() - } - - #[external(v0)] - fn isApprovedForAll( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - panic!("Some error"); - false - } - - #[external(v0)] - fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { - panic!("Some error"); - } - - #[external(v0)] - fn transferFrom( - ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256 - ) { - panic!("Some error"); - } - - #[external(v0)] - fn safeTransferFrom( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - tokenId: u256, - data: Span - ) { - panic!("Some error"); - } - } -} diff --git a/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo b/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo index c65796186..6df306fbd 100644 --- a/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo +++ b/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo @@ -69,169 +69,3 @@ pub(crate) mod DualCaseERC721ReceiverMock { } } } - -#[starknet::contract] -pub(crate) mod SnakeERC721ReceiverMock { - use crate::erc721::ERC721ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721Receiver - impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc721_receiver.initializer(); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - if *data.at(0) == super::SUCCESS { - self.erc721_receiver.on_erc721_received(operator, from, token_id, data) - } else { - 0 - } - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC721ReceiverMock { - use crate::erc721::ERC721ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721Receiver - impl ERC721ReceiverCamelImpl = ERC721ReceiverComponent::ERC721ReceiverCamelImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc721_receiver.initializer(); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252 { - if *data.at(0) == super::SUCCESS { - self.erc721_receiver.onERC721Received(operator, from, tokenId, data) - } else { - 0 - } - } - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC721ReceiverPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } - } -} - -#[starknet::contract] -pub(crate) mod CamelERC721ReceiverPanicMock { - use starknet::ContractAddress; - - #[storage] - pub struct Storage {} - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252 { - panic!("Some error"); - 3 - } - } -} diff --git a/packages/token/src/tests/mocks/non_implementing_mock.cairo b/packages/token/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/token/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} From e73810787975eef1d489f332ae82c03584ca101a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 20:06:25 -0400 Subject: [PATCH 09/76] feat: update docs --- docs/modules/ROOT/pages/guides/snip12.adoc | 8 +-- docs/modules/ROOT/pages/index.adoc | 2 +- docs/modules/ROOT/pages/interfaces.adoc | 78 +++++----------------- 3 files changed, 22 insertions(+), 66 deletions(-) diff --git a/docs/modules/ROOT/pages/guides/snip12.adoc b/docs/modules/ROOT/pages/guides/snip12.adoc index c9e51790c..6e2316067 100644 --- a/docs/modules/ROOT/pages/guides/snip12.adoc +++ b/docs/modules/ROOT/pages/guides/snip12.adoc @@ -241,12 +241,12 @@ TIP: The expected parameter for the `get_message_hash` function is the address o == Full Implementation -:dualcase_dispatchers: xref:/interfaces#dualcase_dispatchers +:isrc6_dispatcher: xref:/api/account#ISRC6 :nonces: xref:/api/utilities#NoncesComponent Finally, the full implementation of the `CustomERC20` contract looks like this: -NOTE: We are using the {dualcase_dispatchers}[`DualCaseAccount`] to verify the signature, +NOTE: We are using the {isrc6_dispatcher}[`ISRC6Dispatcher`] to verify the signature, and the {nonces}[`NoncesComponent`] to handle nonces to prevent replay attacks. [,cairo] @@ -278,7 +278,7 @@ impl StructHashImpl of StructHash { #[starknet::contract] mod CustomERC20 { - use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait}; + use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use starknet::ContractAddress; @@ -348,7 +348,7 @@ mod CustomERC20 { let message = Message { recipient, amount, nonce, expiry }; let hash = message.get_message_hash(owner); - let is_valid_signature_felt = DualCaseAccount { contract_address: owner } + let is_valid_signature_felt = ISRC6Dispatcher { contract_address: owner } .is_valid_signature(hash, signature); // Check either 'VALID' or true for backwards compatibility diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 9e6f5ecbb..59e49dd23 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -21,7 +21,7 @@ before proceeding, and run the following command to check that the installation ---- $ scarb --version -scarb 2.8.2 (a37b4cbfc 2024-09-09) +scarb 2.8.3 (54938ce3b 2024-09-26) cairo: 2.8.2 (https://crates.io/crates/cairo-lang-compiler/2.8.2) sierra: 1.6.0 ---- diff --git a/docs/modules/ROOT/pages/interfaces.adoc b/docs/modules/ROOT/pages/interfaces.adoc index c01486d9f..e2f219755 100644 --- a/docs/modules/ROOT/pages/interfaces.adoc +++ b/docs/modules/ROOT/pages/interfaces.adoc @@ -13,12 +13,13 @@ use openzeppelin_token::erc20::interface::IERC20; or ```cairo -use openzeppelin_token::erc20::dual20::DualCaseERC20; +use openzeppelin_account::interface::ISRC6; ``` NOTE: For simplicity, we'll use ERC20 as example but the same concepts apply to other modules. == Interface traits + The library offers three types of traits to implement or interact with contracts: === Standard traits @@ -44,6 +45,9 @@ pub trait IERC20 { They describe a contract's complete interface. This is useful to interface with a preset contract offered by this library, such as the ERC20 preset that includes functions from different traits such as `IERC20` and `IERC20Camel`. +NOTE: The library offers an ABI trait per component as well, exposing all the external functions +even when most of the time all of them don't need to be implemented at the same time. This can be helpful when interacting with a contract implementing the component, instead of defining a new dispatcher. + ```cairo #[starknet::interface] pub trait ERC20ABI { @@ -72,34 +76,25 @@ pub trait ERC20ABI { ``` === Dispatcher traits -This is a utility trait to interface with contracts whose interface is unknown. Read more in the xref:#dualcase_dispatchers[DualCase Dispatchers] section. -```cairo -#[derive(Copy, Drop)] -pub struct DualCaseERC20 { - contract_address: ContractAddress -} +Traits decorated with `#[starknet::interface]` automatically generate a dispatcher that can be used to interact with contracts that implement the given interface. They can be imported by appending the `Dispatcher` and `DispatcherTrait` suffixes to the trait name, like this: -pub trait DualCaseERC20Trait { - fn name(self: @DualCaseERC20) -> ByteArray; - fn symbol(self: @DualCaseERC20) -> ByteArray; - fn decimals(self: @DualCaseERC20) -> u8; - fn total_supply(self: @DualCaseERC20) -> u256; - fn balance_of(self: @DualCaseERC20, account: ContractAddress) -> u256; - fn allowance(self: @DualCaseERC20, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(self: @DualCaseERC20, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - self: @DualCaseERC20, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(self: @DualCaseERC20, spender: ContractAddress, amount: u256) -> bool; -} +```cairo +use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; ``` +NOTE: In the example, the `IERC20Dispatcher` is the one used to interact with contracts, but the +`IERC20DispatcherTrait` needs to be in scope for the functions to be available. + == Dual interfaces +WARNING: `camelCase` functions are deprecated and maintained only for Backwards Compatibilty. We recommend to +integrate you contracts and components with the `snake_case` interfaces. The `camelCase` functions will be removed in +future versions. + Following the {great-interface-migration} plan, we added `snake_case` functions to all of our preexisting `camelCase` contracts with the goal of eventually dropping support for the latter. -In short, we offer two types of interfaces and utilities to handle them: +In short, the library offers two types of interfaces and utilities to handle them: 1. `camelCase` interfaces, which are the ones we've been using so far. 2. `snake_case` interfaces, which are the ones we're migrating to. @@ -131,7 +126,7 @@ pub trait IERC20 { === `IERC20Camel` -On top of that, we also offer a `camelCase` version of the same interface: +On top of that, the library also offers a `camelCase` version of the same interface: ```cairo #[starknet::interface] @@ -149,42 +144,3 @@ pub trait IERC20Camel { fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; } ``` - -== `DualCase` dispatchers - -WARNING: `DualCase` dispatchers are deprecated, and they will be removed from the library soon. - -WARNING: `DualCase` dispatchers won't work on live chains (`mainnet` or testnets) until they implement panic handling in their runtime. Dispatchers work fine in testing environments. - -In order to ease this transition, OpenZeppelin Contracts for Cairo offer what we call `DualCase` dispatchers such as `DualCaseERC721` or `DualCaseAccount`. - -These modules wrap a target contract with a compatibility layer to expose a `snake_case` interface no matter what casing the underlying contract uses. -This way, an AMM wouldn't have problems integrating tokens independently of their interface. - -For example: - -```cairo -let token = DualCaseERC20 { contract_address: target }; -token.transfer_from(OWNER(), RECIPIENT(), VALUE); -``` - -This is done by simply executing the `snake_case` version of the function (e.g. `transfer_from`) and falling back to the `camelCase` one (e.g. `transferFrom`) in case it reverts with `ENTRYPOINT_NOT_FOUND`, like this: - -```cairo -fn try_selector_with_fallback( - target: ContractAddress, selector: felt252, fallback: felt252, args: Span -) -> SyscallResult> { - match call_contract_syscall(target, selector, args) { - Result::Ok(ret) => Result::Ok(ret), - Result::Err(errors) => { - if *errors.at(0) == 'ENTRYPOINT_NOT_FOUND' { - return call_contract_syscall(target, fallback, args); - } else { - Result::Err(errors) - } - } - } -} -``` - -Trying the `snake_case` interface first renders `camelCase` calls a bit more expensive since a failed `snake_case` call will always happen before. This is a design choice to incentivize casing adoption/transition as per the {great-interface-migration}. From 75974fd3dce00b5761bcdae3231833d38850599d Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 20:30:55 -0400 Subject: [PATCH 10/76] feat: update CHANGELOG --- CHANGELOG.md | 19 +++++++ docs/modules/ROOT/pages/api/utilities.adoc | 65 ---------------------- packages/utils/src/lib.cairo | 16 ------ packages/utils/src/unwrap_and_cast.cairo | 16 ------ 4 files changed, 19 insertions(+), 97 deletions(-) delete mode 100644 packages/utils/src/unwrap_and_cast.cairo diff --git a/CHANGELOG.md b/CHANGELOG.md index 0312236cf..28760df99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump scarb to v2.8.3 (#1166) +### Changed (Breaking) + +- Remove `DualCase dispatchers` + - Remove `try_selector_with_fallback` from `openzeppelin_utils` + - Remove `unwrap_and_cast` module from `openzeppelin_utils` + - Remove `openzeppelin_access::accesscontrol::dual_accesscontrol` + - Remove `openzeppelin_access::ownable::dual_ownable` + - Remove `openzeppelin_account::dual_account` + - Remove `openzeppelin_account::dual_eth_account` + - Remove `openzeppelin_token::erc20::dual20` + - Remove `openzeppelin_token::erc721::dual721` + - Remove `openzeppelin_token::erc721::dual721_receiver` + - Remove `openzeppelin_token::erc1155::dual1155` + - Remove `openzeppelin_token::erc1155::dual1155_receiver` +- `SRC9Component` now uses `ISRC6Dispatcher` instead of `DualCaseAccount` +- `ERC20VotesComponent` now uses `ISRC6Dispatcher` instead of `DualCaseAccount` +- `ERC721Component` now uses `IERC721ReceiverDispatcher` instead of `DualCaseERC721Receiver` +- `ERC1155Component` now uses `IERC1155ReceiverDispatcher` instead of `DualCaseERC1155Receiver` + ## 0.17.0 (2024-09-23) ### Added diff --git a/docs/modules/ROOT/pages/api/utilities.adoc b/docs/modules/ROOT/pages/api/utilities.adoc index 32a69935f..3b912794c 100644 --- a/docs/modules/ROOT/pages/api/utilities.adoc +++ b/docs/modules/ROOT/pages/api/utilities.adoc @@ -19,12 +19,6 @@ Module containing core utilities of the library. [.contract-index] .Members -- -.Functions -* xref:#utils-try_selector_with_fallback[`++try_selector_with_fallback(target, selector, fallback, args)++`] - -.Traits -* xref:#utils-UnwrapAndCast[`++UnwrapAndCast++`] - .Inner modules * xref:#utils-cryptography[`++cryptography++`] * xref:#utils-deployments[`++deployments++`] @@ -33,65 +27,6 @@ Module containing core utilities of the library. * xref:#utils-serde[`++serde++`] -- -[#utils-Functions] -==== Functions - -[.contract-item] -[[utils-try_selector_with_fallback]] -==== `[.contract-item-name]#++try_selector_with_fallback++#++(target: ContractAddress, selector: felt252, fallback: felt252, args: Span) → SyscallResult>++` [.item-kind]#function# - -Tries to call a given selector on a given contract, and if it fails, tries to call a fallback selector. - -It was designed for falling back to the `camelCase` selector for backward compatibility in the -case of a failure of the `snake_case` selector. - -Returns a `SyscallResult` with the result of the successful call. - -Note that: - -- If the first call succeeds, the second call is not attempted. - -- If the first call fails with an error different than `ENTRYPOINT_NOT_FOUND`, the error is returned -without falling back to the second selector. - -- If the first call fails with `ENTRYPOINT_NOT_FOUND`, the second call is attempted, and if it fails its -error is returned. - -WARNING: The fallback mechanism won't work on live chains (mainnet or testnets) until -they implement panic handling in their runtime. - -[#utils-Traits] -==== Traits - -[.contract-item] -[[utils-UnwrapAndCast]] -==== `[.contract-item-name]#++UnwrapAndCast++#` [.item-kind]#trait# - -Trait for exposing an `unwrap_and_cast` function to `SyscallResult` objects. This may be useful -when unwrapping a syscall result to a type implementing the `Serde` trait, and you want to avoid the boilerplate of -casting and unwrapping the result multiple times. - -Usage example: - -```cairo -use openzeppelin_utils::selectors; -use openzeppelin_utils::UnwrapAndCast; - -fn call_and_cast_to_bool(target: ContractAddress, args: Span) -> bool { - try_selector_with_fallback( - target, selectors::has_role, selectors::hasRole, args - ).unwrap_and_cast() -} - -fn call_and_cast_to_felt252(target: ContractAddress, args: Span) -> felt252 { - try_selector_with_fallback( - target, selectors::get_role_admin, selectors::getRoleAdmin, args - ).unwrap_and_cast() -} -``` - -Note that it can be automatically casted to any type implementing the `Serde` trait. - [#utils-Inner-Modules] ==== Inner modules diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index 074486181..a4b7e5370 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -15,22 +15,6 @@ mod tests; pub mod unwrap_and_cast; pub use cryptography::{nonces, snip12}; -pub use unwrap_and_cast::UnwrapAndCast; use starknet::syscalls::call_contract_syscall; use starknet::{ContractAddress, SyscallResult}; - -pub fn try_selector_with_fallback( - target: ContractAddress, selector: felt252, fallback: felt252, args: Span -) -> SyscallResult> { - match call_contract_syscall(target, selector, args) { - Result::Ok(ret) => Result::Ok(ret), - Result::Err(errors) => { - if *errors.at(0) == 'ENTRYPOINT_NOT_FOUND' { - return call_contract_syscall(target, fallback, args); - } else { - Result::Err(errors) - } - } - } -} diff --git a/packages/utils/src/unwrap_and_cast.cairo b/packages/utils/src/unwrap_and_cast.cairo deleted file mode 100644 index a21d36004..000000000 --- a/packages/utils/src/unwrap_and_cast.cairo +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (utils/unwrap_and_cast.cairo) - -use starknet::SyscallResult; -use starknet::SyscallResultTrait; - -pub trait UnwrapAndCast { - fn unwrap_and_cast(self: SyscallResult>) -> T; -} - -impl UnwrapAndCastSerde> of UnwrapAndCast { - fn unwrap_and_cast(self: SyscallResult>) -> T { - let mut result = self.unwrap_syscall(); - Serde::::deserialize(ref result).unwrap() - } -} From 586357ab3cf64676195d7330d82dee927a98150c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 20:32:52 -0400 Subject: [PATCH 11/76] fix: typo --- docs/modules/ROOT/pages/interfaces.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/interfaces.adoc b/docs/modules/ROOT/pages/interfaces.adoc index e2f219755..53e4d125f 100644 --- a/docs/modules/ROOT/pages/interfaces.adoc +++ b/docs/modules/ROOT/pages/interfaces.adoc @@ -88,7 +88,7 @@ NOTE: In the example, the `IERC20Dispatcher` is the one used to interact with co == Dual interfaces -WARNING: `camelCase` functions are deprecated and maintained only for Backwards Compatibilty. We recommend to +WARNING: `camelCase` functions are deprecated and maintained only for Backwards Compatibility. We recommend to integrate you contracts and components with the `snake_case` interfaces. The `camelCase` functions will be removed in future versions. From b2ba912a2c0047a151973a62eb94f6af4eb453d8 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 3 Oct 2024 20:35:13 -0400 Subject: [PATCH 12/76] fix: mod --- packages/utils/src/lib.cairo | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index a4b7e5370..b1bdae976 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -12,8 +12,6 @@ pub mod structs; #[cfg(test)] mod tests; -pub mod unwrap_and_cast; - pub use cryptography::{nonces, snip12}; use starknet::syscalls::call_contract_syscall; From fe6d84666f8c65539e53fa51ca4c13668ad28807 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 4 Oct 2024 15:08:35 -0400 Subject: [PATCH 13/76] feat: remove unused imports --- packages/utils/src/lib.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index b1bdae976..73bdd5bf0 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -13,6 +13,3 @@ pub mod structs; mod tests; pub use cryptography::{nonces, snip12}; -use starknet::syscalls::call_contract_syscall; - -use starknet::{ContractAddress, SyscallResult}; From de11d9b885977b7bc1ec9a5a851437d8f29d7779 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 4 Oct 2024 15:24:38 -0400 Subject: [PATCH 14/76] fix: README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 6e4d27302..1e1e9063f 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,6 @@ mod MyToken { } ``` -### Unsupported - -[`DualCase` dispatchers](https://docs.openzeppelin.com/contracts-cairo/0.17.0/interfaces#dualcase_dispatchers) rely on Sierra's ability to catch a revert to resume execution. Currently, Starknet live chains (testnets and mainnet) don't implement that behavior. Starknet's testing framework does support it. - ## Learn ### Documentation From 55225bc01a8f59a834865c800ee4251367781a4e Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 4 Oct 2024 17:23:05 -0400 Subject: [PATCH 15/76] feat: move mocks into test_common --- Scarb.lock | 6 + packages/access/Scarb.toml | 11 - packages/access/src/tests/mocks.cairo | 2 - .../access/src/tests/test_accesscontrol.cairo | 2 +- packages/access/src/tests/test_ownable.cairo | 2 +- .../src/tests/test_ownable_twostep.cairo | 2 +- packages/account/Scarb.toml | 9 +- packages/account/src/tests/mocks.cairo | 4 - .../account/src/tests/test_eth_account.cairo | 2 +- packages/finance/Scarb.toml | 5 + packages/finance/src/lib.cairo | 2 + packages/finance/src/tests.cairo | 5 +- packages/finance/src/tests/common.cairo | 2 +- packages/finance/src/tests/mocks.cairo | 2 - .../finance/src/tests/mocks/erc20_mocks.cairo | 91 ------- .../src/tests/test_vesting_linear.cairo | 2 +- .../src/tests/test_vesting_steps.cairo | 2 +- packages/governance/Scarb.toml | 8 + packages/governance/src/lib.cairo | 1 + packages/governance/src/tests.cairo | 4 - packages/governance/src/tests/mocks.cairo | 2 - .../tests/mocks/non_implementing_mock.cairo | 10 - .../governance/src/tests/test_timelock.cairo | 8 +- packages/introspection/Scarb.toml | 1 + packages/introspection/src/lib.cairo | 1 + packages/introspection/src/tests.cairo | 3 - packages/introspection/src/tests/mocks.cairo | 1 - .../src/tests/mocks/src5_mocks.cairo | 22 -- .../introspection/src/tests/test_src5.cairo | 2 +- packages/presets/Scarb.toml | 13 + packages/presets/src/lib.cairo | 2 + packages/presets/src/tests.cairo | 9 - packages/presets/src/tests/mocks.cairo | 9 - .../src/tests/mocks/account_mocks.cairo | 87 ------ .../src/tests/mocks/erc1155_mocks.cairo | 50 ---- .../tests/mocks/erc1155_receiver_mocks.cairo | 38 --- .../presets/src/tests/mocks/erc20_mocks.cairo | 79 ------ .../src/tests/mocks/erc721_mocks.cairo | 50 ---- .../tests/mocks/erc721_receiver_mocks.cairo | 71 ----- .../src/tests/mocks/eth_account_mocks.cairo | 39 --- .../tests/mocks/non_implementing_mock.cairo | 10 - packages/security/Scarb.toml | 5 + packages/security/src/lib.cairo | 1 + packages/security/src/tests.cairo | 5 - packages/security/src/tests/mocks.cairo | 3 - .../src/tests/mocks/initializable_mocks.cairo | 23 -- .../src/tests/mocks/pausable_mocks.cairo | 23 -- .../src/tests/test_initializable.cairo | 2 +- .../security/src/tests/test_pausable.cairo | 2 +- .../src/tests/test_reentrancyguard.cairo | 2 +- packages/test_common/Scarb.toml | 2 + packages/test_common/src/mocks.cairo | 14 +- .../src/mocks/{ownable.cairo => access.cairo} | 40 +++ .../test_common/src/mocks/accesscontrol.cairo | 39 --- packages/test_common/src/mocks/account.cairo | 126 +++++++++ packages/test_common/src/mocks/erc1155.cairo | 142 ++++++++++ .../src/mocks/erc20.cairo} | 90 ++++++- .../src/mocks/erc2981.cairo} | 4 +- packages/test_common/src/mocks/erc721.cairo | 254 ++++++++++++++++++ .../test_common/src/mocks/eth_account.cairo | 43 --- .../src/mocks/nonces.cairo} | 4 +- .../src/mocks/security.cairo} | 55 +++- .../src/mocks/src5.cairo} | 2 +- .../src/mocks/timelock.cairo} | 16 +- .../src/mocks/upgrades.cairo} | 16 +- .../src/mocks/vesting.cairo} | 96 ++++++- packages/token/Scarb.toml | 7 + .../token/src/common/erc2981/erc2981.cairo | 2 +- packages/token/src/lib.cairo | 1 + packages/token/src/tests.cairo | 6 - .../src/tests/erc1155/test_erc1155.cairo | 2 +- .../tests/erc1155/test_erc1155_receiver.cairo | 2 +- .../token/src/tests/erc20/test_erc20.cairo | 2 +- .../src/tests/erc20/test_erc20_votes.cairo | 4 +- .../src/tests/erc2981/test_erc2981.cairo | 2 +- .../token/src/tests/erc721/test_erc721.cairo | 2 +- .../tests/erc721/test_erc721_enumerable.cairo | 2 +- .../tests/erc721/test_erc721_receiver.cairo | 2 +- packages/token/src/tests/mocks.cairo | 10 - .../token/src/tests/mocks/account_mocks.cairo | 45 ---- .../token/src/tests/mocks/erc1155_mocks.cairo | 52 ---- .../tests/mocks/erc1155_receiver_mocks.cairo | 40 --- .../token/src/tests/mocks/erc20_mocks.cairo | 40 --- .../tests/mocks/erc721_enumerable_mocks.cairo | 75 ------ .../token/src/tests/mocks/erc721_mocks.cairo | 55 ---- .../tests/mocks/erc721_receiver_mocks.cairo | 71 ----- .../token/src/tests/mocks/src5_mocks.cairo | 22 -- packages/upgrades/Scarb.toml | 4 + packages/upgrades/src/lib.cairo | 1 + packages/upgrades/src/tests.cairo | 3 - packages/upgrades/src/tests/mocks.cairo | 1 - .../upgrades/src/tests/test_upgradeable.cairo | 4 +- packages/utils/Scarb.toml | 1 + packages/utils/src/tests.cairo | 2 - packages/utils/src/tests/test_nonces.cairo | 2 +- 95 files changed, 915 insertions(+), 1227 deletions(-) delete mode 100644 packages/access/src/tests/mocks.cairo delete mode 100644 packages/account/src/tests/mocks.cairo delete mode 100644 packages/finance/src/tests/mocks.cairo delete mode 100644 packages/finance/src/tests/mocks/erc20_mocks.cairo delete mode 100644 packages/governance/src/tests/mocks.cairo delete mode 100644 packages/governance/src/tests/mocks/non_implementing_mock.cairo delete mode 100644 packages/introspection/src/tests/mocks.cairo delete mode 100644 packages/introspection/src/tests/mocks/src5_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/account_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/erc1155_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/erc20_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/erc721_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/eth_account_mocks.cairo delete mode 100644 packages/presets/src/tests/mocks/non_implementing_mock.cairo delete mode 100644 packages/security/src/tests/mocks.cairo delete mode 100644 packages/security/src/tests/mocks/initializable_mocks.cairo delete mode 100644 packages/security/src/tests/mocks/pausable_mocks.cairo rename packages/test_common/src/mocks/{ownable.cairo => access.cairo} (55%) delete mode 100644 packages/test_common/src/mocks/accesscontrol.cairo create mode 100644 packages/test_common/src/mocks/erc1155.cairo rename packages/{token/src/tests/mocks/erc20_votes_mocks.cairo => test_common/src/mocks/erc20.cairo} (52%) rename packages/{token/src/tests/mocks/erc2981_mocks.cairo => test_common/src/mocks/erc2981.cairo} (92%) create mode 100644 packages/test_common/src/mocks/erc721.cairo delete mode 100644 packages/test_common/src/mocks/eth_account.cairo rename packages/{utils/src/tests/mocks/nonces_mocks.cairo => test_common/src/mocks/nonces.cairo} (85%) rename packages/{security/src/tests/mocks/reentrancy_mocks.cairo => test_common/src/mocks/security.cairo} (71%) rename packages/{presets/src/tests/mocks/src5_mocks.cairo => test_common/src/mocks/src5.cairo} (94%) rename packages/{governance/src/tests/mocks/timelock_mocks.cairo => test_common/src/mocks/timelock.cairo} (92%) rename packages/{upgrades/src/tests/mocks/upgrades_mocks.cairo => test_common/src/mocks/upgrades.cairo} (88%) rename packages/{finance/src/tests/mocks/vesting_mocks.cairo => test_common/src/mocks/vesting.cairo} (59%) delete mode 100644 packages/token/src/tests/mocks.cairo delete mode 100644 packages/token/src/tests/mocks/account_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc1155_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc20_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc721_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/erc721_receiver_mocks.cairo delete mode 100644 packages/token/src/tests/mocks/src5_mocks.cairo delete mode 100644 packages/upgrades/src/tests/mocks.cairo diff --git a/Scarb.lock b/Scarb.lock index 0c51b589b..9d2a9e9cf 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -60,6 +60,7 @@ version = "0.17.0" dependencies = [ "openzeppelin_access", "openzeppelin_introspection", + "openzeppelin_test_common", "openzeppelin_testing", "snforge_std", ] @@ -68,6 +69,7 @@ dependencies = [ name = "openzeppelin_introspection" version = "0.17.0" dependencies = [ + "openzeppelin_test_common", "snforge_std", ] @@ -98,6 +100,7 @@ dependencies = [ name = "openzeppelin_security" version = "0.17.0" dependencies = [ + "openzeppelin_test_common", "openzeppelin_testing", "snforge_std", ] @@ -109,6 +112,8 @@ dependencies = [ "openzeppelin_access", "openzeppelin_account", "openzeppelin_finance", + "openzeppelin_introspection", + "openzeppelin_security", "openzeppelin_testing", "openzeppelin_token", "openzeppelin_upgrades", @@ -149,6 +154,7 @@ dependencies = [ name = "openzeppelin_utils" version = "0.17.0" dependencies = [ + "openzeppelin_test_common", "openzeppelin_testing", "snforge_std", ] diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 5ea06fd7e..0f29ee37b 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -38,14 +38,3 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false -build-external-contracts = [ - "openzeppelin_test_common::mocks::accesscontrol::SnakeAccessControlMock", - "openzeppelin_test_common::mocks::accesscontrol::CamelAccessControlMock", - "openzeppelin_test_common::mocks::accesscontrol::SnakeAccessControlPanicMock", - "openzeppelin_test_common::mocks::accesscontrol::CamelAccessControlPanicMock", - "openzeppelin_test_common::mocks::ownable::SnakeOwnableMock", - "openzeppelin_test_common::mocks::ownable::CamelOwnableMock", - "openzeppelin_test_common::mocks::ownable::SnakeOwnablePanicMock", - "openzeppelin_test_common::mocks::ownable::CamelOwnablePanicMock", - "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", -] diff --git a/packages/access/src/tests/mocks.cairo b/packages/access/src/tests/mocks.cairo deleted file mode 100644 index 4f4ebc2d9..000000000 --- a/packages/access/src/tests/mocks.cairo +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod accesscontrol_mocks; -pub(crate) mod ownable_mocks; diff --git a/packages/access/src/tests/test_accesscontrol.cairo b/packages/access/src/tests/test_accesscontrol.cairo index 3c4550016..81adcddfb 100644 --- a/packages/access/src/tests/test_accesscontrol.cairo +++ b/packages/access/src/tests/test_accesscontrol.cairo @@ -4,7 +4,7 @@ use crate::accesscontrol::AccessControlComponent::{ use crate::accesscontrol::interface::{IAccessControl, IAccessControlCamel, IACCESSCONTROL_ID}; use crate::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE}; use openzeppelin_introspection::interface::ISRC5; -use openzeppelin_test_common::mocks::accesscontrol::DualCaseAccessControlMock; +use openzeppelin_test_common::mocks::access::DualCaseAccessControlMock; use openzeppelin_testing::constants::{ ADMIN, AUTHORIZED, OTHER, OTHER_ADMIN, ROLE, OTHER_ROLE, ZERO }; diff --git a/packages/access/src/tests/test_ownable.cairo b/packages/access/src/tests/test_ownable.cairo index 47457cbc5..46c9d56da 100644 --- a/packages/access/src/tests/test_ownable.cairo +++ b/packages/access/src/tests/test_ownable.cairo @@ -2,7 +2,7 @@ use core::num::traits::Zero; use crate::ownable::OwnableComponent::InternalTrait; use crate::ownable::OwnableComponent; use crate::ownable::interface::{IOwnable, IOwnableCamelOnly}; -use openzeppelin_test_common::mocks::ownable::DualCaseOwnableMock; +use openzeppelin_test_common::mocks::access::DualCaseOwnableMock; use openzeppelin_test_common::ownable::OwnableSpyHelpers; use openzeppelin_testing::constants::{ZERO, OTHER, OWNER, RECIPIENT}; use snforge_std::{spy_events, test_address, start_cheat_caller_address}; diff --git a/packages/access/src/tests/test_ownable_twostep.cairo b/packages/access/src/tests/test_ownable_twostep.cairo index 5965bd926..ec6d6242b 100644 --- a/packages/access/src/tests/test_ownable_twostep.cairo +++ b/packages/access/src/tests/test_ownable_twostep.cairo @@ -2,7 +2,7 @@ use core::num::traits::Zero; use crate::ownable::OwnableComponent::{InternalTrait, OwnershipTransferStarted}; use crate::ownable::OwnableComponent; use crate::ownable::interface::{IOwnableTwoStep, IOwnableTwoStepCamelOnly}; -use openzeppelin_test_common::mocks::ownable::DualCaseTwoStepOwnableMock; +use openzeppelin_test_common::mocks::access::DualCaseTwoStepOwnableMock; use openzeppelin_test_common::ownable::OwnableSpyHelpers; use openzeppelin_testing::constants::{ZERO, OWNER, OTHER, NEW_OWNER}; use openzeppelin_testing::events::EventSpyExt; diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index de7a83b55..ac52a50c4 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -40,14 +40,7 @@ sierra = true casm = false build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", - "openzeppelin_test_common::mocks::account::SnakeAccountMock", - "openzeppelin_test_common::mocks::account::SnakeAccountPanicMock", - "openzeppelin_test_common::mocks::account::CamelAccountPanicMock", - "openzeppelin_test_common::mocks::eth_account::SnakeEthAccountMock", - "openzeppelin_test_common::mocks::eth_account::SnakeEthAccountPanicMock", - "openzeppelin_test_common::mocks::eth_account::CamelEthAccountPanicMock", - "openzeppelin_test_common::mocks::eth_account::DualCaseEthAccountMock", + "openzeppelin_test_common::mocks::account::DualCaseEthAccountMock", "openzeppelin_test_common::mocks::src9::SRC9AccountMock", "openzeppelin_test_common::mocks::simple::SimpleMock", - "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", ] diff --git a/packages/account/src/tests/mocks.cairo b/packages/account/src/tests/mocks.cairo deleted file mode 100644 index e9e44bf5d..000000000 --- a/packages/account/src/tests/mocks.cairo +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod account_mocks; -pub(crate) mod eth_account_mocks; -pub(crate) mod simple_mock; -pub(crate) mod src9_mocks; diff --git a/packages/account/src/tests/test_eth_account.cairo b/packages/account/src/tests/test_eth_account.cairo index 1e62f5bb3..cfa32e99e 100644 --- a/packages/account/src/tests/test_eth_account.cairo +++ b/packages/account/src/tests/test_eth_account.cairo @@ -10,7 +10,7 @@ use openzeppelin_test_common::eth_account::EthAccountSpyHelpers; use openzeppelin_test_common::eth_account::{ SIGNED_TX_DATA, SignedTransactionData, get_accept_ownership_signature }; -use openzeppelin_test_common::mocks::eth_account::DualCaseEthAccountMock; +use openzeppelin_test_common::mocks::account::DualCaseEthAccountMock; use openzeppelin_test_common::mocks::simple::{ISimpleMockDispatcher, ISimpleMockDispatcherTrait}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::secp256k1::{KEY_PAIR, KEY_PAIR_2}; diff --git a/packages/finance/Scarb.toml b/packages/finance/Scarb.toml index e1cdf8cb5..1df83c51f 100644 --- a/packages/finance/Scarb.toml +++ b/packages/finance/Scarb.toml @@ -37,3 +37,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::vesting::LinearVestingMock", + "openzeppelin_test_common::mocks::vesting::StepsVestingMock", + "openzeppelin_test_common::mocks::vesting::ERC20OptionalTransferPanicMock", +] diff --git a/packages/finance/src/lib.cairo b/packages/finance/src/lib.cairo index a5a96a4c0..c8f018c2e 100644 --- a/packages/finance/src/lib.cairo +++ b/packages/finance/src/lib.cairo @@ -1,2 +1,4 @@ +#[cfg(test)] mod tests; + pub mod vesting; diff --git a/packages/finance/src/tests.cairo b/packages/finance/src/tests.cairo index 2ccfca4b9..cb61539ba 100644 --- a/packages/finance/src/tests.cairo +++ b/packages/finance/src/tests.cairo @@ -1,7 +1,4 @@ -#[cfg(test)] pub(crate) mod common; -pub(crate) mod mocks; -#[cfg(test)] + mod test_vesting_linear; -#[cfg(test)] mod test_vesting_steps; diff --git a/packages/finance/src/tests/common.cairo b/packages/finance/src/tests/common.cairo index 8dc623a80..bad9056f6 100644 --- a/packages/finance/src/tests/common.cairo +++ b/packages/finance/src/tests/common.cairo @@ -51,7 +51,7 @@ fn deploy_erc20_mock(recipient: ContractAddress, initial_supply: u256) -> IERC20 calldata.append_serde(initial_supply); calldata.append_serde(recipient); - let contract_address = utils::declare_and_deploy("ERC20Mock", calldata); + let contract_address = utils::declare_and_deploy("ERC20OptionalTransferPanicMock", calldata); IERC20Dispatcher { contract_address } } diff --git a/packages/finance/src/tests/mocks.cairo b/packages/finance/src/tests/mocks.cairo deleted file mode 100644 index c5a78361f..000000000 --- a/packages/finance/src/tests/mocks.cairo +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod erc20_mocks; -pub(crate) mod vesting_mocks; diff --git a/packages/finance/src/tests/mocks/erc20_mocks.cairo b/packages/finance/src/tests/mocks/erc20_mocks.cairo deleted file mode 100644 index 21bdec7d9..000000000 --- a/packages/finance/src/tests/mocks/erc20_mocks.cairo +++ /dev/null @@ -1,91 +0,0 @@ -#[starknet::contract] -pub(crate) mod ERC20Mock { - use openzeppelin_token::erc20::interface::IERC20; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - struct Storage { - transfer_should_fail: bool, - #[substorage(v0)] - erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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); - } - - #[abi(embed_v0)] - impl ERC20Impl of IERC20 { - fn total_supply(self: @ContractState) -> u256 { - self.erc20.total_supply() - } - - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - self.erc20.balance_of(account) - } - - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> u256 { - self.erc20.allowance(owner, spender) - } - - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { - if self.transfer_should_fail.read() { - false - } else { - self.erc20.transfer(recipient, amount) - } - } - - fn transfer_from( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) -> bool { - if self.transfer_should_fail.read() { - false - } else { - self.erc20.transfer_from(sender, recipient, amount) - } - } - - fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { - self.erc20.approve(spender, amount) - } - } - - #[generate_trait] - #[abi(per_item)] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn set_transfer_should_fail(ref self: ContractState, should_fail: bool) { - self.transfer_should_fail.write(should_fail); - } - } -} diff --git a/packages/finance/src/tests/test_vesting_linear.cairo b/packages/finance/src/tests/test_vesting_linear.cairo index f7e6251f8..393f180b1 100644 --- a/packages/finance/src/tests/test_vesting_linear.cairo +++ b/packages/finance/src/tests/test_vesting_linear.cairo @@ -1,9 +1,9 @@ use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; use openzeppelin_finance::tests::common::{VestingStrategy, TestData, setup, set_transfer_to_fail}; -use openzeppelin_finance::tests::mocks::vesting_mocks::LinearVestingMock; use openzeppelin_finance::vesting::VestingComponent::InternalImpl; use openzeppelin_finance::vesting::VestingComponent; use openzeppelin_finance::vesting::interface::IVestingDispatcherTrait; +use openzeppelin_test_common::mocks::vesting::LinearVestingMock; use openzeppelin_test_common::vesting::VestingSpyHelpers; use openzeppelin_testing::constants::{OWNER, OTHER}; use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/packages/finance/src/tests/test_vesting_steps.cairo b/packages/finance/src/tests/test_vesting_steps.cairo index f08192b7d..3d11165b0 100644 --- a/packages/finance/src/tests/test_vesting_steps.cairo +++ b/packages/finance/src/tests/test_vesting_steps.cairo @@ -1,9 +1,9 @@ use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; use openzeppelin_finance::tests::common::{VestingStrategy, TestData, setup, set_transfer_to_fail}; -use openzeppelin_finance::tests::mocks::vesting_mocks::StepsVestingMock; use openzeppelin_finance::vesting::VestingComponent::InternalImpl; use openzeppelin_finance::vesting::VestingComponent; use openzeppelin_finance::vesting::interface::IVestingDispatcherTrait; +use openzeppelin_test_common::mocks::vesting::StepsVestingMock; use openzeppelin_test_common::vesting::VestingSpyHelpers; use openzeppelin_testing::constants::{OWNER, OTHER}; use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index d54c38b0a..ce04ec985 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -30,6 +30,7 @@ openzeppelin_introspection = { path = "../introspection" } assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } +openzeppelin_test_common = { path = "../test_common" } [lib] @@ -37,3 +38,10 @@ openzeppelin_testing = { path = "../testing" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::timelock::TimelockControllerMock", + "openzeppelin_test_common::mocks::timelock::MockContract", + "openzeppelin_test_common::mocks::timelock::TimelockAttackerMock", +] + + diff --git a/packages/governance/src/lib.cairo b/packages/governance/src/lib.cairo index 852e24521..1320484fb 100644 --- a/packages/governance/src/lib.cairo +++ b/packages/governance/src/lib.cairo @@ -1,3 +1,4 @@ +#[cfg(test)] mod tests; pub mod timelock; diff --git a/packages/governance/src/tests.cairo b/packages/governance/src/tests.cairo index 221670eec..3aa8297b3 100644 --- a/packages/governance/src/tests.cairo +++ b/packages/governance/src/tests.cairo @@ -1,6 +1,2 @@ -pub(crate) mod mocks; - -#[cfg(test)] mod test_timelock; -#[cfg(test)] mod test_utils; diff --git a/packages/governance/src/tests/mocks.cairo b/packages/governance/src/tests/mocks.cairo deleted file mode 100644 index 6d38a6715..000000000 --- a/packages/governance/src/tests/mocks.cairo +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod non_implementing_mock; -pub(crate) mod timelock_mocks; diff --git a/packages/governance/src/tests/mocks/non_implementing_mock.cairo b/packages/governance/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/governance/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} diff --git a/packages/governance/src/tests/test_timelock.cairo b/packages/governance/src/tests/test_timelock.cairo index a110683b5..5851ab178 100644 --- a/packages/governance/src/tests/test_timelock.cairo +++ b/packages/governance/src/tests/test_timelock.cairo @@ -1,8 +1,5 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::pedersen::PedersenTrait; -use crate::tests::mocks::timelock_mocks::{IMockContractDispatcher, IMockContractDispatcherTrait}; -use crate::tests::mocks::timelock_mocks::{ITimelockAttackerDispatcher}; -use crate::tests::mocks::timelock_mocks::{TimelockControllerMock}; use crate::timelock::OperationState; use crate::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, CallCancelled, MinDelayChanged @@ -21,6 +18,11 @@ use openzeppelin_access::accesscontrol::interface::IACCESSCONTROL_ID; use openzeppelin_access::accesscontrol::interface::IAccessControl; use openzeppelin_introspection::interface::ISRC5_ID; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::timelock::ITimelockAttackerDispatcher; +use openzeppelin_test_common::mocks::timelock::TimelockControllerMock; +use openzeppelin_test_common::mocks::timelock::{ + IMockContractDispatcher, IMockContractDispatcherTrait +}; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{ADMIN, ZERO, OTHER, SALT, FELT_VALUE as VALUE}; use openzeppelin_testing::events::EventSpyExt; diff --git a/packages/introspection/Scarb.toml b/packages/introspection/Scarb.toml index c9567c1e9..32049773b 100644 --- a/packages/introspection/Scarb.toml +++ b/packages/introspection/Scarb.toml @@ -28,6 +28,7 @@ starknet.workspace = true [dev-dependencies] assert_macros.workspace = true snforge_std.workspace = true +openzeppelin_test_common = { path = "../test_common" } [lib] diff --git a/packages/introspection/src/lib.cairo b/packages/introspection/src/lib.cairo index 6134bb225..01a03e7d8 100644 --- a/packages/introspection/src/lib.cairo +++ b/packages/introspection/src/lib.cairo @@ -1,4 +1,5 @@ pub mod interface; pub mod src5; +#[cfg(test)] mod tests; diff --git a/packages/introspection/src/tests.cairo b/packages/introspection/src/tests.cairo index 72a21d551..32094b462 100644 --- a/packages/introspection/src/tests.cairo +++ b/packages/introspection/src/tests.cairo @@ -1,4 +1 @@ -pub(crate) mod mocks; - -#[cfg(test)] mod test_src5; diff --git a/packages/introspection/src/tests/mocks.cairo b/packages/introspection/src/tests/mocks.cairo deleted file mode 100644 index dc995e8e3..000000000 --- a/packages/introspection/src/tests/mocks.cairo +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod src5_mocks; diff --git a/packages/introspection/src/tests/mocks/src5_mocks.cairo b/packages/introspection/src/tests/mocks/src5_mocks.cairo deleted file mode 100644 index 81e65d236..000000000 --- a/packages/introspection/src/tests/mocks/src5_mocks.cairo +++ /dev/null @@ -1,22 +0,0 @@ -#[starknet::contract] -pub(crate) mod SRC5Mock { - use crate::src5::SRC5Component; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - SRC5Event: SRC5Component::Event - } -} diff --git a/packages/introspection/src/tests/test_src5.cairo b/packages/introspection/src/tests/test_src5.cairo index 1f131eeab..0473c8f31 100644 --- a/packages/introspection/src/tests/test_src5.cairo +++ b/packages/introspection/src/tests/test_src5.cairo @@ -1,7 +1,7 @@ use crate::interface::{ISRC5_ID, ISRC5}; use crate::src5::SRC5Component::InternalTrait; use crate::src5::SRC5Component; -use crate::tests::mocks::src5_mocks::SRC5Mock; +use openzeppelin_test_common::mocks::src5::SRC5Mock; const OTHER_ID: felt252 = 0x12345678; diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index 2b7d7c2d9..ec39916d6 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -42,3 +42,16 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::account::DualCaseAccountMock", + "openzeppelin_test_common::mocks::account::SnakeAccountMock", + "openzeppelin_test_common::mocks::account::SnakeEthAccountMock", + "openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock", + "openzeppelin_test_common::mocks::erc20::SnakeERC20Mock", + "openzeppelin_test_common::mocks::erc721::SnakeERC721Mock", + "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", + "openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock", + "openzeppelin_test_common::mocks::erc1155::SnakeERC1155Mock", + "openzeppelin_test_common::mocks::src5::SRC5Mock", + "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", +] diff --git a/packages/presets/src/lib.cairo b/packages/presets/src/lib.cairo index 5dd344f9d..20d787138 100644 --- a/packages/presets/src/lib.cairo +++ b/packages/presets/src/lib.cairo @@ -5,7 +5,9 @@ pub mod erc721; pub mod eth_account; pub mod interfaces; +#[cfg(test)] mod tests; + pub mod universal_deployer; pub mod vesting; diff --git a/packages/presets/src/tests.cairo b/packages/presets/src/tests.cairo index 68fc25020..211c76d41 100644 --- a/packages/presets/src/tests.cairo +++ b/packages/presets/src/tests.cairo @@ -1,16 +1,7 @@ -mod mocks; - -#[cfg(test)] mod test_account; -#[cfg(test)] mod test_erc1155; -#[cfg(test)] mod test_erc20; -#[cfg(test)] mod test_erc721; -#[cfg(test)] mod test_eth_account; -#[cfg(test)] mod test_universal_deployer; -#[cfg(test)] mod test_vesting; diff --git a/packages/presets/src/tests/mocks.cairo b/packages/presets/src/tests/mocks.cairo deleted file mode 100644 index 2037caeeb..000000000 --- a/packages/presets/src/tests/mocks.cairo +++ /dev/null @@ -1,9 +0,0 @@ -pub(crate) mod account_mocks; -pub(crate) mod erc1155_mocks; -pub(crate) mod erc1155_receiver_mocks; -pub(crate) mod erc20_mocks; -pub(crate) mod erc721_mocks; -pub(crate) mod erc721_receiver_mocks; -pub(crate) mod eth_account_mocks; -pub(crate) mod non_implementing_mock; -pub(crate) mod src5_mocks; diff --git a/packages/presets/src/tests/mocks/account_mocks.cairo b/packages/presets/src/tests/mocks/account_mocks.cairo deleted file mode 100644 index c92013f39..000000000 --- a/packages/presets/src/tests/mocks/account_mocks.cairo +++ /dev/null @@ -1,87 +0,0 @@ -#[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); - } -} diff --git a/packages/presets/src/tests/mocks/erc1155_mocks.cairo b/packages/presets/src/tests/mocks/erc1155_mocks.cairo deleted file mode 100644 index 8ebd48000..000000000 --- a/packages/presets/src/tests/mocks/erc1155_mocks.cairo +++ /dev/null @@ -1,50 +0,0 @@ -#[starknet::contract] -pub(crate) mod SnakeERC1155Mock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC1155 - #[abi(embed_v0)] - impl ERC1155Impl = ERC1155Component::ERC1155Impl; - #[abi(embed_v0)] - impl ERC1155MetadataURIImpl = - ERC1155Component::ERC1155MetadataURIImpl; - impl ERC1155InternalImpl = ERC1155Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155: ERC1155Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155Event: ERC1155Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256, - value: u256 - ) { - self.erc1155.initializer(base_uri); - self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); - } -} diff --git a/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo b/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo deleted file mode 100644 index 30e722593..000000000 --- a/packages/presets/src/tests/mocks/erc1155_receiver_mocks.cairo +++ /dev/null @@ -1,38 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC1155ReceiverMock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc1155::ERC1155ReceiverComponent; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - component!( - path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent - ); - - // ERC1155Receiver Mixin - #[abi(embed_v0)] - impl ERC1155ReceiverMixinImpl = - ERC1155ReceiverComponent::ERC1155ReceiverMixinImpl; - impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155_receiver: ERC1155ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc1155_receiver.initializer(); - } -} diff --git a/packages/presets/src/tests/mocks/erc20_mocks.cairo b/packages/presets/src/tests/mocks/erc20_mocks.cairo deleted file mode 100644 index a80b5c29c..000000000 --- a/packages/presets/src/tests/mocks/erc20_mocks.cairo +++ /dev/null @@ -1,79 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - #[abi(embed_v0)] - impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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); - } -} - -#[starknet::contract] -pub(crate) mod SnakeERC20Mock { - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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/presets/src/tests/mocks/erc721_mocks.cairo b/packages/presets/src/tests/mocks/erc721_mocks.cairo deleted file mode 100644 index bf7357b32..000000000 --- a/packages/presets/src/tests/mocks/erc721_mocks.cairo +++ /dev/null @@ -1,50 +0,0 @@ -#[starknet::contract] -pub(crate) mod SnakeERC721Mock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - #[abi(embed_v0)] - impl ERC721Impl = ERC721Component::ERC721Impl; - #[abi(embed_v0)] - impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721.mint(recipient, token_id); - } -} diff --git a/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo b/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo deleted file mode 100644 index b18b1d647..000000000 --- a/packages/presets/src/tests/mocks/erc721_receiver_mocks.cairo +++ /dev/null @@ -1,71 +0,0 @@ -const SUCCESS: felt252 = 'SUCCESS'; - -#[starknet::contract] -pub(crate) mod DualCaseERC721ReceiverMock { - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc721::ERC721ReceiverComponent; - use starknet::ContractAddress; - - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721Receiver - impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc721_receiver.initializer(); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - if *data.at(0) == super::SUCCESS { - self.erc721_receiver.on_erc721_received(operator, from, token_id, data) - } else { - 0 - } - } - - #[external(v0)] - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252 { - Self::on_erc721_received(self, operator, from, tokenId, data) - } - } -} diff --git a/packages/presets/src/tests/mocks/eth_account_mocks.cairo b/packages/presets/src/tests/mocks/eth_account_mocks.cairo deleted file mode 100644 index 392bd12f8..000000000 --- a/packages/presets/src/tests/mocks/eth_account_mocks.cairo +++ /dev/null @@ -1,39 +0,0 @@ -#[starknet::contract(account)] -pub(crate) mod SnakeEthAccountMock { - use openzeppelin_account::EthAccountComponent; - use openzeppelin_account::interface::EthPublicKey; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC6Impl = EthAccountComponent::SRC6Impl; - #[abi(embed_v0)] - impl PublicKeyImpl = EthAccountComponent::PublicKeyImpl; - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub eth_account: EthAccountComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - EthAccountEvent: EthAccountComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, public_key: EthPublicKey) { - self.eth_account.initializer(public_key); - } -} diff --git a/packages/presets/src/tests/mocks/non_implementing_mock.cairo b/packages/presets/src/tests/mocks/non_implementing_mock.cairo deleted file mode 100644 index 67d632c72..000000000 --- a/packages/presets/src/tests/mocks/non_implementing_mock.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::contract] -pub(crate) mod NonImplementingMock { - #[storage] - pub struct Storage {} - - #[external(v0)] - fn nope(self: @ContractState) -> bool { - false - } -} diff --git a/packages/security/Scarb.toml b/packages/security/Scarb.toml index 1e8262f2e..86ebefe15 100644 --- a/packages/security/Scarb.toml +++ b/packages/security/Scarb.toml @@ -28,6 +28,7 @@ starknet.workspace = true assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } +openzeppelin_test_common = { path = "../test_common" } [lib] @@ -35,3 +36,7 @@ openzeppelin_testing = { path = "../testing" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::security::ReentrancyMock", + "openzeppelin_test_common::mocks::security::Attacker", +] diff --git a/packages/security/src/lib.cairo b/packages/security/src/lib.cairo index f8ecbcedc..d479f9e05 100644 --- a/packages/security/src/lib.cairo +++ b/packages/security/src/lib.cairo @@ -3,6 +3,7 @@ pub mod interface; pub mod pausable; pub mod reentrancyguard; +#[cfg(test)] mod tests; pub use initializable::InitializableComponent; diff --git a/packages/security/src/tests.cairo b/packages/security/src/tests.cairo index 0f674d192..35c30a0e8 100644 --- a/packages/security/src/tests.cairo +++ b/packages/security/src/tests.cairo @@ -1,8 +1,3 @@ -pub(crate) mod mocks; - -#[cfg(test)] mod test_initializable; -#[cfg(test)] mod test_pausable; -#[cfg(test)] mod test_reentrancyguard; diff --git a/packages/security/src/tests/mocks.cairo b/packages/security/src/tests/mocks.cairo deleted file mode 100644 index 81134e172..000000000 --- a/packages/security/src/tests/mocks.cairo +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod initializable_mocks; -pub(crate) mod pausable_mocks; -pub(crate) mod reentrancy_mocks; diff --git a/packages/security/src/tests/mocks/initializable_mocks.cairo b/packages/security/src/tests/mocks/initializable_mocks.cairo deleted file mode 100644 index 096acb947..000000000 --- a/packages/security/src/tests/mocks/initializable_mocks.cairo +++ /dev/null @@ -1,23 +0,0 @@ -#[starknet::contract] -pub(crate) mod InitializableMock { - use crate::initializable::InitializableComponent; - - component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); - - #[abi(embed_v0)] - impl InitializableImpl = - InitializableComponent::InitializableImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub initializable: InitializableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - InitializableEvent: InitializableComponent::Event - } -} diff --git a/packages/security/src/tests/mocks/pausable_mocks.cairo b/packages/security/src/tests/mocks/pausable_mocks.cairo deleted file mode 100644 index 383a02fdb..000000000 --- a/packages/security/src/tests/mocks/pausable_mocks.cairo +++ /dev/null @@ -1,23 +0,0 @@ -#[starknet::contract] -pub(crate) mod PausableMock { - use crate::pausable::PausableComponent; - - component!(path: PausableComponent, storage: pausable, event: PausableEvent); - - #[abi(embed_v0)] - impl PausableImpl = PausableComponent::PausableImpl; - impl InternalImpl = PausableComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub pausable: PausableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - PausableEvent: PausableComponent::Event - } -} diff --git a/packages/security/src/tests/test_initializable.cairo b/packages/security/src/tests/test_initializable.cairo index 6817ca6ef..0b7ac8fc7 100644 --- a/packages/security/src/tests/test_initializable.cairo +++ b/packages/security/src/tests/test_initializable.cairo @@ -1,6 +1,6 @@ use crate::InitializableComponent::{InitializableImpl, InternalImpl}; use crate::InitializableComponent; -use crate::tests::mocks::initializable_mocks::InitializableMock; +use openzeppelin_test_common::mocks::security::InitializableMock; type ComponentState = InitializableComponent::ComponentState; diff --git a/packages/security/src/tests/test_pausable.cairo b/packages/security/src/tests/test_pausable.cairo index de50a5683..486819c0e 100644 --- a/packages/security/src/tests/test_pausable.cairo +++ b/packages/security/src/tests/test_pausable.cairo @@ -1,7 +1,7 @@ use crate::PausableComponent::{InternalImpl, PausableImpl}; use crate::PausableComponent::{Paused, Unpaused}; use crate::PausableComponent; -use crate::tests::mocks::pausable_mocks::PausableMock; +use openzeppelin_test_common::mocks::security::PausableMock; use openzeppelin_testing::constants::CALLER; use openzeppelin_testing::events::EventSpyExt; use snforge_std::EventSpy; diff --git a/packages/security/src/tests/test_reentrancyguard.cairo b/packages/security/src/tests/test_reentrancyguard.cairo index aaa1db457..8647c9e9e 100644 --- a/packages/security/src/tests/test_reentrancyguard.cairo +++ b/packages/security/src/tests/test_reentrancyguard.cairo @@ -1,6 +1,6 @@ use crate::ReentrancyGuardComponent::InternalImpl; use crate::ReentrancyGuardComponent; -use crate::tests::mocks::reentrancy_mocks::{ +use openzeppelin_test_common::mocks::security::{ ReentrancyMock, IReentrancyMockDispatcher, IReentrancyMockDispatcherTrait }; use openzeppelin_testing as utils; diff --git a/packages/test_common/Scarb.toml b/packages/test_common/Scarb.toml index 1314cb3ea..6f5f54d22 100644 --- a/packages/test_common/Scarb.toml +++ b/packages/test_common/Scarb.toml @@ -23,6 +23,8 @@ openzeppelin_upgrades = { path = "../upgrades" } openzeppelin_access = { path = "../access" } openzeppelin_account = { path = "../account" } openzeppelin_finance = { path = "../finance" } +openzeppelin_introspection = { path = "../introspection" } +openzeppelin_security = { path = "../security" } openzeppelin_token = { path = "../token" } openzeppelin_testing = { path = "../testing" } openzeppelin_utils = { path = "../utils" } diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo index 9d79b8e03..5cd4a9a96 100644 --- a/packages/test_common/src/mocks.cairo +++ b/packages/test_common/src/mocks.cairo @@ -1,7 +1,15 @@ -pub mod accesscontrol; +pub mod access; pub mod account; -pub mod eth_account; +pub mod erc1155; +pub mod erc20; +pub mod erc2981; +pub mod erc721; pub mod non_implementing; -pub mod ownable; +pub mod nonces; +pub mod security; pub mod simple; +pub mod src5; pub mod src9; +pub mod timelock; +pub mod upgrades; +pub mod vesting; diff --git a/packages/test_common/src/mocks/ownable.cairo b/packages/test_common/src/mocks/access.cairo similarity index 55% rename from packages/test_common/src/mocks/ownable.cairo rename to packages/test_common/src/mocks/access.cairo index 9098b9923..ccca94518 100644 --- a/packages/test_common/src/mocks/ownable.cairo +++ b/packages/test_common/src/mocks/access.cairo @@ -1,3 +1,43 @@ +#[starknet::contract] +pub mod DualCaseAccessControlMock { + use openzeppelin_access::accesscontrol::AccessControlComponent; + use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::ContractAddress; + + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // AccessControlMixin + #[abi(embed_v0)] + impl AccessControlMixinImpl = + AccessControlComponent::AccessControlMixinImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub accesscontrol: AccessControlComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccessControlEvent: AccessControlComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, admin: ContractAddress) { + self.accesscontrol.initializer(); + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); + } +} + #[starknet::contract] pub mod DualCaseOwnableMock { use openzeppelin_access::ownable::OwnableComponent; diff --git a/packages/test_common/src/mocks/accesscontrol.cairo b/packages/test_common/src/mocks/accesscontrol.cairo deleted file mode 100644 index 773c01bd5..000000000 --- a/packages/test_common/src/mocks/accesscontrol.cairo +++ /dev/null @@ -1,39 +0,0 @@ -#[starknet::contract] -pub mod DualCaseAccessControlMock { - use openzeppelin_access::accesscontrol::AccessControlComponent; - use openzeppelin_access::accesscontrol::DEFAULT_ADMIN_ROLE; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // AccessControlMixin - #[abi(embed_v0)] - impl AccessControlMixinImpl = - AccessControlComponent::AccessControlMixinImpl; - impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub accesscontrol: AccessControlComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - AccessControlEvent: AccessControlComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, admin: ContractAddress) { - self.accesscontrol.initializer(); - self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin); - } -} diff --git a/packages/test_common/src/mocks/account.cairo b/packages/test_common/src/mocks/account.cairo index 9e17cf650..69c6eaf02 100644 --- a/packages/test_common/src/mocks/account.cairo +++ b/packages/test_common/src/mocks/account.cairo @@ -43,3 +43,129 @@ pub mod DualCaseAccountMock { self.account.initializer(public_key); } } + +#[starknet::contract(account)] +pub 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 mod DualCaseEthAccountMock { + use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::interface::EthPublicKey; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl SRC6Impl = EthAccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl SRC6CamelOnlyImpl = EthAccountComponent::SRC6CamelOnlyImpl; + #[abi(embed_v0)] + impl DeclarerImpl = EthAccountComponent::DeclarerImpl; + #[abi(embed_v0)] + impl DeployableImpl = EthAccountComponent::DeployableImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub eth_account: EthAccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + EthAccountEvent: EthAccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: EthPublicKey) { + self.eth_account.initializer(public_key); + } +} + +#[starknet::contract(account)] +pub mod SnakeEthAccountMock { + use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::interface::EthPublicKey; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl SRC6Impl = EthAccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl PublicKeyImpl = EthAccountComponent::PublicKeyImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub eth_account: EthAccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + EthAccountEvent: EthAccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: EthPublicKey) { + self.eth_account.initializer(public_key); + } +} diff --git a/packages/test_common/src/mocks/erc1155.cairo b/packages/test_common/src/mocks/erc1155.cairo new file mode 100644 index 000000000..8c206e95e --- /dev/null +++ b/packages/test_common/src/mocks/erc1155.cairo @@ -0,0 +1,142 @@ +#[starknet::contract] +pub mod DualCaseERC1155Mock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155Impl = ERC1155Component::ERC1155Impl; + #[abi(embed_v0)] + impl ERC1155MetadataURIImpl = + ERC1155Component::ERC1155MetadataURIImpl; + #[abi(embed_v0)] + impl ERC721Camel = ERC1155Component::ERC1155CamelImpl; + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc1155: ERC1155Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256, + value: u256 + ) { + self.erc1155.initializer(base_uri); + self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); + } +} + +#[starknet::contract] +pub mod SnakeERC1155Mock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC1155 + #[abi(embed_v0)] + impl ERC1155Impl = ERC1155Component::ERC1155Impl; + #[abi(embed_v0)] + impl ERC1155MetadataURIImpl = + ERC1155Component::ERC1155MetadataURIImpl; + impl ERC1155InternalImpl = ERC1155Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc1155: ERC1155Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155Event: ERC1155Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256, + value: u256 + ) { + self.erc1155.initializer(base_uri); + self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); + } +} + +#[starknet::contract] +pub mod DualCaseERC1155ReceiverMock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc1155::ERC1155ReceiverComponent; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!( + path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent + ); + + // ERC1155Receiver Mixin + #[abi(embed_v0)] + impl ERC1155ReceiverMixinImpl = + ERC1155ReceiverComponent::ERC1155ReceiverMixinImpl; + impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc1155_receiver: ERC1155ReceiverComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc1155_receiver.initializer(); + } +} diff --git a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo b/packages/test_common/src/mocks/erc20.cairo similarity index 52% rename from packages/token/src/tests/mocks/erc20_votes_mocks.cairo rename to packages/test_common/src/mocks/erc20.cairo index c777ec62f..650854aaa 100644 --- a/packages/token/src/tests/mocks/erc20_votes_mocks.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -1,8 +1,88 @@ #[starknet::contract] -pub(crate) mod DualCaseERC20VotesMock { - use crate::erc20::ERC20Component; - use crate::erc20::extensions::ERC20VotesComponent::InternalTrait as ERC20VotesInternalTrait; - use crate::erc20::extensions::ERC20VotesComponent; +pub mod DualCaseERC20Mock { + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[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); + } +} + +#[starknet::contract] +pub mod SnakeERC20Mock { + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[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); + } +} + +#[starknet::contract] +pub mod DualCaseERC20VotesMock { + use openzeppelin_token::erc20::ERC20Component; + use openzeppelin_token::erc20::extensions::ERC20VotesComponent::InternalTrait as ERC20VotesInternalTrait; + use openzeppelin_token::erc20::extensions::ERC20VotesComponent; use openzeppelin_utils::cryptography::nonces::NoncesComponent; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; @@ -47,7 +127,7 @@ pub(crate) mod DualCaseERC20VotesMock { } /// Required for hash computation. - pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata { + pub impl SNIP12MetadataImpl of SNIP12Metadata { fn name() -> felt252 { 'DAPP_NAME' } diff --git a/packages/token/src/tests/mocks/erc2981_mocks.cairo b/packages/test_common/src/mocks/erc2981.cairo similarity index 92% rename from packages/token/src/tests/mocks/erc2981_mocks.cairo rename to packages/test_common/src/mocks/erc2981.cairo index ead9863d4..e60da6232 100644 --- a/packages/token/src/tests/mocks/erc2981_mocks.cairo +++ b/packages/test_common/src/mocks/erc2981.cairo @@ -1,7 +1,7 @@ #[starknet::contract] -pub(crate) mod ERC2981Mock { - use crate::common::erc2981::{ERC2981Component, DefaultConfig}; +pub mod ERC2981Mock { use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::common::erc2981::{ERC2981Component, DefaultConfig}; use starknet::ContractAddress; component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event); diff --git a/packages/test_common/src/mocks/erc721.cairo b/packages/test_common/src/mocks/erc721.cairo new file mode 100644 index 000000000..18106dac7 --- /dev/null +++ b/packages/test_common/src/mocks/erc721.cairo @@ -0,0 +1,254 @@ +const SUCCESS: felt252 = 'SUCCESS'; + +#[starknet::contract] +pub mod DualCaseERC721Mock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC721 + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + #[abi(embed_v0)] + impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC721MetadataCamelOnly = + ERC721Component::ERC721MetadataCamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc721: ERC721Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256 + ) { + self.erc721.initializer(name, symbol, base_uri); + self.erc721.mint(recipient, token_id); + } +} + +#[starknet::contract] +pub mod SnakeERC721Mock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC721 + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc721: ERC721Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256 + ) { + self.erc721.initializer(name, symbol, base_uri); + self.erc721.mint(recipient, token_id); + } +} + +#[starknet::contract] +pub mod DualCaseERC721ReceiverMock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::ERC721ReceiverComponent; + use starknet::ContractAddress; + + component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC721Receiver + impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; + impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc721_receiver: ERC721ReceiverComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721ReceiverEvent: ERC721ReceiverComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.erc721_receiver.initializer(); + } + + #[abi(per_item)] + #[generate_trait] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn on_erc721_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + self.erc721_receiver.on_erc721_received(operator, from, token_id, data) + } else { + 0 + } + } + + #[external(v0)] + fn onERC721Received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + data: Span + ) -> felt252 { + Self::on_erc721_received(self, operator, from, tokenId, data) + } + } +} + +#[starknet::contract] +pub mod ERC721EnumerableMock { + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::ERC721Component; + use openzeppelin_token::erc721::extensions::ERC721EnumerableComponent; + use starknet::ContractAddress; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!( + path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent + ); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // ERC721 + #[abi(embed_v0)] + impl ERC721MixinImpl = ERC721Component::ERC721Impl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // ERC721Enumerable + #[abi(embed_v0)] + impl ERC721EnumerableImpl = + ERC721EnumerableComponent::ERC721EnumerableImpl; + impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc721: ERC721Component::Storage, + #[substorage(v0)] + pub erc721_enumerable: ERC721EnumerableComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + ERC721EnumerableEvent: ERC721EnumerableComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait { + fn before_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) { + let mut contract_state = ERC721Component::HasComponent::get_contract_mut(ref self); + contract_state.erc721_enumerable.before_update(to, token_id); + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + base_uri: ByteArray, + recipient: ContractAddress, + token_id: u256 + ) { + self.erc721.initializer(name, symbol, base_uri); + self.erc721_enumerable.initializer(); + self.erc721.mint(recipient, token_id); + } +} diff --git a/packages/test_common/src/mocks/eth_account.cairo b/packages/test_common/src/mocks/eth_account.cairo deleted file mode 100644 index ebe4c1a5d..000000000 --- a/packages/test_common/src/mocks/eth_account.cairo +++ /dev/null @@ -1,43 +0,0 @@ -#[starknet::contract(account)] -pub mod DualCaseEthAccountMock { - use openzeppelin_account::EthAccountComponent; - use openzeppelin_account::interface::EthPublicKey; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC6Impl = EthAccountComponent::SRC6Impl; - #[abi(embed_v0)] - impl SRC6CamelOnlyImpl = EthAccountComponent::SRC6CamelOnlyImpl; - #[abi(embed_v0)] - impl DeclarerImpl = EthAccountComponent::DeclarerImpl; - #[abi(embed_v0)] - impl DeployableImpl = EthAccountComponent::DeployableImpl; - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub eth_account: EthAccountComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - EthAccountEvent: EthAccountComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState, public_key: EthPublicKey) { - self.eth_account.initializer(public_key); - } -} diff --git a/packages/utils/src/tests/mocks/nonces_mocks.cairo b/packages/test_common/src/mocks/nonces.cairo similarity index 85% rename from packages/utils/src/tests/mocks/nonces_mocks.cairo rename to packages/test_common/src/mocks/nonces.cairo index b2acf4cd5..92365d717 100644 --- a/packages/utils/src/tests/mocks/nonces_mocks.cairo +++ b/packages/test_common/src/mocks/nonces.cairo @@ -1,6 +1,6 @@ #[starknet::contract] -pub(crate) mod NoncesMock { - use crate::cryptography::nonces::NoncesComponent; +pub mod NoncesMock { + use openzeppelin_utils::cryptography::nonces::NoncesComponent; component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); diff --git a/packages/security/src/tests/mocks/reentrancy_mocks.cairo b/packages/test_common/src/mocks/security.cairo similarity index 71% rename from packages/security/src/tests/mocks/reentrancy_mocks.cairo rename to packages/test_common/src/mocks/security.cairo index c270d8d69..cac6c3acc 100644 --- a/packages/security/src/tests/mocks/reentrancy_mocks.cairo +++ b/packages/test_common/src/mocks/security.cairo @@ -1,4 +1,51 @@ use starknet::ContractAddress; +#[starknet::contract] +pub mod InitializableMock { + use openzeppelin_security::initializable::InitializableComponent; + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + + #[abi(embed_v0)] + impl InitializableImpl = + InitializableComponent::InitializableImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub initializable: InitializableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event + } +} + +#[starknet::contract] +pub mod PausableMock { + use openzeppelin_security::pausable::PausableComponent; + + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl InternalImpl = PausableComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub pausable: PausableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + PausableEvent: PausableComponent::Event + } +} #[starknet::interface] trait IReentrancyGuarded { @@ -6,7 +53,7 @@ trait IReentrancyGuarded { } #[starknet::interface] -pub(crate) trait IReentrancyMock { +pub trait IReentrancyMock { fn count(ref self: TState); fn current_count(self: @TState) -> felt252; fn callback(ref self: TState); @@ -16,8 +63,8 @@ pub(crate) trait IReentrancyMock { } #[starknet::contract] -pub(crate) mod ReentrancyMock { - use crate::reentrancyguard::ReentrancyGuardComponent; +pub mod ReentrancyMock { + use openzeppelin_security::reentrancyguard::ReentrancyGuardComponent; use starknet::ContractAddress; use starknet::get_contract_address; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -107,7 +154,7 @@ trait IAttacker { } #[starknet::contract] -pub(crate) mod Attacker { +pub mod Attacker { use starknet::ContractAddress; use starknet::get_caller_address; use super::IReentrancyMockDispatcher; diff --git a/packages/presets/src/tests/mocks/src5_mocks.cairo b/packages/test_common/src/mocks/src5.cairo similarity index 94% rename from packages/presets/src/tests/mocks/src5_mocks.cairo rename to packages/test_common/src/mocks/src5.cairo index df481ced5..0d20d0a40 100644 --- a/packages/presets/src/tests/mocks/src5_mocks.cairo +++ b/packages/test_common/src/mocks/src5.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub(crate) mod SRC5Mock { +pub mod SRC5Mock { use openzeppelin_introspection::src5::SRC5Component; component!(path: SRC5Component, storage: src5, event: SRC5Event); diff --git a/packages/governance/src/tests/mocks/timelock_mocks.cairo b/packages/test_common/src/mocks/timelock.cairo similarity index 92% rename from packages/governance/src/tests/mocks/timelock_mocks.cairo rename to packages/test_common/src/mocks/timelock.cairo index 68359db97..974e7e2b8 100644 --- a/packages/governance/src/tests/mocks/timelock_mocks.cairo +++ b/packages/test_common/src/mocks/timelock.cairo @@ -1,7 +1,7 @@ #[starknet::contract] -pub(crate) mod TimelockControllerMock { - use crate::timelock::TimelockControllerComponent; +pub mod TimelockControllerMock { use openzeppelin_access::accesscontrol::AccessControlComponent; + use openzeppelin_governance::timelock::TimelockControllerComponent; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; @@ -49,14 +49,14 @@ pub(crate) mod TimelockControllerMock { } #[starknet::interface] -pub(crate) trait IMockContract { +pub trait IMockContract { fn set_number(ref self: TState, new_number: felt252); fn get_number(self: @TState) -> felt252; fn failing_function(self: @TState); } #[starknet::contract] -pub(crate) mod MockContract { +pub mod MockContract { use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use super::IMockContract; @@ -82,14 +82,16 @@ pub(crate) mod MockContract { } #[starknet::interface] -pub(crate) trait ITimelockAttacker { +pub trait ITimelockAttacker { fn reenter(ref self: TState); fn reenter_batch(ref self: TState); } #[starknet::contract] -pub(crate) mod TimelockAttackerMock { - use crate::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; +pub mod TimelockAttackerMock { + use openzeppelin_governance::timelock::interface::{ + ITimelockDispatcher, ITimelockDispatcherTrait + }; use starknet::account::Call; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use super::ITimelockAttacker; diff --git a/packages/upgrades/src/tests/mocks/upgrades_mocks.cairo b/packages/test_common/src/mocks/upgrades.cairo similarity index 88% rename from packages/upgrades/src/tests/mocks/upgrades_mocks.cairo rename to packages/test_common/src/mocks/upgrades.cairo index d46e66681..7cd2e87f8 100644 --- a/packages/upgrades/src/tests/mocks/upgrades_mocks.cairo +++ b/packages/test_common/src/mocks/upgrades.cairo @@ -1,11 +1,7 @@ -// These contracts are mocks used to test the core functionality of the upgrade functions. -// The functions are NOT PROTECTED. -// DO NOT USE IN PRODUCTION. - use starknet::ClassHash; #[starknet::interface] -pub(crate) trait IUpgradesV1 { +pub trait IUpgradesV1 { fn upgrade(ref self: TState, new_class_hash: ClassHash); fn set_value(ref self: TState, val: felt252); fn get_value(self: @TState) -> felt252; @@ -13,8 +9,8 @@ pub(crate) trait IUpgradesV1 { } #[starknet::contract] -pub(crate) mod UpgradesV1 { - use crate::UpgradeableComponent; +pub mod UpgradesV1 { + use openzeppelin_upgrades::UpgradeableComponent; use starknet::ClassHash; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -55,7 +51,7 @@ pub(crate) mod UpgradesV1 { } #[starknet::interface] -pub(crate) trait IUpgradesV2 { +pub trait IUpgradesV2 { fn upgrade(ref self: TState, new_class_hash: ClassHash); fn set_value(ref self: TState, val: felt252); fn set_value2(ref self: TState, val: felt252); @@ -64,8 +60,8 @@ pub(crate) trait IUpgradesV2 { } #[starknet::contract] -pub(crate) mod UpgradesV2 { - use crate::UpgradeableComponent; +pub mod UpgradesV2 { + use openzeppelin_upgrades::UpgradeableComponent; use starknet::ClassHash; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; diff --git a/packages/finance/src/tests/mocks/vesting_mocks.cairo b/packages/test_common/src/mocks/vesting.cairo similarity index 59% rename from packages/finance/src/tests/mocks/vesting_mocks.cairo rename to packages/test_common/src/mocks/vesting.cairo index 9fb938459..ae1489cfb 100644 --- a/packages/finance/src/tests/mocks/vesting_mocks.cairo +++ b/packages/test_common/src/mocks/vesting.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub(crate) mod LinearVestingMock { +pub mod LinearVestingMock { use openzeppelin_access::ownable::OwnableComponent; use openzeppelin_finance::vesting::{VestingComponent, LinearVestingSchedule}; use starknet::ContractAddress; @@ -48,7 +48,7 @@ pub(crate) mod LinearVestingMock { } #[starknet::contract] -pub(crate) mod StepsVestingMock { +pub mod StepsVestingMock { use openzeppelin_access::ownable::OwnableComponent; use openzeppelin_finance::vesting::VestingComponent::VestingScheduleTrait; use openzeppelin_finance::vesting::VestingComponent; @@ -125,3 +125,95 @@ pub(crate) mod StepsVestingMock { } } } + +#[starknet::contract] +pub mod ERC20OptionalTransferPanicMock { + use openzeppelin_token::erc20::interface::IERC20; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + transfer_should_fail: bool, + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[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); + } + + #[abi(embed_v0)] + impl ERC20Impl of IERC20 { + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + if self.transfer_should_fail.read() { + false + } else { + self.erc20.transfer(recipient, amount) + } + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + if self.transfer_should_fail.read() { + false + } else { + self.erc20.transfer_from(sender, recipient, amount) + } + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc20.approve(spender, amount) + } + } + + #[generate_trait] + #[abi(per_item)] + impl ExternalImpl of ExternalTrait { + #[external(v0)] + fn set_transfer_should_fail(ref self: ContractState, should_fail: bool) { + self.transfer_should_fail.write(should_fail); + } + } +} diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 158519166..a25227518 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -41,3 +41,10 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::account::DualCaseAccountMock", + "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", + "openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock", + "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", + "openzeppelin_test_common::mocks::src5::SRC5Mock", +] \ No newline at end of file diff --git a/packages/token/src/common/erc2981/erc2981.cairo b/packages/token/src/common/erc2981/erc2981.cairo index 42ef33c36..2355e50b8 100644 --- a/packages/token/src/common/erc2981/erc2981.cairo +++ b/packages/token/src/common/erc2981/erc2981.cairo @@ -247,7 +247,7 @@ pub impl DefaultConfig of ERC2981Component::ImmutableConfig { #[cfg(test)] mod tests { - use crate::tests::mocks::erc2981_mocks::ERC2981Mock; + use openzeppelin_test_common::mocks::erc2981::ERC2981Mock; use starknet::contract_address_const; use super::ERC2981Component::InternalImpl; use super::ERC2981Component; diff --git a/packages/token/src/lib.cairo b/packages/token/src/lib.cairo index e158d0e7d..43b3de05d 100644 --- a/packages/token/src/lib.cairo +++ b/packages/token/src/lib.cairo @@ -3,4 +3,5 @@ pub mod erc1155; pub mod erc20; pub mod erc721; +#[cfg(test)] pub mod tests; diff --git a/packages/token/src/tests.cairo b/packages/token/src/tests.cairo index d37791a03..fdb72f5ef 100644 --- a/packages/token/src/tests.cairo +++ b/packages/token/src/tests.cairo @@ -1,10 +1,4 @@ -#[cfg(test)] pub mod erc1155; -#[cfg(test)] pub mod erc20; -#[cfg(test)] pub mod erc2981; -#[cfg(test)] pub mod erc721; - -pub(crate) mod mocks; diff --git a/packages/token/src/tests/erc1155/test_erc1155.cairo b/packages/token/src/tests/erc1155/test_erc1155.cairo index ffdc620d3..f1bad9234 100644 --- a/packages/token/src/tests/erc1155/test_erc1155.cairo +++ b/packages/token/src/tests/erc1155/test_erc1155.cairo @@ -3,7 +3,6 @@ use crate::erc1155::ERC1155Component::ERC1155CamelImpl; use crate::erc1155::ERC1155Component::{ERC1155Impl, ERC1155MetadataURIImpl, InternalImpl}; use crate::erc1155::ERC1155Component; use crate::erc1155; -use crate::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::erc1155::{ ERC1155SpyHelpers, get_ids_and_values, get_ids_and_split_values @@ -11,6 +10,7 @@ use openzeppelin_test_common::erc1155::{ use openzeppelin_test_common::erc1155::{ setup_account, deploy_another_account_at, setup_src5, setup_receiver }; +use openzeppelin_test_common::mocks::erc1155::DualCaseERC1155Mock; use openzeppelin_testing::constants::{ EMPTY_DATA, ZERO, OWNER, RECIPIENT, OPERATOR, OTHER, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 diff --git a/packages/token/src/tests/erc1155/test_erc1155_receiver.cairo b/packages/token/src/tests/erc1155/test_erc1155_receiver.cairo index e79debe9b..c3154f4ea 100644 --- a/packages/token/src/tests/erc1155/test_erc1155_receiver.cairo +++ b/packages/token/src/tests/erc1155/test_erc1155_receiver.cairo @@ -2,9 +2,9 @@ use crate::erc1155::ERC1155ReceiverComponent::{ ERC1155ReceiverImpl, ERC1155ReceiverCamelImpl, InternalImpl }; use crate::erc1155::interface::IERC1155_RECEIVER_ID; -use crate::tests::mocks::erc1155_receiver_mocks::DualCaseERC1155ReceiverMock; use openzeppelin_introspection::interface::ISRC5_ID; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock; use openzeppelin_testing::constants::{OWNER, OPERATOR, TOKEN_ID, TOKEN_VALUE, EMPTY_DATA}; fn STATE() -> DualCaseERC1155ReceiverMock::ContractState { diff --git a/packages/token/src/tests/erc20/test_erc20.cairo b/packages/token/src/tests/erc20/test_erc20.cairo index f7b6f32ab..1d8c4451a 100644 --- a/packages/token/src/tests/erc20/test_erc20.cairo +++ b/packages/token/src/tests/erc20/test_erc20.cairo @@ -2,8 +2,8 @@ use core::num::traits::Bounded; use crate::erc20::ERC20Component::{ERC20CamelOnlyImpl, ERC20Impl}; use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl}; use crate::erc20::ERC20Component; -use crate::tests::mocks::erc20_mocks::DualCaseERC20Mock; use openzeppelin_test_common::erc20::ERC20SpyHelpers; +use openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock; use openzeppelin_testing::constants::{ ZERO, OWNER, SPENDER, RECIPIENT, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE }; diff --git a/packages/token/src/tests/erc20/test_erc20_votes.cairo b/packages/token/src/tests/erc20/test_erc20_votes.cairo index dbff4693e..6a2be7240 100644 --- a/packages/token/src/tests/erc20/test_erc20_votes.cairo +++ b/packages/token/src/tests/erc20/test_erc20_votes.cairo @@ -5,8 +5,8 @@ use crate::erc20::extensions::ERC20VotesComponent::{DelegateChanged, DelegateVot use crate::erc20::extensions::ERC20VotesComponent::{ERC20VotesImpl, InternalImpl}; use crate::erc20::extensions::ERC20VotesComponent; use crate::erc20::extensions::erc20_votes::Delegation; -use crate::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock::SNIP12MetadataImpl; -use crate::tests::mocks::erc20_votes_mocks::DualCaseERC20VotesMock; +use openzeppelin_test_common::mocks::erc20::DualCaseERC20VotesMock::SNIP12MetadataImpl; +use openzeppelin_test_common::mocks::erc20::DualCaseERC20VotesMock; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{SUPPLY, ZERO, OWNER, RECIPIENT}; use openzeppelin_testing::events::EventSpyExt; diff --git a/packages/token/src/tests/erc2981/test_erc2981.cairo b/packages/token/src/tests/erc2981/test_erc2981.cairo index 3d2dc623f..1b0dbb75c 100644 --- a/packages/token/src/tests/erc2981/test_erc2981.cairo +++ b/packages/token/src/tests/erc2981/test_erc2981.cairo @@ -1,8 +1,8 @@ use crate::common::erc2981::ERC2981Component::{ERC2981Impl, InternalImpl}; use crate::common::erc2981::interface::IERC2981_ID; use crate::common::erc2981::{ERC2981Component, DefaultConfig}; -use crate::tests::mocks::erc2981_mocks::ERC2981Mock; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc2981::ERC2981Mock; use openzeppelin_testing::constants::{ZERO, RECIPIENT}; use starknet::{ContractAddress, contract_address_const}; diff --git a/packages/token/src/tests/erc721/test_erc721.cairo b/packages/token/src/tests/erc721/test_erc721.cairo index d28f9d189..03e9826f2 100644 --- a/packages/token/src/tests/erc721/test_erc721.cairo +++ b/packages/token/src/tests/erc721/test_erc721.cairo @@ -3,9 +3,9 @@ use crate::erc721::ERC721Component::{ERC721Impl, ERC721CamelOnlyImpl}; use crate::erc721::ERC721Component::{ERC721MetadataImpl, InternalImpl}; use crate::erc721::ERC721Component; use crate::erc721; -use crate::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::erc721::ERC721SpyHelpers; +use openzeppelin_test_common::mocks::erc721::DualCaseERC721Mock; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{ DATA, ZERO, OWNER, CALLER, RECIPIENT, SPENDER, OPERATOR, OTHER, NAME, SYMBOL, TOKEN_ID, diff --git a/packages/token/src/tests/erc721/test_erc721_enumerable.cairo b/packages/token/src/tests/erc721/test_erc721_enumerable.cairo index a57589c6b..1c60b5695 100644 --- a/packages/token/src/tests/erc721/test_erc721_enumerable.cairo +++ b/packages/token/src/tests/erc721/test_erc721_enumerable.cairo @@ -4,9 +4,9 @@ use crate::erc721::extensions::erc721_enumerable::ERC721EnumerableComponent::{ }; use crate::erc721::extensions::erc721_enumerable::ERC721EnumerableComponent; use crate::erc721::extensions::erc721_enumerable::interface; -use crate::tests::mocks::erc721_enumerable_mocks::ERC721EnumerableMock; use openzeppelin_introspection::interface::ISRC5_ID; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc721::ERC721EnumerableMock; use openzeppelin_testing::constants::{OWNER, RECIPIENT, OTHER, ZERO}; use starknet::ContractAddress; use starknet::storage::StorageMapReadAccess; diff --git a/packages/token/src/tests/erc721/test_erc721_receiver.cairo b/packages/token/src/tests/erc721/test_erc721_receiver.cairo index aa4fe8276..c24285744 100644 --- a/packages/token/src/tests/erc721/test_erc721_receiver.cairo +++ b/packages/token/src/tests/erc721/test_erc721_receiver.cairo @@ -2,9 +2,9 @@ use crate::erc721::ERC721ReceiverComponent::{ ERC721ReceiverImpl, ERC721ReceiverCamelImpl, InternalImpl }; use crate::erc721::interface::IERC721_RECEIVER_ID; -use crate::tests::mocks::erc721_receiver_mocks::DualCaseERC721ReceiverMock; use openzeppelin_introspection::interface::ISRC5_ID; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock; use openzeppelin_testing::constants::{OWNER, OPERATOR, TOKEN_ID}; fn STATE() -> DualCaseERC721ReceiverMock::ContractState { diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo deleted file mode 100644 index 35ad0586e..000000000 --- a/packages/token/src/tests/mocks.cairo +++ /dev/null @@ -1,10 +0,0 @@ -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; -pub(crate) mod erc721_receiver_mocks; -pub(crate) mod src5_mocks; diff --git a/packages/token/src/tests/mocks/account_mocks.cairo b/packages/token/src/tests/mocks/account_mocks.cairo deleted file mode 100644 index e09f53328..000000000 --- a/packages/token/src/tests/mocks/account_mocks.cairo +++ /dev/null @@ -1,45 +0,0 @@ -#[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); - } -} diff --git a/packages/token/src/tests/mocks/erc1155_mocks.cairo b/packages/token/src/tests/mocks/erc1155_mocks.cairo deleted file mode 100644 index a7442f8c0..000000000 --- a/packages/token/src/tests/mocks/erc1155_mocks.cairo +++ /dev/null @@ -1,52 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC1155Mock { - use crate::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC1155 - #[abi(embed_v0)] - impl ERC1155Impl = ERC1155Component::ERC1155Impl; - #[abi(embed_v0)] - impl ERC1155MetadataURIImpl = - ERC1155Component::ERC1155MetadataURIImpl; - #[abi(embed_v0)] - impl ERC721Camel = ERC1155Component::ERC1155CamelImpl; - impl ERC1155InternalImpl = ERC1155Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155: ERC1155Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155Event: ERC1155Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256, - value: u256 - ) { - self.erc1155.initializer(base_uri); - self.erc1155.mint_with_acceptance_check(recipient, token_id, value, array![].span()); - } -} diff --git a/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo b/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo deleted file mode 100644 index 06443f5f5..000000000 --- a/packages/token/src/tests/mocks/erc1155_receiver_mocks.cairo +++ /dev/null @@ -1,40 +0,0 @@ -const SUCCESS: felt252 = 'SUCCESS'; - -#[starknet::contract] -pub(crate) mod DualCaseERC1155ReceiverMock { - use crate::erc1155::ERC1155ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - component!( - path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent - ); - - // ERC1155Receiver Mixin - #[abi(embed_v0)] - impl ERC1155ReceiverMixinImpl = - ERC1155ReceiverComponent::ERC1155ReceiverMixinImpl; - impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc1155_receiver: ERC1155ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc1155_receiver.initializer(); - } -} diff --git a/packages/token/src/tests/mocks/erc20_mocks.cairo b/packages/token/src/tests/mocks/erc20_mocks.cairo deleted file mode 100644 index d6b0a799b..000000000 --- a/packages/token/src/tests/mocks/erc20_mocks.cairo +++ /dev/null @@ -1,40 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC20Mock { - use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use starknet::ContractAddress; - - component!(path: ERC20Component, storage: erc20, event: ERC20Event); - - #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - #[abi(embed_v0)] - impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; - impl InternalImpl = ERC20Component::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc20: ERC20Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC20Event: ERC20Component::Event - } - - #[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/tests/mocks/erc721_enumerable_mocks.cairo b/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo deleted file mode 100644 index 096dea067..000000000 --- a/packages/token/src/tests/mocks/erc721_enumerable_mocks.cairo +++ /dev/null @@ -1,75 +0,0 @@ -#[starknet::contract] -pub(crate) mod ERC721EnumerableMock { - use crate::erc721::ERC721Component; - use crate::erc721::extensions::ERC721EnumerableComponent; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!( - path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent - ); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - #[abi(embed_v0)] - impl ERC721MixinImpl = ERC721Component::ERC721Impl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // ERC721Enumerable - #[abi(embed_v0)] - impl ERC721EnumerableImpl = - ERC721EnumerableComponent::ERC721EnumerableImpl; - impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub erc721_enumerable: ERC721EnumerableComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - ERC721EnumerableEvent: ERC721EnumerableComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait { - fn before_update( - ref self: ERC721Component::ComponentState, - to: ContractAddress, - token_id: u256, - auth: ContractAddress - ) { - let mut contract_state = ERC721Component::HasComponent::get_contract_mut(ref self); - contract_state.erc721_enumerable.before_update(to, token_id); - } - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721_enumerable.initializer(); - self.erc721.mint(recipient, token_id); - } -} diff --git a/packages/token/src/tests/mocks/erc721_mocks.cairo b/packages/token/src/tests/mocks/erc721_mocks.cairo deleted file mode 100644 index 432c72da9..000000000 --- a/packages/token/src/tests/mocks/erc721_mocks.cairo +++ /dev/null @@ -1,55 +0,0 @@ -#[starknet::contract] -pub(crate) mod DualCaseERC721Mock { - use crate::erc721::{ERC721Component, ERC721HooksEmptyImpl}; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721Component, storage: erc721, event: ERC721Event); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721 - #[abi(embed_v0)] - impl ERC721Impl = ERC721Component::ERC721Impl; - #[abi(embed_v0)] - impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; - #[abi(embed_v0)] - impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; - #[abi(embed_v0)] - impl ERC721MetadataCamelOnly = - ERC721Component::ERC721MetadataCamelOnlyImpl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721: ERC721Component::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721Event: ERC721Component::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - base_uri: ByteArray, - recipient: ContractAddress, - token_id: u256 - ) { - self.erc721.initializer(name, symbol, base_uri); - self.erc721.mint(recipient, token_id); - } -} diff --git a/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo b/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo deleted file mode 100644 index 6df306fbd..000000000 --- a/packages/token/src/tests/mocks/erc721_receiver_mocks.cairo +++ /dev/null @@ -1,71 +0,0 @@ -const SUCCESS: felt252 = 'SUCCESS'; - -#[starknet::contract] -pub(crate) mod DualCaseERC721ReceiverMock { - use crate::erc721::ERC721ReceiverComponent; - use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - // ERC721Receiver - impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // SRC5 - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - SRC5Event: SRC5Component::Event - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.erc721_receiver.initializer(); - } - - #[abi(per_item)] - #[generate_trait] - impl ExternalImpl of ExternalTrait { - #[external(v0)] - fn on_erc721_received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - if *data.at(0) == super::SUCCESS { - self.erc721_receiver.on_erc721_received(operator, from, token_id, data) - } else { - 0 - } - } - - #[external(v0)] - fn onERC721Received( - self: @ContractState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252 { - Self::on_erc721_received(self, operator, from, tokenId, data) - } - } -} diff --git a/packages/token/src/tests/mocks/src5_mocks.cairo b/packages/token/src/tests/mocks/src5_mocks.cairo deleted file mode 100644 index df481ced5..000000000 --- a/packages/token/src/tests/mocks/src5_mocks.cairo +++ /dev/null @@ -1,22 +0,0 @@ -#[starknet::contract] -pub(crate) mod SRC5Mock { - use openzeppelin_introspection::src5::SRC5Component; - - component!(path: SRC5Component, storage: src5, event: SRC5Event); - - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub src5: SRC5Component::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - SRC5Event: SRC5Component::Event - } -} diff --git a/packages/upgrades/Scarb.toml b/packages/upgrades/Scarb.toml index a059c95c0..1a0786c84 100644 --- a/packages/upgrades/Scarb.toml +++ b/packages/upgrades/Scarb.toml @@ -36,3 +36,7 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false +build-external-contracts = [ + "openzeppelin_test_common::mocks::upgrades::UpgradesV1", + "openzeppelin_test_common::mocks::upgrades::UpgradesV2", +] diff --git a/packages/upgrades/src/lib.cairo b/packages/upgrades/src/lib.cairo index 1f6920e8c..bd960aff0 100644 --- a/packages/upgrades/src/lib.cairo +++ b/packages/upgrades/src/lib.cairo @@ -1,5 +1,6 @@ pub mod interface; +#[cfg(test)] pub mod tests; pub mod upgradeable; diff --git a/packages/upgrades/src/tests.cairo b/packages/upgrades/src/tests.cairo index 617be2f0e..fe66e01e3 100644 --- a/packages/upgrades/src/tests.cairo +++ b/packages/upgrades/src/tests.cairo @@ -1,4 +1 @@ -pub(crate) mod mocks; - -#[cfg(test)] mod test_upgradeable; diff --git a/packages/upgrades/src/tests/mocks.cairo b/packages/upgrades/src/tests/mocks.cairo deleted file mode 100644 index a287b9e8f..000000000 --- a/packages/upgrades/src/tests/mocks.cairo +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod upgrades_mocks; diff --git a/packages/upgrades/src/tests/test_upgradeable.cairo b/packages/upgrades/src/tests/test_upgradeable.cairo index 1cb956261..19a87be4f 100644 --- a/packages/upgrades/src/tests/test_upgradeable.cairo +++ b/packages/upgrades/src/tests/test_upgradeable.cairo @@ -1,5 +1,5 @@ -use crate::tests::mocks::upgrades_mocks::{IUpgradesV1Dispatcher, IUpgradesV1DispatcherTrait}; -use crate::tests::mocks::upgrades_mocks::{IUpgradesV2Dispatcher, IUpgradesV2DispatcherTrait}; +use openzeppelin_test_common::mocks::upgrades::{IUpgradesV1Dispatcher, IUpgradesV1DispatcherTrait}; +use openzeppelin_test_common::mocks::upgrades::{IUpgradesV2Dispatcher, IUpgradesV2DispatcherTrait}; use openzeppelin_test_common::upgrades::UpgradeableSpyHelpers; use openzeppelin_testing::constants::{CLASS_HASH_ZERO, FELT_VALUE as VALUE}; use openzeppelin_testing::{declare_class, deploy}; diff --git a/packages/utils/Scarb.toml b/packages/utils/Scarb.toml index ac9b18f1f..0454fc82d 100644 --- a/packages/utils/Scarb.toml +++ b/packages/utils/Scarb.toml @@ -29,6 +29,7 @@ starknet.workspace = true assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } +openzeppelin_test_common = { path = "../test_common" } [lib] diff --git a/packages/utils/src/tests.cairo b/packages/utils/src/tests.cairo index 37cf414d3..fedcc380c 100644 --- a/packages/utils/src/tests.cairo +++ b/packages/utils/src/tests.cairo @@ -1,4 +1,2 @@ -pub(crate) mod mocks; - mod test_nonces; mod test_snip12; diff --git a/packages/utils/src/tests/test_nonces.cairo b/packages/utils/src/tests/test_nonces.cairo index 694908aa9..fdc783035 100644 --- a/packages/utils/src/tests/test_nonces.cairo +++ b/packages/utils/src/tests/test_nonces.cairo @@ -2,7 +2,7 @@ use core::num::traits::Zero; use crate::cryptography::interface::INonces; use crate::cryptography::nonces::NoncesComponent::InternalTrait; use crate::cryptography::nonces::NoncesComponent; -use crate::tests::mocks::nonces_mocks::NoncesMock; +use openzeppelin_test_common::mocks::nonces::NoncesMock; use openzeppelin_testing::constants::OWNER; type ComponentState = NoncesComponent::ComponentState; From 72451fac2abc654e9a40ae628a4ad9a26d146268 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 4 Oct 2024 17:53:34 -0400 Subject: [PATCH 16/76] feat: remove mocks from release target --- packages/account/Scarb.toml | 2 ++ packages/finance/Scarb.toml | 2 ++ packages/governance/Scarb.toml | 4 ++-- packages/presets/Scarb.toml | 2 ++ packages/security/Scarb.toml | 2 ++ packages/token/Scarb.toml | 4 +++- packages/upgrades/Scarb.toml | 2 ++ 7 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index ac52a50c4..722e1ee5e 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -38,6 +38,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::account::DualCaseEthAccountMock", diff --git a/packages/finance/Scarb.toml b/packages/finance/Scarb.toml index 1df83c51f..a80e942cf 100644 --- a/packages/finance/Scarb.toml +++ b/packages/finance/Scarb.toml @@ -37,6 +37,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::vesting::LinearVestingMock", "openzeppelin_test_common::mocks::vesting::StepsVestingMock", diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index ce04ec985..e0bc89a1c 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -38,10 +38,10 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::timelock::TimelockControllerMock", "openzeppelin_test_common::mocks::timelock::MockContract", "openzeppelin_test_common::mocks::timelock::TimelockAttackerMock", ] - - diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index ec39916d6..9e6a4dbdb 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -42,6 +42,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::account::SnakeAccountMock", diff --git a/packages/security/Scarb.toml b/packages/security/Scarb.toml index 86ebefe15..ea4242943 100644 --- a/packages/security/Scarb.toml +++ b/packages/security/Scarb.toml @@ -36,6 +36,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::security::ReentrancyMock", "openzeppelin_test_common::mocks::security::Attacker", diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index a25227518..5a97b179c 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -41,10 +41,12 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", "openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock", "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", "openzeppelin_test_common::mocks::src5::SRC5Mock", -] \ No newline at end of file +] diff --git a/packages/upgrades/Scarb.toml b/packages/upgrades/Scarb.toml index 1a0786c84..9553796d7 100644 --- a/packages/upgrades/Scarb.toml +++ b/packages/upgrades/Scarb.toml @@ -36,6 +36,8 @@ openzeppelin_test_common = { path = "../test_common" } allowed-libfuncs-list.name = "experimental" sierra = true casm = false + +[[test]] build-external-contracts = [ "openzeppelin_test_common::mocks::upgrades::UpgradesV1", "openzeppelin_test_common::mocks::upgrades::UpgradesV2", From f575ba81c24b83e98703f38a654bcd6fa519a44b Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Sun, 6 Oct 2024 19:59:57 -0400 Subject: [PATCH 17/76] fix: mock --- packages/account/src/extensions/src9/src9.cairo | 1 - packages/test_common/src/mocks/vesting.cairo | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/account/src/extensions/src9/src9.cairo b/packages/account/src/extensions/src9/src9.cairo index d9e4ee2ed..468e1475f 100644 --- a/packages/account/src/extensions/src9/src9.cairo +++ b/packages/account/src/extensions/src9/src9.cairo @@ -10,7 +10,6 @@ /// interface. #[starknet::component] pub mod SRC9Component { - use crate::dual_account::{DualCaseAccount, DualCaseAccountTrait}; use crate::extensions::src9::OutsideExecution; use crate::extensions::src9::interface; use crate::extensions::src9::snip12_utils::OutsideExecutionStructHash; diff --git a/packages/test_common/src/mocks/vesting.cairo b/packages/test_common/src/mocks/vesting.cairo index 2cfc22942..ae1489cfb 100644 --- a/packages/test_common/src/mocks/vesting.cairo +++ b/packages/test_common/src/mocks/vesting.cairo @@ -1,7 +1,7 @@ #[starknet::contract] pub mod LinearVestingMock { - use crate::vesting::{VestingComponent, LinearVestingSchedule}; use openzeppelin_access::ownable::OwnableComponent; + use openzeppelin_finance::vesting::{VestingComponent, LinearVestingSchedule}; use starknet::ContractAddress; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -49,9 +49,9 @@ pub mod LinearVestingMock { #[starknet::contract] pub mod StepsVestingMock { - use crate::vesting::VestingComponent::VestingScheduleTrait; - use crate::vesting::VestingComponent; use openzeppelin_access::ownable::OwnableComponent; + use openzeppelin_finance::vesting::VestingComponent::VestingScheduleTrait; + use openzeppelin_finance::vesting::VestingComponent; use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; From 1c5c522c72ff3bf8ddc26f2c3a15088c81cfaf7a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 7 Oct 2024 13:25:51 +0200 Subject: [PATCH 18/76] fix: imports --- packages/finance/src/tests/test_vesting_linear.cairo | 2 +- packages/finance/src/tests/test_vesting_steps.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/finance/src/tests/test_vesting_linear.cairo b/packages/finance/src/tests/test_vesting_linear.cairo index 8ed5ecf4b..fc4045c8a 100644 --- a/packages/finance/src/tests/test_vesting_linear.cairo +++ b/packages/finance/src/tests/test_vesting_linear.cairo @@ -1,9 +1,9 @@ use crate::tests::common::{VestingStrategy, TestData, setup, set_transfer_to_fail}; -use crate::tests::mocks::vesting_mocks::LinearVestingMock; use crate::vesting::VestingComponent::InternalImpl; use crate::vesting::VestingComponent; use crate::vesting::interface::IVestingDispatcherTrait; use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin_test_common::mocks::vesting::LinearVestingMock; use openzeppelin_test_common::vesting::VestingSpyHelpers; use openzeppelin_testing::constants::{OWNER, OTHER}; use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/packages/finance/src/tests/test_vesting_steps.cairo b/packages/finance/src/tests/test_vesting_steps.cairo index 9c71eb043..a1ec4cbfc 100644 --- a/packages/finance/src/tests/test_vesting_steps.cairo +++ b/packages/finance/src/tests/test_vesting_steps.cairo @@ -1,9 +1,9 @@ use crate::tests::common::{VestingStrategy, TestData, setup, set_transfer_to_fail}; -use crate::tests::mocks::vesting_mocks::StepsVestingMock; use crate::vesting::VestingComponent::InternalImpl; use crate::vesting::VestingComponent; use crate::vesting::interface::IVestingDispatcherTrait; use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin_test_common::mocks::vesting::StepsVestingMock; use openzeppelin_test_common::vesting::VestingSpyHelpers; use openzeppelin_testing::constants::{OWNER, OTHER}; use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; From f14f635597af93712b9a0ff24193d362fe065bd6 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 7 Oct 2024 23:15:33 +0200 Subject: [PATCH 19/76] feat: bumo scarb and remove assert_macros from manifest --- CHANGELOG.md | 2 +- Scarb.toml | 8 +++----- packages/access/Scarb.toml | 1 - packages/account/Scarb.toml | 1 - packages/introspection/Scarb.toml | 1 - packages/merkle_tree/Scarb.toml | 1 - packages/presets/Scarb.toml | 1 - packages/security/Scarb.toml | 1 - packages/test_common/Scarb.toml | 1 - packages/testing/Scarb.toml | 1 - packages/token/Scarb.toml | 1 - packages/upgrades/Scarb.toml | 1 - packages/utils/Scarb.toml | 1 - 13 files changed, 4 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d79d469..b35b45f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump scarb to v2.8.3 (#1166) +- Bump scarb to v2.8.4 (#1146) ### Changed (Breaking) diff --git a/Scarb.toml b/Scarb.toml index 50c76abd1..1edaaa5b2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -24,8 +24,8 @@ keywords.workspace = true [workspace.package] version = "0.17.0" edition = "2024_07" -cairo-version = "2.8.2" -scarb-version = "2.8.3" +cairo-version = "2.8.4" +scarb-version = "2.8.4" authors = ["OpenZeppelin Community "] description = "OpenZeppelin Contracts written in Cairo for Starknet, a decentralized ZK Rollup" documentation = "https://docs.openzeppelin.com/contracts-cairo" @@ -40,8 +40,7 @@ keywords = [ ] [workspace.dependencies] -assert_macros = "2.8.2" -starknet = "2.8.2" +starknet = "2.8.4" snforge_std = "0.31.0" [dependencies] @@ -59,7 +58,6 @@ openzeppelin_upgrades = { path = "packages/upgrades" } openzeppelin_utils = { path = "packages/utils" } [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_test_common = { path = "packages/test_common" } openzeppelin_testing = { path = "packages/testing" } diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 0f29ee37b..6e031bf4e 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -27,7 +27,6 @@ openzeppelin_introspection = { path = "../introspection" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index 722e1ee5e..c85a34728 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -27,7 +27,6 @@ openzeppelin_introspection = { path = "../introspection" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/introspection/Scarb.toml b/packages/introspection/Scarb.toml index 32049773b..eddf48b03 100644 --- a/packages/introspection/Scarb.toml +++ b/packages/introspection/Scarb.toml @@ -26,7 +26,6 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/merkle_tree/Scarb.toml b/packages/merkle_tree/Scarb.toml index f9b50d14f..c0b0317d0 100644 --- a/packages/merkle_tree/Scarb.toml +++ b/packages/merkle_tree/Scarb.toml @@ -20,6 +20,5 @@ license-file.workspace = true fmt.workspace = true [dev-dependencies] -assert_macros.workspace = true starknet.workspace = true snforge_std.workspace = true diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index 9e6a4dbdb..b45e1f057 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -31,7 +31,6 @@ openzeppelin_upgrades = { path = "../upgrades" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/security/Scarb.toml b/packages/security/Scarb.toml index ea4242943..5963a6bdf 100644 --- a/packages/security/Scarb.toml +++ b/packages/security/Scarb.toml @@ -25,7 +25,6 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/test_common/Scarb.toml b/packages/test_common/Scarb.toml index 6f5f54d22..1d488b357 100644 --- a/packages/test_common/Scarb.toml +++ b/packages/test_common/Scarb.toml @@ -16,7 +16,6 @@ keywords.workspace = true fmt.workspace = true [dependencies] -assert_macros.workspace = true starknet.workspace = true snforge_std.workspace = true openzeppelin_upgrades = { path = "../upgrades" } diff --git a/packages/testing/Scarb.toml b/packages/testing/Scarb.toml index de27ed1e8..01451e4c4 100644 --- a/packages/testing/Scarb.toml +++ b/packages/testing/Scarb.toml @@ -21,7 +21,6 @@ license-file.workspace = true fmt.workspace = true [dependencies] -assert_macros.workspace = true starknet.workspace = true snforge_std.workspace = true diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 5a97b179c..722ca2431 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -30,7 +30,6 @@ openzeppelin_governance = { path = "../governance" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/upgrades/Scarb.toml b/packages/upgrades/Scarb.toml index 9553796d7..cd995ce6e 100644 --- a/packages/upgrades/Scarb.toml +++ b/packages/upgrades/Scarb.toml @@ -25,7 +25,6 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/utils/Scarb.toml b/packages/utils/Scarb.toml index 0454fc82d..ce54e739d 100644 --- a/packages/utils/Scarb.toml +++ b/packages/utils/Scarb.toml @@ -26,7 +26,6 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] -assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } From 8a763e0629dcf1b818438d67d44907c4c6c99110 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 7 Oct 2024 23:22:11 +0200 Subject: [PATCH 20/76] feat: re-add assert_macros --- Scarb.toml | 1 + packages/access/Scarb.toml | 1 + packages/account/Scarb.toml | 1 + packages/introspection/Scarb.toml | 1 + packages/merkle_tree/Scarb.toml | 1 + packages/presets/Scarb.toml | 1 + packages/security/Scarb.toml | 1 + packages/testing/Scarb.toml | 1 + packages/token/Scarb.toml | 1 + packages/upgrades/Scarb.toml | 1 + packages/utils/Scarb.toml | 1 + 11 files changed, 11 insertions(+) diff --git a/Scarb.toml b/Scarb.toml index 1edaaa5b2..8106757db 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -40,6 +40,7 @@ keywords = [ ] [workspace.dependencies] +assert_macros = "2.8.4" starknet = "2.8.4" snforge_std = "0.31.0" diff --git a/packages/access/Scarb.toml b/packages/access/Scarb.toml index 6e031bf4e..0f29ee37b 100644 --- a/packages/access/Scarb.toml +++ b/packages/access/Scarb.toml @@ -27,6 +27,7 @@ openzeppelin_introspection = { path = "../introspection" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index c85a34728..722e1ee5e 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -27,6 +27,7 @@ openzeppelin_introspection = { path = "../introspection" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/introspection/Scarb.toml b/packages/introspection/Scarb.toml index eddf48b03..32049773b 100644 --- a/packages/introspection/Scarb.toml +++ b/packages/introspection/Scarb.toml @@ -26,6 +26,7 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/merkle_tree/Scarb.toml b/packages/merkle_tree/Scarb.toml index c0b0317d0..f9b50d14f 100644 --- a/packages/merkle_tree/Scarb.toml +++ b/packages/merkle_tree/Scarb.toml @@ -20,5 +20,6 @@ license-file.workspace = true fmt.workspace = true [dev-dependencies] +assert_macros.workspace = true starknet.workspace = true snforge_std.workspace = true diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index b45e1f057..9e6a4dbdb 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -31,6 +31,7 @@ openzeppelin_upgrades = { path = "../upgrades" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/security/Scarb.toml b/packages/security/Scarb.toml index 5963a6bdf..ea4242943 100644 --- a/packages/security/Scarb.toml +++ b/packages/security/Scarb.toml @@ -25,6 +25,7 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/testing/Scarb.toml b/packages/testing/Scarb.toml index 01451e4c4..de27ed1e8 100644 --- a/packages/testing/Scarb.toml +++ b/packages/testing/Scarb.toml @@ -21,6 +21,7 @@ license-file.workspace = true fmt.workspace = true [dependencies] +assert_macros.workspace = true starknet.workspace = true snforge_std.workspace = true diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 722ca2431..5a97b179c 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -30,6 +30,7 @@ openzeppelin_governance = { path = "../governance" } openzeppelin_utils = { path = "../utils" } [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/upgrades/Scarb.toml b/packages/upgrades/Scarb.toml index cd995ce6e..9553796d7 100644 --- a/packages/upgrades/Scarb.toml +++ b/packages/upgrades/Scarb.toml @@ -25,6 +25,7 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } diff --git a/packages/utils/Scarb.toml b/packages/utils/Scarb.toml index ce54e739d..0454fc82d 100644 --- a/packages/utils/Scarb.toml +++ b/packages/utils/Scarb.toml @@ -26,6 +26,7 @@ fmt.workspace = true starknet.workspace = true [dev-dependencies] +assert_macros.workspace = true snforge_std.workspace = true openzeppelin_testing = { path = "../testing" } openzeppelin_test_common = { path = "../test_common" } From 46fd0ac6afc0e60235e748b2cf96dcf397fa895b Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 8 Oct 2024 11:55:32 +0200 Subject: [PATCH 21/76] feat: add test target names --- packages/account/Scarb.toml | 1 + packages/finance/Scarb.toml | 1 + packages/governance/Scarb.toml | 1 + packages/presets/Scarb.toml | 1 + packages/security/Scarb.toml | 1 + packages/token/Scarb.toml | 1 + packages/upgrades/Scarb.toml | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/account/Scarb.toml b/packages/account/Scarb.toml index 722e1ee5e..f6d5645f5 100644 --- a/packages/account/Scarb.toml +++ b/packages/account/Scarb.toml @@ -40,6 +40,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_account_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::account::DualCaseEthAccountMock", diff --git a/packages/finance/Scarb.toml b/packages/finance/Scarb.toml index a80e942cf..6b357bdce 100644 --- a/packages/finance/Scarb.toml +++ b/packages/finance/Scarb.toml @@ -39,6 +39,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_finance_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::vesting::LinearVestingMock", "openzeppelin_test_common::mocks::vesting::StepsVestingMock", diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index 7a70d71ee..c35180b44 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -41,6 +41,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_governance_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::timelock::TimelockControllerMock", "openzeppelin_test_common::mocks::timelock::MockContract", diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index 9e6a4dbdb..7ed4414f4 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -44,6 +44,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_presets_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::account::SnakeAccountMock", diff --git a/packages/security/Scarb.toml b/packages/security/Scarb.toml index ea4242943..37d03ab2f 100644 --- a/packages/security/Scarb.toml +++ b/packages/security/Scarb.toml @@ -38,6 +38,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_security_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::security::ReentrancyMock", "openzeppelin_test_common::mocks::security::Attacker", diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 5a97b179c..92e74c40b 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -43,6 +43,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_token_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", diff --git a/packages/upgrades/Scarb.toml b/packages/upgrades/Scarb.toml index 9553796d7..7e253af2d 100644 --- a/packages/upgrades/Scarb.toml +++ b/packages/upgrades/Scarb.toml @@ -38,6 +38,7 @@ sierra = true casm = false [[test]] +name = "openzeppelin_upgrades_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::upgrades::UpgradesV1", "openzeppelin_test_common::mocks::upgrades::UpgradesV2", From 182f470b7b6207c179a46e6f1b23430b662ab8a5 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 8 Oct 2024 12:27:20 +0200 Subject: [PATCH 22/76] docs: update index --- docs/modules/ROOT/pages/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 59e49dd23..9c673c5c8 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -21,8 +21,8 @@ before proceeding, and run the following command to check that the installation ---- $ scarb --version -scarb 2.8.3 (54938ce3b 2024-09-26) -cairo: 2.8.2 (https://crates.io/crates/cairo-lang-compiler/2.8.2) +scarb 2.8.4 (2aa4e193e 2024-10-07) +cairo: 2.8.4 (https://crates.io/crates/cairo-lang-compiler/2.8.4) sierra: 1.6.0 ---- From 6d68bd0468f0a2a895cdeee6342a48922894ce4c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 11 Oct 2024 11:11:20 +0200 Subject: [PATCH 23/76] feat: add interface --- packages/governance/src/governor.cairo | 2 + .../governance/src/governor/governor.cairo | 1 + .../governance/src/governor/interface.cairo | 198 ++++++++++++++++++ packages/governance/src/lib.cairo | 2 + 4 files changed, 203 insertions(+) create mode 100644 packages/governance/src/governor.cairo create mode 100644 packages/governance/src/governor/governor.cairo create mode 100644 packages/governance/src/governor/interface.cairo diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo new file mode 100644 index 000000000..fa802d903 --- /dev/null +++ b/packages/governance/src/governor.cairo @@ -0,0 +1,2 @@ +pub mod governor; +pub mod interface; diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/governance/src/governor/governor.cairo @@ -0,0 +1 @@ + diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo new file mode 100644 index 000000000..4339ce9f3 --- /dev/null +++ b/packages/governance/src/governor/interface.cairo @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/interface.cairo) + +use starknet::ContractAddress; +use starknet::account::Call; + +#[starknet::interface] +pub trait IERC6372 { + /// Clock used for flagging checkpoints. + /// Can be overridden to implement timestamp based checkpoints (and voting). + fn clock(self: @TState) -> u64; + + /// Description of the clock. + /// See https://eips.ethereum.org/EIPS/eip-6372#clock_mode + fn CLOCK_MODE(self: @TState) -> ByteArray; +} + +#[derive(Copy, Drop, Serde)] +enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed +} + +#[starknet::interface] +pub trait IGovernor { + /// Name of the governor instance (used in building the SNIP-12 domain separator). + fn name(self: @TState) -> felt252; + + /// Version of the governor instance (used in building SNIP-12 domain separator). + fn version(self: @TState) -> felt252; + + /// A description of the possible `support` values for `cast_vote` and the way these votes are + /// counted, meant to be consumed by UIs to show correct vote options and interpret the results. + /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for + /// example `support=bravo&quorum=for,abstain`. + /// + /// There are 2 standard keys: `support` and `quorum`. + /// + /// - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in + /// `GovernorBravo`. + /// - `quorum=bravo` means that only For votes are counted towards quorum. + /// - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. + /// + /// If a counting module makes use of encoded `params`, it should include this under a `params` + /// key with a unique name that describes the behavior. For example: + /// + /// - `params=fractional` might refer to a scheme where votes are divided fractionally between + /// for/against/abstain. + /// - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. + /// + /// NOTE: The string can be decoded by the standard + /// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] + /// JavaScript class. + fn COUNTING_MODE(self: @TState) -> ByteArray; + + /// Hashing function used to (re)build the proposal id from the proposal details. + fn hash_proposal(self: @TState, calls: Span, description_hash: felt252) -> felt252; + + /// Returns the state of a proposal, given its id. + fn state(self: @TState, proposal_id: felt252) -> ProposalState; + + /// The number of votes required in order for a voter to become a proposer. + fn proposal_threshold(self: @TState, proposal_id: felt252) -> u256; + + /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot is + /// performed at the end of this block. Hence, voting for this proposal starts at the beginning + /// of the following block. + fn proposal_snapshot(self: @TState, proposal_id: felt252) -> u64; + + /// Timepoint at which votes close. If using block number, votes close at the end of this block, + /// so it is possible to cast a vote during this block. + fn proposal_deadline(self: @TState, proposal_id: felt252) -> u64; + + /// The account that created a proposal. + fn proposal_proposer(self: @TState, proposal_id: felt252) -> ContractAddress; + + /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` and + /// `proposal_deadline`, this doesn't use the governor clock, and instead relies on the + /// executor's clock which may be different. In most cases this will be a timestamp. + fn proposal_eta(self: @TState, proposal_id: felt252) -> u64; + + /// Whether a proposal needs to be queued before execution. + fn proposal_needs_queuing(self: @TState, proposal_id: felt252) -> bool; + + /// Delay between the proposal is created and the vote starts. The unit this duration is + /// expressed in depends on the clock (see ERC-6372) this contract uses. + /// + /// This can be increased to leave time for users to buy voting power, or delegate it, before + /// the voting of a proposal starts. + fn voting_delay(self: @TState) -> u64; + + /// Delay between the vote start and vote end. The unit this duration is expressed in depends on + /// the clock (see ERC-6372) this contract uses. + /// + /// NOTE: The `voting_delay` can delay the start of the vote. This must be considered when + /// setting the voting duration compared to the voting delay. + /// + /// NOTE: This value is stored when the proposal is submitted so that possible changes to the + /// value do not affect proposals that have already been submitted. + fn voting_period(self: @TState) -> u64; + + /// Minimum number of cast voted required for a proposal to be successful. + /// + /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This + /// allows to scale the quorum depending on values such as the total supply of a token at this + /// timepoint. + fn quorum(self: @TState, timepoint: u64) -> u256; + + /// Voting power of an `account` at a specific `timepoint`. + /// + /// NOTE: this can be implemented in a number of ways, for example by reading the delegated + /// balance from one (or multiple) `ERC20Votes` tokens. + fn get_votes(self: @TState, account: ContractAddress, timepoint: u64) -> u256; + + /// Voting power of an `account` at a specific `timepoint` given additional encoded parameters. + fn get_votes_with_params( + self: @TState, account: ContractAddress, timepoint: u64, params: Span + ) -> u256; + + /// Returns whether `account` has cast a vote on `proposal_id`. + fn has_voted(self: @TState, proposal_id: felt252, account: ContractAddress) -> bool; + + /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and + /// lasts for a duration specified by `voting_period`. + /// + /// NOTE: The state of the Governor and `targets` may change between the proposal creation and + /// its execution. This may be the result of third party actions on the targeted contracts, or + /// other governor proposals. For example, the balance of this contract could be updated or its + /// access control permissions may be modified, possibly compromising the proposal's ability to + /// execute successfully (e.g. the governor doesn't have enough value to cover a proposal with + /// multiple transfers). + /// + /// Returns the id of the proposal. + fn propose(self: @TState, calls: Span, description: ByteArray) -> felt252; + + /// Queue a proposal. Some governors require this step to be performed before execution can + /// happen. If queuing is not necessary, this function may revert. + /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the + /// deadline to be reached. + /// + /// Returns the id of the proposal. + fn queue(self: @TState, calls: Span, description_hash: felt252) -> felt252; + + /// Execute a successful proposal. This requires the quorum to be reached, the vote to be + /// successful, and the deadline to be reached. Depending on the governor it might also be + /// required that the proposal was queued and that some delay passed. + /// + /// NOTE: Some modules can modify the requirements for execution, for example by adding an + /// additional timelock (See `timelock_controller`). + /// + /// Returns the id of the proposal. + fn execute(self: @TState, calls: Span, description_hash: felt252) -> felt252; + + /// Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending + /// state, i.e. before the vote starts. + /// + /// Returns the id of the proposal. + fn cancel(self: @TState, calls: Span, description_hash: felt252) -> felt252; + + /// Cast a vote. + fn cast_vote(self: @TState, proposal_id: felt252, support: u8) -> u256; + + /// Cast a vote with a reason. + fn cast_vote_with_reason( + self: @TState, proposal_id: felt252, support: u8, reason: ByteArray + ) -> u256; + + /// Cast a vote with a reason and additional serialized parameters. + fn cast_vote_with_reason_and_params( + self: @TState, proposal_id: felt252, support: u8, reason: ByteArray, params: Span + ) -> u256; + + /// Cast a vote using the voter's signature. + fn cast_vote_by_sig( + self: @TState, + proposal_id: felt252, + support: u8, + voter: ContractAddress, + signature: Span + ) -> u256; + + /// Cast a vote with a reason and additional serialized parameters using the voter's signature + fn cast_vote_with_reason_and_params_by_sig( + self: @TState, + proposal_id: felt252, + support: u8, + voter: ContractAddress, + reason: ByteArray, + params: Span, + signature: Span + ) -> u256; +} diff --git a/packages/governance/src/lib.cairo b/packages/governance/src/lib.cairo index 1320484fb..767e84a23 100644 --- a/packages/governance/src/lib.cairo +++ b/packages/governance/src/lib.cairo @@ -1,3 +1,5 @@ +pub mod governor; + #[cfg(test)] mod tests; From 7117e98b9776970daf11c8b2c3c795eff8974a86 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 11 Oct 2024 13:36:28 +0200 Subject: [PATCH 24/76] feat: add double ended queue struct --- CHANGELOG.md | 4 + .../governance/src/governor/governor.cairo | 7 ++ packages/utils/src/structs.cairo | 1 + .../src/structs/double_ended_queue.cairo | 93 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 packages/utils/src/structs/double_ended_queue.cairo diff --git a/CHANGELOG.md b/CHANGELOG.md index b35b45f1e..623b387f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- DoubleEndedQueue struct to `openzeppelin_utils` (#) + ### Changed - Bump scarb to v2.8.4 (#1146) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 8b1378917..2c97434ad 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -1 +1,8 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/governor.cairo) +/// # Governor Component +/// +/// Core of the governance system. +#[starknet::component] +pub mod GovernorComponent {} diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 3b3d8eaa6..09aeddd65 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -1,2 +1,3 @@ pub mod checkpoint; +pub mod double_ended_queue; pub mod storage_array; diff --git a/packages/utils/src/structs/double_ended_queue.cairo b/packages/utils/src/structs/double_ended_queue.cairo new file mode 100644 index 000000000..3b018cdfb --- /dev/null +++ b/packages/utils/src/structs/double_ended_queue.cairo @@ -0,0 +1,93 @@ +use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; +use starknet::storage::{StoragePath, Mutable}; +use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + +/// A sequence with the ability to efficiently push and pop items (i.e. insert and remove) on both +/// ends of the sequence (called front and back). Among other access patterns, it can be used to +/// implement efficient LIFO and FIFO queues. All operations are O(1) constant time. This includes +/// ´clear´, given that the existing queue contents are left in storage. +/// +/// NOTE: This is a phantom type which can only be used in storage, and members are only accessible +/// through the ´DoubleEndedQueueTrait´ implementation. +#[starknet::storage_node] +pub struct DoubleEndedQueue { + pub(crate) _begin: felt252, + pub(crate) _end: felt252, + pub(crate) _data: Map +} + +pub mod DoubleEndedQueueErrors { + pub const FULL_QUEUE: felt252 = 'Queue: full queue'; + pub const EMPTY_QUEUE: felt252 = 'Queue: empty queue'; +} + +#[generate_trait] +pub impl DoubleEndedQueueImpl of DoubleEndedQueueTrait { + /// Inserts an item at the end of the queue. + fn push_back(self: StoragePath>, value: felt252) { + let back_index = self._end.read(); + assert(back_index + 1 != self._begin.read(), DoubleEndedQueueErrors::FULL_QUEUE); + self._data.write(back_index, value); + self._end.write(back_index + 1); + } + + /// Removes the item at the end of the queue and returns it. + fn pop_back(self: StoragePath>) -> felt252 { + let mut back_index = self._end.read(); + assert(back_index != self._begin.read(), DoubleEndedQueueErrors::EMPTY_QUEUE); + back_index -= 1; + let value = self._data.read(back_index); + self._data.write(back_index, 0); + self._end.write(back_index); + value + } + + /// Inserts an item at the beginning of the queue. + fn push_front(self: StoragePath>, value: felt252) { + let front_index = self._begin.read() - 1; + assert(front_index != self._end.read(), DoubleEndedQueueErrors::FULL_QUEUE); + self._data.write(front_index, value); + self._begin.write(front_index); + } + + /// Removes the item at the beginning of the queue and returns it. + fn pop_front(self: StoragePath>) -> felt252 { + let front_index = self._begin.read(); + assert(front_index != self._end.read(), DoubleEndedQueueErrors::EMPTY_QUEUE); + let value = self._data.read(front_index); + self._data.write(front_index, 0); + self._begin.write(front_index + 1); + value + } + + /// Resets the queue back to being empty. + /// + /// NOTE: The current items are left behind in storage. This does not affect the functioning + /// of the queue. + fn clear(self: StoragePath>) { + self._begin.write(0); + self._end.write(0); + } + + /// Returns the item at the beginning of the queue. + fn front(self: StoragePath) -> felt252 { + assert(!self.is_empty(), DoubleEndedQueueErrors::EMPTY_QUEUE); + self._data.read(self._begin.read()) + } + + /// Returns the item at the end of the queue. + fn back(self: StoragePath) -> felt252 { + assert(!self.is_empty(), DoubleEndedQueueErrors::EMPTY_QUEUE); + self._data.read(self._end.read() - 1) + } + + /// Returns the number of items in the queue. + fn len(self: StoragePath) -> felt252 { + self._end.read() - self._begin.read() + } + + /// Returns true if the queue is empty. + fn is_empty(self: StoragePath) -> bool { + self._begin.read() == self._end.read() + } +} From 5eb6f2886b8776ec8e02826412481ec70b7a1e8f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 11 Oct 2024 14:38:03 +0200 Subject: [PATCH 25/76] feat: add proposal core --- packages/governance/src/governor.cairo | 4 + .../governance/src/governor/governor.cairo | 7 +- .../src/governor/proposal_core.cairo | 79 +++++++++++++++++++ packages/token/src/erc1155/erc1155.cairo | 6 +- packages/utils/src/structs/checkpoint.cairo | 18 +++-- 5 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 packages/governance/src/governor/proposal_core.cairo diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo index fa802d903..317bad4cd 100644 --- a/packages/governance/src/governor.cairo +++ b/packages/governance/src/governor.cairo @@ -1,2 +1,6 @@ pub mod governor; pub mod interface; +pub mod proposal_core; + +pub use governor::GovernorComponent; +pub use proposal_core::ProposalCore; \ No newline at end of file diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 2c97434ad..1761078b6 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -5,4 +5,9 @@ /// /// Core of the governance system. #[starknet::component] -pub mod GovernorComponent {} +pub mod GovernorComponent { + use crate::governor::ProposalCore; + + #[storage] + pub struct Storage {} +} diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo new file mode 100644 index 000000000..150b6b84e --- /dev/null +++ b/packages/governance/src/governor/proposal_core.cairo @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/proposal_core.cairo) + +use starknet::ContractAddress; +use starknet::storage_access::StorePacking; + +#[derive(Copy, Drop, Serde)] +pub struct ProposalCore { + proposer: ContractAddress, + vote_start: u64, + vote_duration: u64, + executed: bool, + canceled: bool, + eta_seconds: u64 +} + +const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; +const _2_POW_120: felt252 = 0x1000000000000000000000000000000; +const _2_POW_119: felt252 = 0x800000000000000000000000000000; +const _2_POW_118: felt252 = 0x400000000000000000000000000000; +const _2_POW_54: felt252 = 0x40000000000000; + +const _64_BITS_MASK: u256 = 0xffffffffffffffff; +const _1_BIT_MASK: u256 = 0x1; + +/// Packs a ProposalCore into a (felt252, felt252). +/// +/// The packing is done as follows: +/// +/// - The first felt of the tuple contains `proposer` serialized. +/// - The second felt of the tuple contains `vote_start`, `vote_duration`, `executed`, `canceled` +/// and `eta_seconds` organized as presented next. +/// - `vote_start` is stored at range [4,67] bits (0-indexed), taking the most significant usable +/// bits. +/// - `vote_duration` is stored at range [68, 131], following `vote_start`. +/// - `executed` is stored at range [132, 132], following `vote_duration`. +/// - `canceled` is stored at range [133, 133], following `executed`. +/// - `eta_seconds` is stored at range [134, 197], following `canceled`. +/// +/// NOTE: In the second felt252, the first four bits are skipped to avoid representation errors due +/// to `felt252` max value being a bit less than a 252 bits number max value +/// (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/). +impl ProposalCoreStorePacking of StorePacking { + fn pack(value: ProposalCore) -> (felt252, felt252) { + let proposal = value; + + // shift-left to reach the corresponding positions + let vote_start = proposal.vote_start.into() * _2_POW_184; + let vote_duration = proposal.vote_duration.into() * _2_POW_120; + let executed = proposal.executed.into() * _2_POW_119; + let canceled = proposal.canceled.into() * _2_POW_118; + let eta_seconds = proposal.eta_seconds.into() * _2_POW_54; + + let second_felt = vote_start + vote_duration + executed + canceled + eta_seconds; + + (proposal.proposer.into(), second_felt) + } + + fn unpack(value: (felt252, felt252)) -> ProposalCore { + let (proposer, second_felt) = value; + let second_felt: u256 = second_felt.into(); + + // shift-right and mask to extract the corresponding values + let vote_start: u256 = (second_felt / _2_POW_184.into()) & _64_BITS_MASK; + let vote_duration: u256 = (second_felt / _2_POW_120.into()) & _64_BITS_MASK; + let executed: u256 = (second_felt / _2_POW_119.into()) & _1_BIT_MASK; + let canceled: u256 = (second_felt / _2_POW_118.into()) & _1_BIT_MASK; + let eta_seconds: u256 = (second_felt / _2_POW_54.into()) & _64_BITS_MASK; + + ProposalCore { + proposer: proposer.try_into().unwrap(), + vote_start: vote_start.try_into().unwrap(), + vote_duration: vote_duration.try_into().unwrap(), + executed: executed > 0, + canceled: canceled > 0, + eta_seconds: eta_seconds.try_into().unwrap() + } + } +} diff --git a/packages/token/src/erc1155/erc1155.cairo b/packages/token/src/erc1155/erc1155.cairo index 84dc32580..d1e3ebbc6 100644 --- a/packages/token/src/erc1155/erc1155.cairo +++ b/packages/token/src/erc1155/erc1155.cairo @@ -181,8 +181,7 @@ pub mod ERC1155Component { /// - `from` is not the zero address. /// - `to` is not the zero address. /// - If `to` refers to a non-account contract, it must implement - /// `IERC1155Receiver::on_ERC1155_received` - /// and return the required magic value. + /// `IERC1155Receiver::on_ERC1155_received` and return the required magic value. /// /// Emits a `TransferSingle` event. fn safe_transfer_from( @@ -213,8 +212,7 @@ pub mod ERC1155Component { /// - `to` is not the zero address. /// - `token_ids` and `values` must have the same length. /// - If `to` refers to a non-account contract, it must implement - /// `IERC1155Receiver::on_ERC1155_batch_received` - /// and return the acceptance magic value. + /// `IERC1155Receiver::on_ERC1155_batch_received` and return the acceptance magic value. /// /// Emits either a `TransferSingle` or a `TransferBatch` event, depending on the length of /// the array arguments. diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo index f4e2e1b55..93b42ea72 100644 --- a/packages/utils/src/structs/checkpoint.cairo +++ b/packages/utils/src/structs/checkpoint.cairo @@ -160,21 +160,25 @@ impl CheckpointImpl of CheckpointTrait { } const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; +const _128_BITS_MASK: u256 = 0xffffffffffffffffffffffffffffffff; /// Packs a Checkpoint into a (felt252, felt252). /// /// The packing is done as follows: +/// /// - The first felt of the tuple contains `key` and `value.low`. -/// - In this first felt, the first four bits are skipped to avoid representation errors due -/// to `felt252` max value being a bit less than a 252 bits number max value -/// (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/). /// - `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. +/// +/// NOTE: In this first felt, the first four bits are skipped to avoid representation errors due +/// to `felt252` max value being a bit less than a 252 bits number max value +/// (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/). impl CheckpointStorePacking of StorePacking { fn pack(value: Checkpoint) -> (felt252, felt252) { let checkpoint = value; - // shift-left by 184 bits + + // shift-left to reach the corresponding position let key = checkpoint.key.into() * _2_POW_184; let key_and_low = key + checkpoint.value.low.into(); @@ -183,11 +187,11 @@ impl CheckpointStorePacking of StorePacking { fn unpack(value: (felt252, felt252)) -> Checkpoint { let (key_and_low, high) = value; - let key_and_low: u256 = key_and_low.into(); - // shift-right by 184 bits + + // shift-right and mask to extract the corresponding values let key: u256 = key_and_low / _2_POW_184.into(); - let low = key_and_low & 0xffffffffffffffffffffffffffffffff; + let low = key_and_low & _128_BITS_MASK; Checkpoint { key: key.try_into().unwrap(), From 8b35ed13045fc190c5f72e61761d0ea52b4fea83 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 16 Oct 2024 10:05:12 +0200 Subject: [PATCH 26/76] + --- .../governance/src/governor/governor.cairo | 43 ++++++++++++++++++- packages/utils/src/structs.cairo | 3 ++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 1761078b6..3b3f5a2b6 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -7,7 +7,48 @@ #[starknet::component] pub mod GovernorComponent { use crate::governor::ProposalCore; + use openzeppelin_utils::structs::DoubleEndedQueue; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] - pub struct Storage {} + pub struct Storage { + proposals: Map, + governance_call: DoubleEndedQueue + } + + mod Errors { + pub const INVALID_ROYALTY: felt252 = 'ERC2981: invalid royalty'; + } + + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// - `FEE_DENOMINATOR`: The denominator with which to interpret the fee set in + /// `set_token_royalty` and `set_default_royalty` as a fraction of the sale price. + /// + /// Requirements: + /// + /// - `FEE_DENOMINATOR` must be greater than 0. + pub trait ImmutableConfig { + const FEE_DENOMINATOR: u128; + + fn validate() { + assert(Self::FEE_DENOMINATOR > 0, Errors::INVALID_FEE_DENOMINATOR); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +Drop + > of InternalTrait { + fn assert_only_governance(ref self: ComponentState) { + + } + + fn executor(self: @ComponentState) -> ContractAddress { + starknet::get_contract_address() + } + } } diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 09aeddd65..40c9e11cf 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -1,3 +1,6 @@ pub mod checkpoint; pub mod double_ended_queue; pub mod storage_array; + +pub use checkpoint::{Trace, Checkpoint}; +pub use double_ended_queue::DoubleEndedQueue; \ No newline at end of file From bf74d7f5ea3176a876971b24278871a604075b96 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 16 Oct 2024 12:23:44 +0200 Subject: [PATCH 27/76] feat: add extension traits --- .../src/accesscontrol/accesscontrol.cairo | 1 - packages/governance/src/governor.cairo | 1 + .../governance/src/governor/governor.cairo | 83 ++++++++++++++----- .../governance/src/governor/interface.cairo | 2 + 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/packages/access/src/accesscontrol/accesscontrol.cairo b/packages/access/src/accesscontrol/accesscontrol.cairo index d634918a3..8e238afe9 100644 --- a/packages/access/src/accesscontrol/accesscontrol.cairo +++ b/packages/access/src/accesscontrol/accesscontrol.cairo @@ -9,7 +9,6 @@ pub mod AccessControlComponent { use crate::accesscontrol::interface; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; - use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; use starknet::get_caller_address; diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo index 59f61c256..3b9e99fac 100644 --- a/packages/governance/src/governor.cairo +++ b/packages/governance/src/governor.cairo @@ -1,3 +1,4 @@ +pub mod extensions; pub mod governor; pub mod interface; pub mod proposal_core; diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 3b3f5a2b6..f1905b76b 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -7,8 +7,15 @@ #[starknet::component] pub mod GovernorComponent { use crate::governor::ProposalCore; + use crate::governor::interface::IGOVERNOR_ID; use openzeppelin_utils::structs::DoubleEndedQueue; - use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; + use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; + use starknet::ContractAddress; + use starknet::storage::Map; + use starknet::account::Call; #[storage] pub struct Storage { @@ -17,38 +24,74 @@ pub mod GovernorComponent { } mod Errors { - pub const INVALID_ROYALTY: felt252 = 'ERC2981: invalid royalty'; + pub const EXECUTOR_ONLY: felt252 = 'Governor: executor only'; } - /// Constants expected to be defined at the contract level used to configure the component - /// behaviour. - /// - /// - `FEE_DENOMINATOR`: The denominator with which to interpret the fee set in - /// `set_token_royalty` and `set_default_royalty` as a fraction of the sale price. - /// - /// Requirements: - /// - /// - `FEE_DENOMINATOR` must be greater than 0. - pub trait ImmutableConfig { - const FEE_DENOMINATOR: u128; - - fn validate() { - assert(Self::FEE_DENOMINATOR > 0, Errors::INVALID_FEE_DENOMINATOR); - } + // + // Extensions + // + + pub trait GovernorCountingTrait { + fn COUNTING_MODE() -> ByteArray; + fn has_voted(proposal_id: felt252, account: ContractAddress) -> bool; + fn quorum_reached(proposal_id: felt252) -> bool; + fn vote_succeeded(proposal_id: felt252) -> bool; + fn count_vote( + proposal_id: felt252, + account: ContractAddress, + support: u8, + total_weight: u256, + params: Span + ) -> u256; + } + + pub trait GovernorVotesTrait { + fn clock() -> u64; + fn CLOCK_MODE() -> ByteArray; + fn get_votes(account: ContractAddress, timepoint: u64, params: Span) -> u256; + } + + pub trait GovernorExecutorTrait { + fn executor(self: @ComponentState) -> ContractAddress; } + // + // Internal + // + #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, + +GovernorExecutorTrait, + impl SRC5: SRC5Component::HasComponent, +Drop > of InternalTrait { + /// Initializes the contract by registering the supported interface Ids. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(IGOVERNOR_ID); + src5_component.register_interface(IERC721_RECEIVER_ID); + src5_component.register_interface(IERC1155_RECEIVER_ID); + } + fn assert_only_governance(ref self: ComponentState) { - + let executor = self.executor(); + assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY); + + // TODO: either check that the calldata matches the whitelist or assume the Executor + // can't execute proposals not created from the Governor itself. + () } - fn executor(self: @ComponentState) -> ContractAddress { - starknet::get_contract_address() + /// Hashing function used to (re)build the proposal id from the proposal details. + fn hash_proposal( + ref self: ComponentState, + calls: Span, description_hash: felt252 + ) -> felt252 { + let proposal_hash = proposal.get_hash(); + let proposal_id = starknet::hash(&proposal_hash); + proposal_id } } } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 4339ce9f3..0355292e1 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -4,6 +4,8 @@ use starknet::ContractAddress; use starknet::account::Call; +pub const IGOVERNOR_ID: felt252 = 0x1f; // TODO: Update this value. + #[starknet::interface] pub trait IERC6372 { /// Clock used for flagging checkpoints. From 154503632e9d788599327cf5d78d2a24bd18b487 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 16 Oct 2024 12:46:30 +0200 Subject: [PATCH 28/76] feat: add hash_proposal --- .../src/accesscontrol/accesscontrol.cairo | 1 + .../governance/src/governor/governor.cairo | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/access/src/accesscontrol/accesscontrol.cairo b/packages/access/src/accesscontrol/accesscontrol.cairo index 8e238afe9..d634918a3 100644 --- a/packages/access/src/accesscontrol/accesscontrol.cairo +++ b/packages/access/src/accesscontrol/accesscontrol.cairo @@ -9,6 +9,7 @@ pub mod AccessControlComponent { use crate::accesscontrol::interface; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; + use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_introspection::src5::SRC5Component; use starknet::ContractAddress; use starknet::get_caller_address; diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index f1905b76b..a940e28b0 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -6,16 +6,18 @@ /// Core of the governance system. #[starknet::component] pub mod GovernorComponent { + use core::hash::{HashStateTrait, HashStateExTrait}; + use core::poseidon::{PoseidonTrait, poseidon_hash_span}; use crate::governor::ProposalCore; use crate::governor::interface::IGOVERNOR_ID; - use openzeppelin_utils::structs::DoubleEndedQueue; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; + use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; + use openzeppelin_utils::structs::DoubleEndedQueue; use starknet::ContractAddress; - use starknet::storage::Map; use starknet::account::Call; + use starknet::storage::Map; #[storage] pub struct Storage { @@ -86,12 +88,23 @@ pub mod GovernorComponent { /// Hashing function used to (re)build the proposal id from the proposal details. fn hash_proposal( - ref self: ComponentState, - calls: Span, description_hash: felt252 + ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { - let proposal_hash = proposal.get_hash(); - let proposal_id = starknet::hash(&proposal_hash); - proposal_id + let mut hashed_calls = array![]; + + for call in calls { + let hash_state = PoseidonTrait::new(); + let hash = hash_state + .update_with(*call.to) + .update_with(*call.selector) + .update_with(poseidon_hash_span(*call.calldata)) + .finalize(); + + hashed_calls.append(hash); + }; + hashed_calls.append(description_hash); + + poseidon_hash_span(hashed_calls.span()) } } } From 14f5792f463a9473ce737c8c09ebad87dfcdf29c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 16 Oct 2024 13:08:07 +0200 Subject: [PATCH 29/76] feat: add state internal function --- .../governance/src/governor/governor.cairo | 92 ++++++++++++++++--- .../governance/src/governor/interface.cairo | 2 +- .../src/governor/proposal_core.cairo | 12 +-- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index a940e28b0..6d12fd9ec 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -7,9 +7,10 @@ #[starknet::component] pub mod GovernorComponent { use core::hash::{HashStateTrait, HashStateExTrait}; + use core::num::traits::Zero; use core::poseidon::{PoseidonTrait, poseidon_hash_span}; use crate::governor::ProposalCore; - use crate::governor::interface::IGOVERNOR_ID; + use crate::governor::interface::{ProposalState, IGOVERNOR_ID}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; @@ -17,7 +18,7 @@ pub mod GovernorComponent { use openzeppelin_utils::structs::DoubleEndedQueue; use starknet::ContractAddress; use starknet::account::Call; - use starknet::storage::Map; + use starknet::storage::{Map, StorageMapReadAccess}; #[storage] pub struct Storage { @@ -27,18 +28,20 @@ pub mod GovernorComponent { mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Governor: executor only'; + pub const NONEXISTENT_PROPOSAL: felt252 = 'Governor: nonexistent proposal'; } // // Extensions // - pub trait GovernorCountingTrait { - fn COUNTING_MODE() -> ByteArray; - fn has_voted(proposal_id: felt252, account: ContractAddress) -> bool; - fn quorum_reached(proposal_id: felt252) -> bool; - fn vote_succeeded(proposal_id: felt252) -> bool; + pub trait GovernorCountingTrait { + fn COUNTING_MODE(self: @ComponentState) -> ByteArray; + fn has_voted(self: @ComponentState, proposal_id: felt252, account: ContractAddress) -> bool; + fn quorum_reached(self: @ComponentState, proposal_id: felt252) -> bool; + fn vote_succeeded(self: @ComponentState, proposal_id: felt252) -> bool; fn count_vote( + ref self: ComponentState, proposal_id: felt252, account: ContractAddress, support: u8, @@ -47,16 +50,21 @@ pub mod GovernorComponent { ) -> u256; } - pub trait GovernorVotesTrait { - fn clock() -> u64; - fn CLOCK_MODE() -> ByteArray; - fn get_votes(account: ContractAddress, timepoint: u64, params: Span) -> u256; - } - pub trait GovernorExecutorTrait { fn executor(self: @ComponentState) -> ContractAddress; } + pub trait GovernorVotesTrait { + fn clock(self: @ComponentState) -> u64; + fn CLOCK_MODE(self: @ComponentState) -> ByteArray; + fn get_votes( + self: @ComponentState, + account: ContractAddress, + timepoint: u64, + params: Span + ) -> u256; + } + // // Internal // @@ -65,7 +73,9 @@ pub mod GovernorComponent { pub impl InternalImpl< TContractState, +HasComponent, + +GovernorCountingTrait, +GovernorExecutorTrait, + +GovernorVotesTrait, impl SRC5: SRC5Component::HasComponent, +Drop > of InternalTrait { @@ -106,5 +116,61 @@ pub mod GovernorComponent { poseidon_hash_span(hashed_calls.span()) } + + /// Returns the state of a proposal, given its id. + fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState { + let proposal = self.proposals.read(proposal_id); + + if proposal.executed { + return ProposalState::Executed; + } + + if proposal.canceled { + return ProposalState::Canceled; + } + + let snapshot = self.proposal_snapshot(proposal_id); + + assert(snapshot.is_non_zero(), Errors::NONEXISTENT_PROPOSAL); + + let current_timepoint = self.clock(); + + if current_timepoint < snapshot { + return ProposalState::Pending; + } + + let deadline = self.proposal_deadline(proposal_id); + + if current_timepoint < deadline { + return ProposalState::Active; + } else if !self.quorum_reached(proposal_id) || !self.vote_succeeded(proposal_id) { + return ProposalState::Defeated; + } else if self.proposal_eta(proposal_id).is_zero() { + return ProposalState::Succeeded; + } else { + return ProposalState::Queued; + } + } + + /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot + /// is performed at the end of this block. Hence, voting for this proposal starts at the + /// beginning of the following block. + fn proposal_snapshot(self: @ComponentState, proposal_id: felt252) -> u64 { + self.proposals.read(proposal_id).vote_start + } + + /// Timepoint at which votes close. If using block number, votes close at the end of this + /// block, so it is possible to cast a vote during this block. + fn proposal_deadline(self: @ComponentState, proposal_id: felt252) -> u64 { + let proposal = self.proposals.read(proposal_id); + proposal.vote_start + proposal.vote_duration + } + + /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` and + /// `proposal_deadline`, this doesn't use the governor clock, and instead relies on the + /// executor's clock which may be different. In most cases this will be a timestamp. + fn proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { + self.proposals.read(proposal_id).eta_seconds + } } } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 0355292e1..96f2df19d 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -18,7 +18,7 @@ pub trait IERC6372 { } #[derive(Copy, Drop, Serde)] -enum ProposalState { +pub enum ProposalState { Pending, Active, Canceled, diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index 150b6b84e..42650efde 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -6,12 +6,12 @@ use starknet::storage_access::StorePacking; #[derive(Copy, Drop, Serde)] pub struct ProposalCore { - proposer: ContractAddress, - vote_start: u64, - vote_duration: u64, - executed: bool, - canceled: bool, - eta_seconds: u64 + pub proposer: ContractAddress, + pub vote_start: u64, + pub vote_duration: u64, + pub executed: bool, + pub canceled: bool, + pub eta_seconds: u64 } const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; From 609769103d10a97a1149a75686a9d4b4fe67933f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 17 Oct 2024 16:59:41 +0200 Subject: [PATCH 30/76] feat: add bytearray to utils --- .../governance/src/governor/governor.cairo | 89 ++++++++++++++++++- .../governance/src/governor/interface.cairo | 22 +++-- packages/utils/src/bytearray.cairo | 60 +++++++++++++ packages/utils/src/lib.cairo | 1 + 4 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 packages/utils/src/bytearray.cairo diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 6d12fd9ec..51ffd0468 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -15,8 +15,10 @@ pub mod GovernorComponent { use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; + use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::structs::DoubleEndedQueue; use starknet::ContractAddress; + use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess}; @@ -37,7 +39,9 @@ pub mod GovernorComponent { pub trait GovernorCountingTrait { fn COUNTING_MODE(self: @ComponentState) -> ByteArray; - fn has_voted(self: @ComponentState, proposal_id: felt252, account: ContractAddress) -> bool; + fn has_voted( + self: @ComponentState, proposal_id: felt252, account: ContractAddress + ) -> bool; fn quorum_reached(self: @ComponentState, proposal_id: felt252) -> bool; fn vote_succeeded(self: @ComponentState, proposal_id: felt252) -> bool; fn count_vote( @@ -97,6 +101,7 @@ pub mod GovernorComponent { } /// Hashing function used to (re)build the proposal id from the proposal details. + /// TODO: check if we should be using Pedersen hash instead of Poseidon. fn hash_proposal( ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { @@ -152,6 +157,11 @@ pub mod GovernorComponent { } } + /// The number of votes required in order for a voter to become a proposer. + fn proposal_threshold(self: @ComponentState, proposal_id: felt252) -> u256 { + 0 + } + /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot /// is performed at the end of this block. Hence, voting for this proposal starts at the /// beginning of the following block. @@ -166,11 +176,84 @@ pub mod GovernorComponent { proposal.vote_start + proposal.vote_duration } - /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` and - /// `proposal_deadline`, this doesn't use the governor clock, and instead relies on the + /// The account that created a proposal. + fn proposal_proposer( + self: @ComponentState, proposal_id: felt252 + ) -> ContractAddress { + self.proposals.read(proposal_id).proposer + } + + /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` + /// and `proposal_deadline`, this doesn't use the governor clock, and instead relies on the /// executor's clock which may be different. In most cases this will be a timestamp. fn proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { self.proposals.read(proposal_id).eta_seconds } + + /// Whether a proposal needs to be queued before execution. + fn proposal_needs_queuing( + self: @ComponentState, proposal_id: felt252 + ) -> bool { + false + } + + /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and + /// lasts for a duration specified by `voting_period`. + /// + /// NOTE: The state of the Governor and `targets` may change between the proposal creation + /// and its execution. This may be the result of third party actions on the targeted + /// contracts, or other governor proposals. For example, the balance of this contract could + /// be updated or its access control permissions may be modified, possibly compromising the + /// proposal's ability to execute successfully (e.g. the governor doesn't have enough value + /// to cover a proposal with multiple transfers). + /// + /// Returns the id of the proposal. + fn propose( + ref self: ComponentState, calls: Span, description: ByteArray + ) -> felt252 { + let proposer = starknet::get_caller_address(); + 1 + } + + /// Checks if the proposer is authorized to submit a proposal with the given description. + /// + /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address + /// written as a hex string (case insensitive), then the submission of this proposal will + /// only be authorized to said address. + /// + /// This is used for frontrunning protection. By adding this pattern at the end of their + /// proposal, one can ensure that no other address can submit the same proposal. An attacker + /// would have to either remove or change that part, which would result in a different + /// proposal id. + /// + /// If the description does not match this pattern, it is unrestricted and anyone can submit + /// it. This includes: + /// - If the `0x???` part is not a valid hex string. + /// - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. + /// - If it ends with the expected suffix followed by newlines or other whitespace. + /// - If it ends with some other similar suffix, e.g. `#other=abc`. + /// - If it does not end with any such suffix. + fn is_valid_description_for_proposer( + self: @ComponentState, proposer: ContractAddress, description: ByteArray + ) -> bool { + let length = description.len(); + + // Length is too short to contain a valid proposer suffix + if description.len() < 52 { + return true; + } + + // Extract what would be the `#proposer=` marker beginning the suffix + let marker = description.read_n_bytes(length - 52, 10); + + // If the marker is not found, there is no proposer suffix to check + if marker != "#proposer=" { + return true; + } + + let expected_address = description.read_n_bytes(length - 42, 42); + let proposer: felt252 = proposer.into(); + proposer.to_byte_array(16, 64) == expected_address + } } } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 96f2df19d..740ad72d4 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -139,7 +139,7 @@ pub trait IGovernor { /// multiple transfers). /// /// Returns the id of the proposal. - fn propose(self: @TState, calls: Span, description: ByteArray) -> felt252; + fn propose(ref self: TState, calls: Span, description: ByteArray) -> felt252; /// Queue a proposal. Some governors require this step to be performed before execution can /// happen. If queuing is not necessary, this function may revert. @@ -147,7 +147,7 @@ pub trait IGovernor { /// deadline to be reached. /// /// Returns the id of the proposal. - fn queue(self: @TState, calls: Span, description_hash: felt252) -> felt252; + fn queue(ref self: TState, calls: Span, description_hash: felt252) -> felt252; /// Execute a successful proposal. This requires the quorum to be reached, the vote to be /// successful, and the deadline to be reached. Depending on the governor it might also be @@ -157,30 +157,34 @@ pub trait IGovernor { /// additional timelock (See `timelock_controller`). /// /// Returns the id of the proposal. - fn execute(self: @TState, calls: Span, description_hash: felt252) -> felt252; + fn execute(ref self: TState, calls: Span, description_hash: felt252) -> felt252; /// Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending /// state, i.e. before the vote starts. /// /// Returns the id of the proposal. - fn cancel(self: @TState, calls: Span, description_hash: felt252) -> felt252; + fn cancel(ref self: TState, calls: Span, description_hash: felt252) -> felt252; /// Cast a vote. - fn cast_vote(self: @TState, proposal_id: felt252, support: u8) -> u256; + fn cast_vote(ref self: TState, proposal_id: felt252, support: u8) -> u256; /// Cast a vote with a reason. fn cast_vote_with_reason( - self: @TState, proposal_id: felt252, support: u8, reason: ByteArray + ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray ) -> u256; /// Cast a vote with a reason and additional serialized parameters. fn cast_vote_with_reason_and_params( - self: @TState, proposal_id: felt252, support: u8, reason: ByteArray, params: Span + ref self: TState, + proposal_id: felt252, + support: u8, + reason: ByteArray, + params: Span ) -> u256; /// Cast a vote using the voter's signature. fn cast_vote_by_sig( - self: @TState, + ref self: TState, proposal_id: felt252, support: u8, voter: ContractAddress, @@ -189,7 +193,7 @@ pub trait IGovernor { /// Cast a vote with a reason and additional serialized parameters using the voter's signature fn cast_vote_with_reason_and_params_by_sig( - self: @TState, + ref self: TState, proposal_id: felt252, support: u8, voter: ContractAddress, diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo new file mode 100644 index 000000000..a118ccd60 --- /dev/null +++ b/packages/utils/src/bytearray.cairo @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (utils/bytearray.cairo) + +use core::byte_array::ByteArrayTrait; +use core::to_byte_array::FormatAsByteArray; + +/// Reads n bytes from a byte array starting from a given index. +/// +/// Requirements: +/// +/// - `start + n` must be less than or equal to the length of the byte array. +pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray { + assert(start + n <= data.len(), 'ByteArray: out of bounds'); + + let mut result: ByteArray = Default::default(); + for i in 0..n { + result.append_byte(data[start + i]); + }; + result +} + +/// Converts a value to a byte array with a given base and padding. +/// +/// Requirements: +/// +/// - `base` can not be zero. +pub fn to_byte_array< + T, +FormatAsByteArray, +Into, +Copy +>( + value: @T, base: u8, padding: u16 +) -> ByteArray { + let value: felt252 = (*value).into(); + let base: felt252 = base.into(); + let mut byte_array = value.format_as_byte_array(base.try_into().unwrap()); + + if padding.into() > byte_array.len() { + let mut padding = padding.into() - byte_array.len(); + while padding > 0 { + byte_array += "0"; + padding -= 1; + }; + }; + byte_array +} + +/// ByteArray extension trait. +#[generate_trait] +pub impl ByteArrayExtImpl of ByteArrayExtTrait { + fn read_n_bytes(self: @ByteArray, start: u32, n: u32) -> ByteArray { + read_n_bytes(self, start, n) + } + + fn to_byte_array< + T, +FormatAsByteArray, +Into, +Copy + >( + self: @T, base: u8, padding: u16 + ) -> ByteArray { + to_byte_array(self, base, padding) + } +} diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index 2e96323af..687a3fa57 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.17.0 (utils/lib.cairo) +pub mod bytearray; pub mod cryptography; pub mod deployments; pub mod interfaces; From a7dbea2eaa2f653012653451d3ec04e017f62095 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 17 Oct 2024 17:24:37 +0200 Subject: [PATCH 31/76] fix: bytearray trait types --- .../governance/src/governor/governor.cairo | 34 +++++++++++++++++-- packages/utils/src/bytearray.cairo | 14 ++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 51ffd0468..6b62c6016 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -31,6 +31,8 @@ pub mod GovernorComponent { mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Governor: executor only'; pub const NONEXISTENT_PROPOSAL: felt252 = 'Governor: nonexistent proposal'; + pub const RESTRICTED_PROPOSER: felt252 = 'Governor: restricted proposer'; + pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Governor: insufficient votes'; } // @@ -62,6 +64,11 @@ pub mod GovernorComponent { fn clock(self: @ComponentState) -> u64; fn CLOCK_MODE(self: @ComponentState) -> ByteArray; fn get_votes( + self: @ComponentState, + account: ContractAddress, + timepoint: u64 + ) -> u256; + fn get_votes_with_params( self: @ComponentState, account: ContractAddress, timepoint: u64, @@ -158,7 +165,7 @@ pub mod GovernorComponent { } /// The number of votes required in order for a voter to become a proposer. - fn proposal_threshold(self: @ComponentState, proposal_id: felt252) -> u256 { + fn proposal_threshold(self: @ComponentState) -> u256 { 0 } @@ -200,6 +207,8 @@ pub mod GovernorComponent { /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and /// lasts for a duration specified by `voting_period`. /// + /// This function has opt-in frontrunning protection, described in `is_valid_description_for_proposer`. + /// /// NOTE: The state of the Governor and `targets` may change between the proposal creation /// and its execution. This may be the result of third party actions on the targeted /// contracts, or other governor proposals. For example, the balance of this contract could @@ -212,6 +221,26 @@ pub mod GovernorComponent { ref self: ComponentState, calls: Span, description: ByteArray ) -> felt252 { let proposer = starknet::get_caller_address(); + + // Check descrption for restricted proposer + assert( + self.is_valid_description_for_proposer(proposer, @description), + Errors::RESTRICTED_PROPOSER + ); + + // Check proposal threshold + let vote_threshold = self.proposal_threshold(); + if vote_threshold > 0 { + let votes = self.get_votes(proposer, self.clock() - 1); + assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); + } + + self._propose(calls, @description, proposer) + } + + fn _propose( + ref self: ComponentState, calls: Span, description: @ByteArray, proposer: ContractAddress + ) -> felt252 { 1 } @@ -234,7 +263,7 @@ pub mod GovernorComponent { /// - If it ends with some other similar suffix, e.g. `#other=abc`. /// - If it does not end with any such suffix. fn is_valid_description_for_proposer( - self: @ComponentState, proposer: ContractAddress, description: ByteArray + self: @ComponentState, proposer: ContractAddress, description: @ByteArray ) -> bool { let length = description.len(); @@ -252,7 +281,6 @@ pub mod GovernorComponent { } let expected_address = description.read_n_bytes(length - 42, 42); - let proposer: felt252 = proposer.into(); proposer.to_byte_array(16, 64) == expected_address } } diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index a118ccd60..ec0d1ef9d 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -25,7 +25,7 @@ pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray { /// /// - `base` can not be zero. pub fn to_byte_array< - T, +FormatAsByteArray, +Into, +Copy + T, +Into, +Copy >( value: @T, base: u8, padding: u16 ) -> ByteArray { @@ -46,12 +46,22 @@ pub fn to_byte_array< /// ByteArray extension trait. #[generate_trait] pub impl ByteArrayExtImpl of ByteArrayExtTrait { + // Reads n bytes from a byte array starting from a given index. + /// + /// Requirements: + /// + /// - `start + n` must be less than or equal to the length of the byte array. fn read_n_bytes(self: @ByteArray, start: u32, n: u32) -> ByteArray { read_n_bytes(self, start, n) } + /// Converts a value to a byte array with a given base and padding. + /// + /// Requirements: + /// + /// - `base` can not be zero. fn to_byte_array< - T, +FormatAsByteArray, +Into, +Copy + T, +Into, +Copy >( self: @T, base: u8, padding: u16 ) -> ByteArray { From 4fc97a0adac36b641bcc756cbe521d1a5ae8ee3d Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 18 Oct 2024 12:05:30 +0200 Subject: [PATCH 32/76] feat: apply stash --- packages/governance/src/governor/governor.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 6b62c6016..d70713399 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -241,6 +241,8 @@ pub mod GovernorComponent { fn _propose( ref self: ComponentState, calls: Span, description: @ByteArray, proposer: ContractAddress ) -> felt252 { + let proposal_id = self.hash_proposal(calls, description.hash()); + 1 } From 6df91a84e0f39b296da7dd11c819d109fbe2140c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 21 Oct 2024 11:59:51 +0200 Subject: [PATCH 33/76] feat: add propose mechanism --- .../governance/src/governor/governor.cairo | 91 ++++++++++++++++--- .../src/governor/proposal_core.cairo | 2 +- packages/utils/src/bytearray.cairo | 26 ++++-- .../src/structs/double_ended_queue.cairo | 3 + 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index d70713399..d13f950d5 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -18,9 +18,8 @@ pub mod GovernorComponent { use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::structs::DoubleEndedQueue; use starknet::ContractAddress; - use starknet::account::Call; - use starknet::storage::{Map, StorageMapReadAccess}; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; #[storage] pub struct Storage { @@ -28,9 +27,30 @@ pub mod GovernorComponent { governance_call: DoubleEndedQueue } + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ProposalCreated: ProposalCreated + } + + // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // restriction. + /// Emitted when `call` is scheduled as part of operation `id`. + #[derive(Drop, starknet::Event)] + pub struct ProposalCreated { + pub proposal_id: felt252, + pub proposer: ContractAddress, + pub calls: Span, + pub signatures: Span>, + pub vote_start: u64, + pub vote_end: u64, + pub description: ByteArray + } + mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Governor: executor only'; pub const NONEXISTENT_PROPOSAL: felt252 = 'Governor: nonexistent proposal'; + pub const EXISTENT_PROPOSAL: felt252 = 'Governor: existent proposal'; pub const RESTRICTED_PROPOSER: felt252 = 'Governor: restricted proposer'; pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Governor: insufficient votes'; } @@ -63,10 +83,10 @@ pub mod GovernorComponent { pub trait GovernorVotesTrait { fn clock(self: @ComponentState) -> u64; fn CLOCK_MODE(self: @ComponentState) -> ByteArray; + fn voting_delay(self: @ComponentState) -> u64; + fn voting_period(self: @ComponentState) -> u64; fn get_votes( - self: @ComponentState, - account: ContractAddress, - timepoint: u64 + self: @ComponentState, account: ContractAddress, timepoint: u64 ) -> u256; fn get_votes_with_params( self: @ComponentState, @@ -205,9 +225,10 @@ pub mod GovernorComponent { } /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and - /// lasts for a duration specified by `voting_period`. + /// lasts for a duration specified by `voting_period`. Returns the id of the proposal. /// - /// This function has opt-in frontrunning protection, described in `is_valid_description_for_proposer`. + /// This function has opt-in frontrunning protection, described in + /// `is_valid_description_for_proposer`. /// /// NOTE: The state of the Governor and `targets` may change between the proposal creation /// and its execution. This may be the result of third party actions on the targeted @@ -216,7 +237,14 @@ pub mod GovernorComponent { /// proposal's ability to execute successfully (e.g. the governor doesn't have enough value /// to cover a proposal with multiple transfers). /// - /// Returns the id of the proposal. + /// Requirements: + /// + /// - The proposer must be authorized to submit the proposal. + /// - The proposer must have enough votes to submit the proposal if `proposal_threshold` is + /// greater than zero. + /// - The proposal must not already exist. + /// + /// Emits a `ProposalCreated` event. fn propose( ref self: ComponentState, calls: Span, description: ByteArray ) -> felt252 { @@ -238,12 +266,51 @@ pub mod GovernorComponent { self._propose(calls, @description, proposer) } + /// Internal propose mechanism. Returns the proposal id. + /// + /// Requirements: + /// + /// - The proposal must not already exist. + /// + /// Emits a `ProposalCreated` event. fn _propose( - ref self: ComponentState, calls: Span, description: @ByteArray, proposer: ContractAddress + ref self: ComponentState, + calls: Span, + description: @ByteArray, + proposer: ContractAddress ) -> felt252 { let proposal_id = self.hash_proposal(calls, description.hash()); - 1 + assert(self.proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL); + + let snapshot = self.clock() + self.voting_delay(); + let duration = self.voting_period(); + + let proposal = ProposalCore { + proposer, + vote_start: snapshot, + vote_duration: duration, + executed: false, + canceled: false, + eta_seconds: 0 + }; + + self.proposals.write(proposal_id, proposal); + + self + .emit( + ProposalCreated { + proposal_id, + proposer, + calls, + signatures: array![].span(), + vote_start: snapshot, + vote_end: snapshot + duration, + description: description.clone() + } + ); + + proposal_id } /// Checks if the proposer is authorized to submit a proposal with the given description. @@ -265,7 +332,9 @@ pub mod GovernorComponent { /// - If it ends with some other similar suffix, e.g. `#other=abc`. /// - If it does not end with any such suffix. fn is_valid_description_for_proposer( - self: @ComponentState, proposer: ContractAddress, description: @ByteArray + self: @ComponentState, + proposer: ContractAddress, + description: @ByteArray ) -> bool { let length = description.len(); diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index 42650efde..83beb150d 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/proposal_core.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (governance/governor/proposal_core.cairo) use starknet::ContractAddress; use starknet::storage_access::StorePacking; diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index ec0d1ef9d..8f8829df2 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (utils/bytearray.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (utils/bytearray.cairo) use core::byte_array::ByteArrayTrait; +use core::poseidon::poseidon_hash_span; use core::to_byte_array::FormatAsByteArray; /// Reads n bytes from a byte array starting from a given index. @@ -24,9 +25,7 @@ pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray { /// Requirements: /// /// - `base` can not be zero. -pub fn to_byte_array< - T, +Into, +Copy ->( +pub fn to_byte_array, +Copy>( value: @T, base: u8, padding: u16 ) -> ByteArray { let value: felt252 = (*value).into(); @@ -43,6 +42,15 @@ pub fn to_byte_array< byte_array } +/// Hashes a byte array using the Poseidon hash algorithm. +/// Encodes the byte array as a ´Span´ by serializing ´data´. +pub fn hash_byte_array(data: @ByteArray) -> felt252 { + let mut serialized = array![]; + data.serialize(ref serialized); + + poseidon_hash_span(serialized.span()) +} + /// ByteArray extension trait. #[generate_trait] pub impl ByteArrayExtImpl of ByteArrayExtTrait { @@ -60,11 +68,15 @@ pub impl ByteArrayExtImpl of ByteArrayExtTrait { /// Requirements: /// /// - `base` can not be zero. - fn to_byte_array< - T, +Into, +Copy - >( + fn to_byte_array, +Copy>( self: @T, base: u8, padding: u16 ) -> ByteArray { to_byte_array(self, base, padding) } + + /// Hashes a byte array using the Poseidon hash algorithm. + /// Encodes the byte array as a ´Span´ by serializing ´data´. + fn hash(self: @ByteArray) -> felt252 { + hash_byte_array(self) + } } diff --git a/packages/utils/src/structs/double_ended_queue.cairo b/packages/utils/src/structs/double_ended_queue.cairo index 3b018cdfb..db65a16d3 100644 --- a/packages/utils/src/structs/double_ended_queue.cairo +++ b/packages/utils/src/structs/double_ended_queue.cairo @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 (utils/structs/double_ended_queue.cairo) + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::storage::{StoragePath, Mutable}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; From e7127f0a1092aa2d31badb86e215cca09838a651 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 21 Oct 2024 12:40:06 +0200 Subject: [PATCH 34/76] feat: add queue mechanism --- .../governance/src/governor/governor.cairo | 92 +++++++++++++++++-- .../governance/src/governor/interface.cairo | 2 +- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index d13f950d5..0ea03ae37 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -30,7 +30,8 @@ pub mod GovernorComponent { #[event] #[derive(Drop, starknet::Event)] pub enum Event { - ProposalCreated: ProposalCreated + ProposalCreated: ProposalCreated, + ProposalQueued: ProposalQueued } // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC @@ -47,12 +48,24 @@ pub mod GovernorComponent { pub description: ByteArray } + // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // restriction. + /// Emitted when a proposal is queued. + #[derive(Drop, starknet::Event)] + pub struct ProposalQueued { + pub proposal_id: felt252, + pub eta_seconds: u64 + } + + // TODO: check prefix for errors mod Errors { - pub const EXECUTOR_ONLY: felt252 = 'Governor: executor only'; - pub const NONEXISTENT_PROPOSAL: felt252 = 'Governor: nonexistent proposal'; - pub const EXISTENT_PROPOSAL: felt252 = 'Governor: existent proposal'; - pub const RESTRICTED_PROPOSER: felt252 = 'Governor: restricted proposer'; - pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Governor: insufficient votes'; + pub const EXECUTOR_ONLY: felt252 = 'Gov: executor only'; + pub const NONEXISTENT_PROPOSAL: felt252 = 'Gov: nonexistent proposal'; + pub const EXISTENT_PROPOSAL: felt252 = 'Gov: existent proposal'; + pub const RESTRICTED_PROPOSER: felt252 = 'Gov: restricted proposer'; + pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Gov: insufficient votes'; + pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Gov: unexpected proposal state'; + pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Gov: queue not implemented'; } // @@ -96,6 +109,21 @@ pub mod GovernorComponent { ) -> u256; } + pub trait GovernorQueueTrait { + /// Queuing mechanism. Can be used to modify the way queuing is + /// performed (for example adding a vault/timelock). + /// + /// Must return a timestamp that describes the expected ETA for execution. If the returned + /// value is 0, the core will consider queueing did not succeed, and the public `queue` + /// function will revert. + fn queue_operations( + ref self: ComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) -> u64; + } + // // Internal // @@ -107,6 +135,7 @@ pub mod GovernorComponent { +GovernorCountingTrait, +GovernorExecutorTrait, +GovernorVotesTrait, + +GovernorQueueTrait, impl SRC5: SRC5Component::HasComponent, +Drop > of InternalTrait { @@ -313,6 +342,40 @@ pub mod GovernorComponent { proposal_id } + /// Queue a proposal. Some governors require this step to be performed before execution can + /// happen. If queuing is not necessary, this function may revert. + /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the + /// deadline to be reached. + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Succeeded` state. + /// - The queue operation must return a non-zero ETA. + /// + /// Emits a `ProposalQueued` event. + fn queue( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + let proposal_id = self.hash_proposal(calls, description_hash); + + self.validate_state(proposal_id, array![ProposalState::Succeeded].span()); + + let eta_seconds = self.queue_operations(proposal_id, calls, description_hash); + + assert(eta_seconds > 0, Errors::QUEUE_NOT_IMPLEMENTED); + + let mut proposal = self.proposals.read(proposal_id); + proposal.eta_seconds = eta_seconds; + + self.proposals.write(proposal_id, proposal); + + self.emit(ProposalQueued { proposal_id, eta_seconds }); + + proposal_id + } + /// Checks if the proposer is authorized to submit a proposal with the given description. /// /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address @@ -354,5 +417,22 @@ pub mod GovernorComponent { let expected_address = description.read_n_bytes(length - 42, 42); proposer.to_byte_array(16, 64) == expected_address } + + /// Validates that the proposal is in one of the expected states. + fn validate_state( + self: @ComponentState, + proposal_id: felt252, + allowed_states: Span + ) { + let current_state = self.state(proposal_id); + let mut found = false; + for state in allowed_states { + if current_state == *state { + found = true; + break; + } + }; + assert(found, Errors::UNEXPECTED_PROPOSAL_STATE); + } } } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 740ad72d4..ff6563d98 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -17,7 +17,7 @@ pub trait IERC6372 { fn CLOCK_MODE(self: @TState) -> ByteArray; } -#[derive(Copy, Drop, Serde)] +#[derive(Copy, PartialEq, Drop, Serde)] pub enum ProposalState { Pending, Active, From aa121b63756aff4bf5a64606e88b4350aa05dff9 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 21 Oct 2024 13:21:51 +0200 Subject: [PATCH 35/76] feat: add execute and cancel --- .../governance/src/governor/governor.cairo | 165 ++++++++++++++++-- packages/utils/src/structs.cairo | 2 +- .../src/structs/double_ended_queue.cairo | 2 +- 3 files changed, 151 insertions(+), 18 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 0ea03ae37..766adf794 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -16,7 +16,7 @@ pub mod GovernorComponent { use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; - use openzeppelin_utils::structs::DoubleEndedQueue; + use openzeppelin_utils::structs::{DoubleEndedQueue, DoubleEndedQueueTrait}; use starknet::ContractAddress; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; @@ -31,7 +31,9 @@ pub mod GovernorComponent { #[derive(Drop, starknet::Event)] pub enum Event { ProposalCreated: ProposalCreated, - ProposalQueued: ProposalQueued + ProposalQueued: ProposalQueued, + ProposalExecuted: ProposalExecuted, + ProposalCanceled: ProposalCanceled } // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC @@ -57,9 +59,26 @@ pub mod GovernorComponent { pub eta_seconds: u64 } + // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // restriction. + /// Emitted when a proposal is executed. + #[derive(Drop, starknet::Event)] + pub struct ProposalExecuted { + pub proposal_id: felt252 + } + + // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // restriction. + /// Emitted when a proposal is canceled. + #[derive(Drop, starknet::Event)] + pub struct ProposalCanceled { + pub proposal_id: felt252 + } + // TODO: check prefix for errors mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Gov: executor only'; + pub const PROPOSER_ONLY: felt252 = 'Gov: proposer only'; pub const NONEXISTENT_PROPOSAL: felt252 = 'Gov: nonexistent proposal'; pub const EXISTENT_PROPOSAL: felt252 = 'Gov: existent proposal'; pub const RESTRICTED_PROPOSER: felt252 = 'Gov: restricted proposer'; @@ -89,8 +108,20 @@ pub mod GovernorComponent { ) -> u256; } - pub trait GovernorExecutorTrait { + pub trait GovernorExecuteTrait { + /// Address through which the governor executes action. + /// Should be used to specify whether the module execute actions through another contract + /// such as a timelock. fn executor(self: @ComponentState) -> ContractAddress; + + /// Execution mechanism. Can be used to modify the way execution is + /// performed (for example adding a vault/timelock). + fn execute_operations( + ref self: ComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ); } pub trait GovernorVotesTrait { @@ -113,7 +144,9 @@ pub mod GovernorComponent { /// Queuing mechanism. Can be used to modify the way queuing is /// performed (for example adding a vault/timelock). /// - /// Must return a timestamp that describes the expected ETA for execution. If the returned + /// Requirements: + /// + /// - Must return a timestamp that describes the expected ETA for execution. If the returned /// value is 0, the core will consider queueing did not succeed, and the public `queue` /// function will revert. fn queue_operations( @@ -122,6 +155,15 @@ pub mod GovernorComponent { calls: Span, description_hash: felt252 ) -> u64; + + /// Whether a proposal needs to be queued before execution. + /// + /// Requirements: + /// + /// - Must return true if the proposal needs to be queued before execution. + fn proposal_needs_queuing( + self: @ComponentState, proposal_id: felt252 + ) -> bool; } // @@ -133,7 +175,7 @@ pub mod GovernorComponent { TContractState, +HasComponent, +GovernorCountingTrait, - +GovernorExecutorTrait, + +GovernorExecuteTrait, +GovernorVotesTrait, +GovernorQueueTrait, impl SRC5: SRC5Component::HasComponent, @@ -246,13 +288,6 @@ pub mod GovernorComponent { self.proposals.read(proposal_id).eta_seconds } - /// Whether a proposal needs to be queued before execution. - fn proposal_needs_queuing( - self: @ComponentState, proposal_id: felt252 - ) -> bool { - false - } - /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and /// lasts for a duration specified by `voting_period`. Returns the id of the proposal. /// @@ -342,7 +377,7 @@ pub mod GovernorComponent { proposal_id } - /// Queue a proposal. Some governors require this step to be performed before execution can + /// Queues a proposal. Some governors require this step to be performed before execution can /// happen. If queuing is not necessary, this function may revert. /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the /// deadline to be reached. @@ -359,16 +394,13 @@ pub mod GovernorComponent { ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { let proposal_id = self.hash_proposal(calls, description_hash); - self.validate_state(proposal_id, array![ProposalState::Succeeded].span()); let eta_seconds = self.queue_operations(proposal_id, calls, description_hash); - assert(eta_seconds > 0, Errors::QUEUE_NOT_IMPLEMENTED); let mut proposal = self.proposals.read(proposal_id); proposal.eta_seconds = eta_seconds; - self.proposals.write(proposal_id, proposal); self.emit(ProposalQueued { proposal_id, eta_seconds }); @@ -376,6 +408,107 @@ pub mod GovernorComponent { proposal_id } + /// Executes a successful proposal. This requires the quorum to be reached, the vote to be + /// successful, and the deadline to be reached. Depending on the governor it might also be + /// required that the proposal was queued and that some delay passed. + /// + /// NOTE: Some modules can modify the requirements for execution, for example by adding an + /// additional timelock (See `timelock_controller`). + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Succeeded` or `Queued` state. + /// + /// Emits a `ProposalExecuted` event. + fn execute( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + let proposal_id = self.hash_proposal(calls, description_hash); + self + .validate_state( + proposal_id, array![ProposalState::Succeeded, ProposalState::Queued].span() + ); + + // Mark proposal as executed to avoid reentrancy + let mut proposal = self.proposals.read(proposal_id); + proposal.executed = true; + self.proposals.write(proposal_id, proposal); + + let self_executor = self.executor() == starknet::get_contract_address(); + // Register governance call in queue before execution + if self_executor { // TODO: Save the calldatas in the governance_call queue + } + + self.execute_operations(proposal_id, calls, description_hash); + + // Clean up the governance call queue + if self_executor + && (@self).governance_call.deref().len() > 0 { // TODO: Clean up the queue + } + + self.emit(ProposalExecuted { proposal_id }); + + proposal_id + } + + /// Cancels a proposal. A proposal is cancellable by the proposer, but only while it is + /// Pending state, i.e. before the vote starts. + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Pending` state. + /// + /// Emits a `ProposalCanceled` event. + fn cancel( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + let proposal_id = self.hash_proposal(calls, description_hash); + self.validate_state(proposal_id, array![ProposalState::Pending].span()); + + assert( + starknet::get_caller_address() == self.proposal_proposer(proposal_id), + Errors::PROPOSER_ONLY + ); + + self._cancel(proposal_id, calls, description_hash) + } + + /// Internal cancel mechanism with minimal restrictions. Returns the id of the proposal. + /// + /// Requirements: + /// + /// - A proposal can be cancelled in any state other than + /// Canceled, Expired, or Executed. Once cancelled a proposal can't be re-submitted. + /// + /// Emits a `ProposalCanceled` event. + fn _cancel( + ref self: ComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) -> felt252 { + let valid_states = array![ + ProposalState::Pending, + ProposalState::Active, + ProposalState::Defeated, + ProposalState::Succeeded, + ProposalState::Queued + ]; + self.validate_state(proposal_id, valid_states.span()); + + let mut proposal = self.proposals.read(proposal_id); + proposal.canceled = true; + self.proposals.write(proposal_id, proposal); + + self.emit(ProposalCanceled { proposal_id }); + + proposal_id + } + /// Checks if the proposer is authorized to submit a proposal with the given description. /// /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 9fd1e700d..13abe72cb 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -2,4 +2,4 @@ pub mod checkpoint; pub mod double_ended_queue; pub use checkpoint::{Trace, Checkpoint}; -pub use double_ended_queue::DoubleEndedQueue; +pub use double_ended_queue::{DoubleEndedQueue, DoubleEndedQueueTrait}; diff --git a/packages/utils/src/structs/double_ended_queue.cairo b/packages/utils/src/structs/double_ended_queue.cairo index db65a16d3..615c9520f 100644 --- a/packages/utils/src/structs/double_ended_queue.cairo +++ b/packages/utils/src/structs/double_ended_queue.cairo @@ -85,7 +85,7 @@ pub impl DoubleEndedQueueImpl of DoubleEndedQueueTrait { } /// Returns the number of items in the queue. - fn len(self: StoragePath) -> felt252 { + fn len(self: StoragePath) -> u32 { self._end.read() - self._begin.read() } From 7f0fde60142d7d6f12970543f34e979a82436473 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 22 Oct 2024 16:04:54 +0200 Subject: [PATCH 36/76] feat: add main trait --- .../governance/src/governor/governor.cairo | 296 ++++++++++++++++-- 1 file changed, 269 insertions(+), 27 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 766adf794..4ba290243 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -10,7 +10,7 @@ pub mod GovernorComponent { use core::num::traits::Zero; use core::poseidon::{PoseidonTrait, poseidon_hash_span}; use crate::governor::ProposalCore; - use crate::governor::interface::{ProposalState, IGOVERNOR_ID}; + use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; @@ -20,6 +20,7 @@ pub mod GovernorComponent { use starknet::ContractAddress; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; #[storage] pub struct Storage { @@ -88,16 +89,11 @@ pub mod GovernorComponent { } // - // Extensions + // Extensions traits // pub trait GovernorCountingTrait { - fn COUNTING_MODE(self: @ComponentState) -> ByteArray; - fn has_voted( - self: @ComponentState, proposal_id: felt252, account: ContractAddress - ) -> bool; - fn quorum_reached(self: @ComponentState, proposal_id: felt252) -> bool; - fn vote_succeeded(self: @ComponentState, proposal_id: felt252) -> bool; + fn counting_mode(self: @ComponentState) -> ByteArray; fn count_vote( ref self: ComponentState, proposal_id: felt252, @@ -106,6 +102,24 @@ pub mod GovernorComponent { total_weight: u256, params: Span ) -> u256; + fn has_voted( + self: @ComponentState, proposal_id: felt252, account: ContractAddress + ) -> bool; + fn quorum_reached(self: @ComponentState, proposal_id: felt252) -> bool; + fn vote_succeeded(self: @ComponentState, proposal_id: felt252) -> bool; + } + + pub trait GovernorVotesTrait { + fn clock(self: @ComponentState) -> u64; + fn CLOCK_MODE(self: @ComponentState) -> ByteArray; + fn voting_delay(self: @ComponentState) -> u64; + fn voting_period(self: @ComponentState) -> u64; + fn get_votes( + self: @ComponentState, + account: ContractAddress, + timepoint: u64, + params: Span + ) -> u256; } pub trait GovernorExecuteTrait { @@ -124,22 +138,6 @@ pub mod GovernorComponent { ); } - pub trait GovernorVotesTrait { - fn clock(self: @ComponentState) -> u64; - fn CLOCK_MODE(self: @ComponentState) -> ByteArray; - fn voting_delay(self: @ComponentState) -> u64; - fn voting_period(self: @ComponentState) -> u64; - fn get_votes( - self: @ComponentState, account: ContractAddress, timepoint: u64 - ) -> u256; - fn get_votes_with_params( - self: @ComponentState, - account: ContractAddress, - timepoint: u64, - params: Span - ) -> u256; - } - pub trait GovernorQueueTrait { /// Queuing mechanism. Can be used to modify the way queuing is /// performed (for example adding a vault/timelock). @@ -166,6 +164,197 @@ pub mod GovernorComponent { ) -> bool; } + // + // External + // + + #[embeddable_as(GovernorImpl)] + impl Governor< + TContractState, +HasComponent, + +GovernorCountingTrait, + impl Metadata: SNIP12Metadata, + +Drop, + > of IGovernor> { + /// Name of the governor instance (used in building the SNIP-12 domain separator). + fn name(self: @ComponentState) -> felt252 { + Metadata::name() + } + + /// Version of the governor instance (used in building SNIP-12 domain separator). + fn version(self: @ComponentState) -> felt252 { + Metadata::version() + } + + /// A description of the possible `support` values for `cast_vote` and the way these votes are + /// counted, meant to be consumed by UIs to show correct vote options and interpret the results. + /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for + /// example `support=bravo&quorum=for,abstain`. + /// + /// There are 2 standard keys: `support` and `quorum`. + /// + /// - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in + /// `GovernorBravo`. + /// - `quorum=bravo` means that only For votes are counted towards quorum. + /// - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. + /// + /// If a counting module makes use of encoded `params`, it should include this under a `params` + /// key with a unique name that describes the behavior. For example: + /// + /// - `params=fractional` might refer to a scheme where votes are divided fractionally between + /// for/against/abstain. + /// - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. + /// + /// NOTE: The string can be decoded by the standard + /// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] + /// JavaScript class. + fn COUNTING_MODE(self: @ComponentState) -> ByteArray { + self.counting_mode() + } + + /// Hashing function used to (re)build the proposal id from the proposal details. + fn hash_proposal( + self: @ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + self._hash_proposal(calls, description_hash) + } + + fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState { + ProposalState::Pending + } + + fn proposal_threshold(self: @ComponentState, proposal_id: felt252) -> u256 { + 1 + } + + fn proposal_snapshot(self: @ComponentState, proposal_id: felt252) -> u64 { + 1 + } + + fn proposal_deadline(self: @ComponentState, proposal_id: felt252) -> u64 { + 1 + } + + fn proposal_proposer( + self: @ComponentState, proposal_id: felt252 + ) -> ContractAddress { + Default::default() + } + + fn proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { + 1 + } + + fn proposal_needs_queuing( + self: @ComponentState, proposal_id: felt252 + ) -> bool { + false + } + + fn voting_delay(self: @ComponentState) -> u64 { + 1 + } + + fn voting_period(self: @ComponentState) -> u64 { + 1 + } + + fn quorum(self: @ComponentState, timepoint: u64) -> u256 { + 1 + } + + fn get_votes( + self: @ComponentState, account: ContractAddress, timepoint: u64 + ) -> u256 { + 1 + } + + fn get_votes_with_params( + self: @ComponentState, + account: ContractAddress, + timepoint: u64, + params: Span + ) -> u256 { + 1 + } + + fn has_voted( + self: @ComponentState, proposal_id: felt252, account: ContractAddress + ) -> bool { + false + } + + fn propose( + ref self: ComponentState, calls: Span, description: ByteArray + ) -> felt252 { + 1 + } + + fn queue( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + 1 + } + + fn execute( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + 1 + } + + fn cancel( + ref self: ComponentState, calls: Span, description_hash: felt252 + ) -> felt252 { + 1 + } + + fn cast_vote( + ref self: ComponentState, proposal_id: felt252, support: u8 + ) -> u256 { + 1 + } + + fn cast_vote_with_reason( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + reason: ByteArray + ) -> u256 { + 1 + } + + fn cast_vote_with_reason_and_params( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + reason: ByteArray, + params: Span + ) -> u256 { + 1 + } + + fn cast_vote_by_sig( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + voter: ContractAddress, + signature: Span + ) -> u256 { + 1 + } + + fn cast_vote_with_reason_and_params_by_sig( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + voter: ContractAddress, + reason: ByteArray, + params: Span, + signature: Span + ) -> u256 { + 1 + } + } + // // Internal // @@ -198,9 +387,9 @@ pub mod GovernorComponent { () } - /// Hashing function used to (re)build the proposal id from the proposal details. + /// Returns a hash of the proposal using the Poseidon algorithm. /// TODO: check if we should be using Pedersen hash instead of Poseidon. - fn hash_proposal( + fn _hash_proposal( ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { let mut hashed_calls = array![]; @@ -323,7 +512,7 @@ pub mod GovernorComponent { // Check proposal threshold let vote_threshold = self.proposal_threshold(); if vote_threshold > 0 { - let votes = self.get_votes(proposer, self.clock() - 1); + let votes = self.get_votes(proposer, self.clock() - 1, array![].span()); assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); } @@ -509,6 +698,59 @@ pub mod GovernorComponent { proposal_id } + /// Cast a vote. + fn cast_vote( + ref self: ComponentState, proposal_id: felt252, support: u8 + ) -> u256 { + 1 + } + + /// Cast a vote with a reason. + fn cast_vote_with_reason( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + reason: ByteArray + ) -> u256 { + 1 + } + + /// Cast a vote with a reason and additional serialized parameters. + fn cast_vote_with_reason_and_params( + ref self: ComponentState, + proposal_id: felt252, + support: u8, + reason: ByteArray, + params: Span + ) -> u256 { + 1 + } + + fn _cast_vote( + ref self: ComponentState, + proposal_id: felt252, + account: ContractAddress, + support: u8, + reason: ByteArray, + ) -> u256 { + 1 + } + + fn _cast_vote_with_params( + ref self: ComponentState, + proposal_id: felt252, + account: ContractAddress, + support: u8, + reason: ByteArray, + params: Span + ) -> u256 { + self.validate_state(proposal_id, array![ProposalState::Active].span()); + + // let total_weight = self.get_votes(account, self.clock() - 1); + + 1 + } + /// Checks if the proposer is authorized to submit a proposal with the given description. /// /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address From df44b1c970ce0df4c0d40c5795c7a18a82240412 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 28 Oct 2024 14:46:17 +0100 Subject: [PATCH 37/76] feat: finish first extension --- .../governance/src/governor/extensions.cairo | 1 + .../extensions/governor_counting_simple.cairo | 160 +++++ .../governance/src/governor/governor.cairo | 661 +++++++++++------- .../governance/src/governor/interface.cairo | 5 +- .../src/structs/double_ended_queue.cairo | 2 +- 5 files changed, 559 insertions(+), 270 deletions(-) create mode 100644 packages/governance/src/governor/extensions.cairo create mode 100644 packages/governance/src/governor/extensions/governor_counting_simple.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo new file mode 100644 index 000000000..12dccb9c9 --- /dev/null +++ b/packages/governance/src/governor/extensions.cairo @@ -0,0 +1 @@ +pub mod governor_counting_simple; diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo new file mode 100644 index 000000000..870508d03 --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_counting_simple.cairo) + +/// # GovernorCountingSimple Component +/// +/// Extension of GovernorComponent for simple, 3 options, vote counting. +#[starknet::component] +pub mod GovernorCountingSimpleComponent { + use crate::governor::GovernorComponent::{ + GovernorCountingTrait, InternalImpl, ComponentState as GovernorComponentState + }; + use crate::governor::GovernorComponent; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::ContractAddress; + use starknet::storage::{Map, StoragePathEntry, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + type ProposalId = felt252; + + #[storage] + pub struct Storage { + GovernorCountingSimple_proposals_votes: Map, + } + + /// Supported vote types. + enum VoteType { + Against, + For, + Abstain, + } + + impl U8TryIntoVoteType of TryInto { + fn try_into(self: u8) -> Option { + match self { + 0 => Option::Some(VoteType::Against), + 1 => Option::Some(VoteType::For), + 2 => Option::Some(VoteType::Abstain), + _ => Option::None, + } + } + } + + #[starknet::storage_node] + struct ProposalVote { + against_votes: u256, + for_votes: u256, + abstain_votes: u256, + has_voted: Map + } + + mod Errors { + pub const ALREADY_CAST_VOTE: felt252 = 'Already cast vote'; + pub const INVALID_VOTE_TYPE: felt252 = 'Invalid vote type'; + } + + impl GovernorCounting< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorQuorumTrait, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorExecuteTrait, + +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorProposeTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + impl GovernorCountingSimple: HasComponent, + +Drop + > of GovernorCountingTrait { + /// See `GovernorComponent::GovernorCountingTrait::counting_mode`. + fn counting_mode(self: @GovernorComponentState) -> ByteArray { + return "support=bravo&quorum=for,abstain"; + } + + /// See `GovernorComponent::GovernorCountingTrait::count_vote`. + /// + /// In this module, the support follows the `VoteType` enum (from Governor Bravo). + fn count_vote( + ref self: GovernorComponentState, + proposal_id: felt252, + account: ContractAddress, + support: u8, + total_weight: u256, + params: Span + ) -> u256 { + let mut contract = self.get_contract_mut(); + let mut this_component = GovernorCountingSimple::get_component_mut(ref contract); + + let proposal_votes = this_component + .GovernorCountingSimple_proposals_votes + .entry(proposal_id); + assert(!proposal_votes.has_voted.read(account), Errors::ALREADY_CAST_VOTE); + + proposal_votes.has_voted.write(account, true); + + let support: VoteType = support.try_into().expect(Errors::INVALID_VOTE_TYPE); + match support { + VoteType::Against => { + let current_votes = proposal_votes.against_votes.read(); + proposal_votes.against_votes.write(current_votes + total_weight); + }, + VoteType::For => { + let current_votes = proposal_votes.for_votes.read(); + proposal_votes.for_votes.write(current_votes + total_weight); + }, + VoteType::Abstain => { + let current_votes = proposal_votes.abstain_votes.read(); + proposal_votes.abstain_votes.write(current_votes + total_weight); + } + } + total_weight + } + + /// See `GovernorComponent::GovernorCountingTrait::has_voted`. + fn has_voted( + self: @GovernorComponentState, + proposal_id: felt252, + account: ContractAddress + ) -> bool { + let contract = self.get_contract(); + let this_component = GovernorCountingSimple::get_component(contract); + let proposal_votes = this_component + .GovernorCountingSimple_proposals_votes + .entry(proposal_id); + + proposal_votes.has_voted.read(account) + } + + /// See `GovernorComponent::GovernorCountingTrait::quorum_reached`. + fn quorum_reached( + self: @GovernorComponentState, proposal_id: felt252 + ) -> bool { + let contract = self.get_contract(); + let this_component = GovernorCountingSimple::get_component(contract); + + let proposal_votes = this_component + .GovernorCountingSimple_proposals_votes + .entry(proposal_id); + let snapshot = self._proposal_snapshot(proposal_id); + + self.quorum(snapshot) <= proposal_votes.for_votes.read() + + proposal_votes.abstain_votes.read() + } + + /// See `GovernorComponent::GovernorCountingTrait::vote_succeeded`. + /// + /// In this module, the `for_votes` must be strictly over the `against_votes`. + fn vote_succeeded( + self: @GovernorComponentState, proposal_id: felt252 + ) -> bool { + let contract = self.get_contract(); + let this_component = GovernorCountingSimple::get_component(contract); + let proposal_votes = this_component + .GovernorCountingSimple_proposals_votes + .entry(proposal_id); + + proposal_votes.for_votes.read() > proposal_votes.against_votes.read() + } + } +} diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 4ba290243..4882fc2ad 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/governor.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (governance/governor/governor.cairo) /// # Governor Component /// @@ -16,16 +16,16 @@ pub mod GovernorComponent { use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use openzeppelin_utils::structs::{DoubleEndedQueue, DoubleEndedQueueTrait}; use starknet::ContractAddress; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; - use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; #[storage] pub struct Storage { - proposals: Map, - governance_call: DoubleEndedQueue + Governor_proposals: Map, + Governor_governance_call: DoubleEndedQueue } #[event] @@ -34,10 +34,12 @@ pub mod GovernorComponent { ProposalCreated: ProposalCreated, ProposalQueued: ProposalQueued, ProposalExecuted: ProposalExecuted, - ProposalCanceled: ProposalCanceled + ProposalCanceled: ProposalCanceled, + VoteCast: VoteCast, + VoteCastWithParams: VoteCastWithParams } - // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when `call` is scheduled as part of operation `id`. #[derive(Drop, starknet::Event)] @@ -51,7 +53,7 @@ pub mod GovernorComponent { pub description: ByteArray } - // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is queued. #[derive(Drop, starknet::Event)] @@ -60,7 +62,7 @@ pub mod GovernorComponent { pub eta_seconds: u64 } - // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is executed. #[derive(Drop, starknet::Event)] @@ -68,7 +70,7 @@ pub mod GovernorComponent { pub proposal_id: felt252 } - // TODO: Maybe add indexed keys and rename members since we don't have the GovernorBravo BC + // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is canceled. #[derive(Drop, starknet::Event)] @@ -76,6 +78,34 @@ pub mod GovernorComponent { pub proposal_id: felt252 } + /// Emitted when a vote is cast without params. + #[derive(Drop, starknet::Event)] + pub struct VoteCast { + #[key] + pub voter: ContractAddress, + pub proposal_id: felt252, + pub support: u8, + pub weight: u256, + pub reason: ByteArray + } + + /// Emitted when a vote is cast with params. + /// + /// NOTE: `support` values should be seen as buckets. Their interpretation depends on the voting + /// module used. + /// `params` are additional encoded parameters. Their interpretation also depends on the voting + /// module used. + #[derive(Drop, starknet::Event)] + pub struct VoteCastWithParams { + #[key] + pub voter: ContractAddress, + pub proposal_id: felt252, + pub support: u8, + pub weight: u256, + pub reason: ByteArray, + pub params: Span + } + // TODO: check prefix for errors mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Gov: executor only'; @@ -92,8 +122,29 @@ pub mod GovernorComponent { // Extensions traits // + pub trait GovernorSettingsTrait { + /// Default additional encoded parameters used by cast_vote methods that don't include them. + /// + /// Note: Should be defined by specific implementations to use an appropriate value, the + /// meaning of the additional params, in the context of that implementation + fn default_params(self: @ComponentState) -> Span; + } + + + pub trait GovernorQuorumTrait { + /// See `interface::IGovernor::::quorum`. + fn quorum(self: @ComponentState, timepoint: u64) -> u256; + } + pub trait GovernorCountingTrait { + /// See `interface::IGovernor::COUNTING_MODE`. fn counting_mode(self: @ComponentState) -> ByteArray; + + /// Register a vote for `proposal_id` by `account` with a given `support`, + /// voting `weight` and voting `params`. + /// + /// NOTE: Support is generic and can represent various things depending on the voting system + /// used. fn count_vote( ref self: ComponentState, proposal_id: felt252, @@ -102,18 +153,33 @@ pub mod GovernorComponent { total_weight: u256, params: Span ) -> u256; + + /// See `interface::IGovernor::has_voted`. fn has_voted( self: @ComponentState, proposal_id: felt252, account: ContractAddress ) -> bool; + + /// Returns whether amount of votes already cast passes the threshold limit. fn quorum_reached(self: @ComponentState, proposal_id: felt252) -> bool; + + /// Returns whether the proposal is successful or not. fn vote_succeeded(self: @ComponentState, proposal_id: felt252) -> bool; } pub trait GovernorVotesTrait { + /// See `interface::IERC6372::clock`. fn clock(self: @ComponentState) -> u64; - fn CLOCK_MODE(self: @ComponentState) -> ByteArray; + + /// See `interface::IERC6372::CLOCK_MODE`. + fn clock_mode(self: @ComponentState) -> ByteArray; + + /// See `interface::IGovernor::voting_delay`. fn voting_delay(self: @ComponentState) -> u64; + + /// See `interface::IGovernor::voting_period`. fn voting_period(self: @ComponentState) -> u64; + + /// See `interface::IGovernor::get_votes`. fn get_votes( self: @ComponentState, account: ContractAddress, @@ -122,6 +188,11 @@ pub mod GovernorComponent { ) -> u256; } + pub trait GovernorProposeTrait { + /// See `interface::IGovernor::proposal_threshold`. + fn proposal_threshold(self: @ComponentState) -> u256; + } + pub trait GovernorExecuteTrait { /// Address through which the governor executes action. /// Should be used to specify whether the module execute actions through another contract @@ -154,11 +225,7 @@ pub mod GovernorComponent { description_hash: felt252 ) -> u64; - /// Whether a proposal needs to be queued before execution. - /// - /// Requirements: - /// - /// - Must return true if the proposal needs to be queued before execution. + /// See `interface::IGovernor::proposal_needs_queuing`. fn proposal_needs_queuing( self: @ComponentState, proposal_id: felt252 ) -> bool; @@ -170,8 +237,16 @@ pub mod GovernorComponent { #[embeddable_as(GovernorImpl)] impl Governor< - TContractState, +HasComponent, - +GovernorCountingTrait, + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +GovernorSettingsTrait, + +GovernorExecuteTrait, + impl GovernorQuorum: GovernorQuorumTrait, + impl GovernorCounting: GovernorCountingTrait, + impl GovernorPropose: GovernorProposeTrait, + impl GovernorQueue: GovernorQueueTrait, + impl GovernorVotes: GovernorVotesTrait, impl Metadata: SNIP12Metadata, +Drop, > of IGovernor> { @@ -185,10 +260,11 @@ pub mod GovernorComponent { Metadata::version() } - /// A description of the possible `support` values for `cast_vote` and the way these votes are - /// counted, meant to be consumed by UIs to show correct vote options and interpret the results. - /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for - /// example `support=bravo&quorum=for,abstain`. + /// A description of the possible `support` values for `cast_vote` and the way these votes + /// are counted, meant to be consumed by UIs to show correct vote options and interpret the + /// results. + /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect, + /// for example `support=bravo&quorum=for,abstain`. /// /// There are 2 standard keys: `support` and `quorum`. /// @@ -197,11 +273,12 @@ pub mod GovernorComponent { /// - `quorum=bravo` means that only For votes are counted towards quorum. /// - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. /// - /// If a counting module makes use of encoded `params`, it should include this under a `params` + /// If a counting module makes use of encoded `params`, it should include this under a + /// `params` /// key with a unique name that describes the behavior. For example: /// - /// - `params=fractional` might refer to a scheme where votes are divided fractionally between - /// for/against/abstain. + /// - `params=fractional` might refer to a scheme where votes are divided fractionally + /// between for/against/abstain. /// - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. /// /// NOTE: The string can be decoded by the standard @@ -218,110 +295,292 @@ pub mod GovernorComponent { self._hash_proposal(calls, description_hash) } + /// Returns the state of a proposal, given its id. fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState { - ProposalState::Pending + self._state(proposal_id) } - fn proposal_threshold(self: @ComponentState, proposal_id: felt252) -> u256 { - 1 + /// The number of votes required in order for a voter to become a proposer. + fn proposal_threshold(self: @ComponentState) -> u256 { + self._proposal_threshold() } + /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot + /// is performed at the end of this block. Hence, voting for this proposal starts at the + /// beginning of the following block. fn proposal_snapshot(self: @ComponentState, proposal_id: felt252) -> u64 { - 1 + self._proposal_snapshot(proposal_id) } + /// Timepoint at which votes close. If using block number, votes close at the end of this + /// block, so it is possible to cast a vote during this block. fn proposal_deadline(self: @ComponentState, proposal_id: felt252) -> u64 { - 1 + self._proposal_deadline(proposal_id) } + /// The account that created a proposal. fn proposal_proposer( self: @ComponentState, proposal_id: felt252 ) -> ContractAddress { - Default::default() + self._proposal_proposer(proposal_id) } + /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` + /// and `proposal_deadline`, this doesn't use the governor clock, and instead relies on the + /// executor's clock which may be different. In most cases this will be a timestamp. fn proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { - 1 + self._proposal_eta(proposal_id) } + /// Whether a proposal needs to be queued before execution. fn proposal_needs_queuing( self: @ComponentState, proposal_id: felt252 ) -> bool { - false + GovernorQueue::proposal_needs_queuing(self, proposal_id) } + /// Delay between the proposal is created and the vote starts. The unit this duration is + /// expressed in depends on the clock (see ERC-6372) this contract uses. + /// + /// This can be increased to leave time for users to buy voting power, or delegate it, + /// before the voting of a proposal starts. fn voting_delay(self: @ComponentState) -> u64 { - 1 + GovernorVotes::voting_delay(self) } + /// Delay between the vote start and vote end. The unit this duration is expressed in + /// depends on the clock (see ERC-6372) this contract uses. + /// + /// NOTE: The `voting_delay` can delay the start of the vote. This must be considered when + /// setting the voting duration compared to the voting delay. + /// + /// NOTE: This value is stored when the proposal is submitted so that possible changes to + /// the value do not affect proposals that have already been submitted. fn voting_period(self: @ComponentState) -> u64 { - 1 + GovernorVotes::voting_period(self) } + /// Minimum number of cast voted required for a proposal to be successful. + /// + /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This + /// allows to scale the quorum depending on values such as the total supply of a token at + /// this timepoint. fn quorum(self: @ComponentState, timepoint: u64) -> u256 { - 1 + GovernorQuorum::quorum(self, timepoint) } + /// Voting power of an `account` at a specific `timepoint`. + /// + /// NOTE: this can be implemented in a number of ways, for example by reading the delegated + /// balance from one (or multiple) `ERC20Votes` tokens. fn get_votes( self: @ComponentState, account: ContractAddress, timepoint: u64 ) -> u256 { - 1 + self._get_votes(account, timepoint, self.default_params()) } + /// Voting power of an `account` at a specific `timepoint` given additional encoded + /// parameters. fn get_votes_with_params( self: @ComponentState, account: ContractAddress, timepoint: u64, params: Span ) -> u256 { - 1 + self._get_votes(account, timepoint, params) } + /// Returns whether `account` has cast a vote on `proposal_id`. fn has_voted( self: @ComponentState, proposal_id: felt252, account: ContractAddress ) -> bool { - false + GovernorCounting::has_voted(self, proposal_id, account) } + /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and + /// lasts for a duration specified by `voting_period`. Returns the id of the proposal. + /// + /// This function has opt-in frontrunning protection, described in + /// `is_valid_description_for_proposer`. + /// + /// NOTE: The state of the Governor and `targets` may change between the proposal creation + /// and its execution. This may be the result of third party actions on the targeted + /// contracts, or other governor proposals. For example, the balance of this contract could + /// be updated or its access control permissions may be modified, possibly compromising the + /// proposal's ability to execute successfully (e.g. the governor doesn't have enough value + /// to cover a proposal with multiple transfers). + /// + /// Requirements: + /// + /// - The proposer must be authorized to submit the proposal. + /// - The proposer must have enough votes to submit the proposal if `proposal_threshold` is + /// greater than zero. + /// - The proposal must not already exist. + /// + /// Emits a `ProposalCreated` event. fn propose( ref self: ComponentState, calls: Span, description: ByteArray ) -> felt252 { - 1 + let proposer = starknet::get_caller_address(); + + // Check descrption for restricted proposer + assert( + self.is_valid_description_for_proposer(proposer, @description), + Errors::RESTRICTED_PROPOSER + ); + + // Check proposal threshold + let vote_threshold = self._proposal_threshold(); + if vote_threshold > 0 { + let votes = self._get_votes(proposer, self.clock() - 1, self.default_params()); + assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); + } + + self._propose(calls, @description, proposer) } + /// Queues a proposal. Some governors require this step to be performed before execution can + /// happen. If queuing is not necessary, this function may revert. + /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the + /// deadline to be reached. + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Succeeded` state. + /// - The queue operation must return a non-zero ETA. + /// + /// Emits a `ProposalQueued` event. fn queue( ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { - 1 + let proposal_id = self._hash_proposal(calls, description_hash); + self.validate_state(proposal_id, array![ProposalState::Succeeded].span()); + + let eta_seconds = self.queue_operations(proposal_id, calls, description_hash); + assert(eta_seconds > 0, Errors::QUEUE_NOT_IMPLEMENTED); + + let mut proposal = self.Governor_proposals.read(proposal_id); + proposal.eta_seconds = eta_seconds; + self.Governor_proposals.write(proposal_id, proposal); + + self.emit(ProposalQueued { proposal_id, eta_seconds }); + + proposal_id } + /// Executes a successful proposal. This requires the quorum to be reached, the vote to be + /// successful, and the deadline to be reached. Depending on the governor it might also be + /// required that the proposal was queued and that some delay passed. + /// + /// NOTE: Some modules can modify the requirements for execution, for example by adding an + /// additional timelock (See `timelock_controller`). + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Succeeded` or `Queued` state. + /// + /// Emits a `ProposalExecuted` event. fn execute( ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { - 1 + let proposal_id = self._hash_proposal(calls, description_hash); + self + .validate_state( + proposal_id, array![ProposalState::Succeeded, ProposalState::Queued].span() + ); + + // Mark proposal as executed to avoid reentrancy + let mut proposal = self.Governor_proposals.read(proposal_id); + proposal.executed = true; + self.Governor_proposals.write(proposal_id, proposal); + + let self_executor = self.executor() == starknet::get_contract_address(); + // Register governance call in queue before execution + if self_executor { // TODO: save the calldatas in the governance_call queue + } + + self.execute_operations(proposal_id, calls, description_hash); + + // Clean up the governance call queue + if self_executor + && (@self) + .Governor_governance_call + .deref() + .len() + .into() > 0_u256 { // TODO: clean up the queue + } + + self.emit(ProposalExecuted { proposal_id }); + + proposal_id } + /// Cancels a proposal. A proposal is cancellable by the proposer, but only while it is + /// Pending state, i.e. before the vote starts. + /// + /// Returns the id of the proposal. + /// + /// Requirements: + /// + /// - The proposal must be in the `Pending` state. + /// + /// Emits a `ProposalCanceled` event. fn cancel( ref self: ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { - 1 + let proposal_id = self._hash_proposal(calls, description_hash); + self.validate_state(proposal_id, array![ProposalState::Pending].span()); + + assert( + starknet::get_caller_address() == self.proposal_proposer(proposal_id), + Errors::PROPOSER_ONLY + ); + + self._cancel(proposal_id, calls, description_hash) } + /// Cast a vote. + /// + /// Requirements: + /// + /// - The proposal must be active. + /// + /// Emits a `VoteCast` event. fn cast_vote( ref self: ComponentState, proposal_id: felt252, support: u8 ) -> u256 { - 1 + let voter = starknet::get_caller_address(); + self._cast_vote(proposal_id, voter, support, "", self.default_params()) } + /// Cast a vote with a reason. + /// + /// Requirements: + /// + /// - The proposal must be active. + /// + /// Emits a `VoteCast` event. fn cast_vote_with_reason( ref self: ComponentState, proposal_id: felt252, support: u8, reason: ByteArray ) -> u256 { - 1 + let voter = starknet::get_caller_address(); + self._cast_vote(proposal_id, voter, support, reason, self.default_params()) } + /// Cast a vote with a reason and additional serialized parameters. + /// + /// Requirements: + /// + /// - The proposal must be active. + /// + /// Emits a `VoteCast` event. fn cast_vote_with_reason_and_params( ref self: ComponentState, proposal_id: felt252, @@ -329,9 +588,11 @@ pub mod GovernorComponent { reason: ByteArray, params: Span ) -> u256 { - 1 + let voter = starknet::get_caller_address(); + self._cast_vote(proposal_id, voter, support, reason, params) } + /// TODO: implement vote by signature fn cast_vote_by_sig( ref self: ComponentState, proposal_id: felt252, @@ -342,6 +603,7 @@ pub mod GovernorComponent { 1 } + /// TODO: implement vote by signature fn cast_vote_with_reason_and_params_by_sig( ref self: ComponentState, proposal_id: felt252, @@ -363,10 +625,12 @@ pub mod GovernorComponent { pub impl InternalImpl< TContractState, +HasComponent, + +GovernorSettingsTrait, +GovernorCountingTrait, +GovernorExecuteTrait, - +GovernorVotesTrait, +GovernorQueueTrait, + impl GovernorPropose: GovernorProposeTrait, + impl GovernorVotes: GovernorVotesTrait, impl SRC5: SRC5Component::HasComponent, +Drop > of InternalTrait { @@ -390,7 +654,7 @@ pub mod GovernorComponent { /// Returns a hash of the proposal using the Poseidon algorithm. /// TODO: check if we should be using Pedersen hash instead of Poseidon. fn _hash_proposal( - ref self: ComponentState, calls: Span, description_hash: felt252 + self: @ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { let mut hashed_calls = array![]; @@ -409,114 +673,82 @@ pub mod GovernorComponent { poseidon_hash_span(hashed_calls.span()) } - /// Returns the state of a proposal, given its id. - fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState { - let proposal = self.proposals.read(proposal_id); - - if proposal.executed { - return ProposalState::Executed; - } - - if proposal.canceled { - return ProposalState::Canceled; - } - - let snapshot = self.proposal_snapshot(proposal_id); - - assert(snapshot.is_non_zero(), Errors::NONEXISTENT_PROPOSAL); - - let current_timepoint = self.clock(); - - if current_timepoint < snapshot { - return ProposalState::Pending; - } - - let deadline = self.proposal_deadline(proposal_id); - - if current_timepoint < deadline { - return ProposalState::Active; - } else if !self.quorum_reached(proposal_id) || !self.vote_succeeded(proposal_id) { - return ProposalState::Defeated; - } else if self.proposal_eta(proposal_id).is_zero() { - return ProposalState::Succeeded; - } else { - return ProposalState::Queued; - } + /// Internal wrapper for `GovernorVotesTrait::get_votes`. + fn _get_votes( + self: @ComponentState, + account: ContractAddress, + timepoint: u64, + params: Span + ) -> u256 { + GovernorVotes::get_votes(self, account, timepoint, params) } - /// The number of votes required in order for a voter to become a proposer. - fn proposal_threshold(self: @ComponentState) -> u256 { - 0 + /// Internal wrapper for `GovernorProposeTrait::proposal_threshold`. + fn _proposal_threshold(self: @ComponentState) -> u256 { + GovernorPropose::proposal_threshold(self) } /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot /// is performed at the end of this block. Hence, voting for this proposal starts at the /// beginning of the following block. - fn proposal_snapshot(self: @ComponentState, proposal_id: felt252) -> u64 { - self.proposals.read(proposal_id).vote_start + fn _proposal_snapshot(self: @ComponentState, proposal_id: felt252) -> u64 { + self.Governor_proposals.read(proposal_id).vote_start } /// Timepoint at which votes close. If using block number, votes close at the end of this /// block, so it is possible to cast a vote during this block. - fn proposal_deadline(self: @ComponentState, proposal_id: felt252) -> u64 { - let proposal = self.proposals.read(proposal_id); + fn _proposal_deadline(self: @ComponentState, proposal_id: felt252) -> u64 { + let proposal = self.Governor_proposals.read(proposal_id); proposal.vote_start + proposal.vote_duration } /// The account that created a proposal. - fn proposal_proposer( + fn _proposal_proposer( self: @ComponentState, proposal_id: felt252 ) -> ContractAddress { - self.proposals.read(proposal_id).proposer + self.Governor_proposals.read(proposal_id).proposer } /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` /// and `proposal_deadline`, this doesn't use the governor clock, and instead relies on the /// executor's clock which may be different. In most cases this will be a timestamp. - fn proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { - self.proposals.read(proposal_id).eta_seconds + fn _proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { + self.Governor_proposals.read(proposal_id).eta_seconds } - /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and - /// lasts for a duration specified by `voting_period`. Returns the id of the proposal. - /// - /// This function has opt-in frontrunning protection, described in - /// `is_valid_description_for_proposer`. - /// - /// NOTE: The state of the Governor and `targets` may change between the proposal creation - /// and its execution. This may be the result of third party actions on the targeted - /// contracts, or other governor proposals. For example, the balance of this contract could - /// be updated or its access control permissions may be modified, possibly compromising the - /// proposal's ability to execute successfully (e.g. the governor doesn't have enough value - /// to cover a proposal with multiple transfers). - /// - /// Requirements: - /// - /// - The proposer must be authorized to submit the proposal. - /// - The proposer must have enough votes to submit the proposal if `proposal_threshold` is - /// greater than zero. - /// - The proposal must not already exist. - /// - /// Emits a `ProposalCreated` event. - fn propose( - ref self: ComponentState, calls: Span, description: ByteArray - ) -> felt252 { - let proposer = starknet::get_caller_address(); + /// Returns the state of a proposal, given its id. + fn _state(self: @ComponentState, proposal_id: felt252) -> ProposalState { + let proposal = self.Governor_proposals.read(proposal_id); - // Check descrption for restricted proposer - assert( - self.is_valid_description_for_proposer(proposer, @description), - Errors::RESTRICTED_PROPOSER - ); + if proposal.executed { + return ProposalState::Executed; + } - // Check proposal threshold - let vote_threshold = self.proposal_threshold(); - if vote_threshold > 0 { - let votes = self.get_votes(proposer, self.clock() - 1, array![].span()); - assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); + if proposal.canceled { + return ProposalState::Canceled; } - self._propose(calls, @description, proposer) + let snapshot = self._proposal_snapshot(proposal_id); + + assert(snapshot.is_non_zero(), Errors::NONEXISTENT_PROPOSAL); + + let current_timepoint = self.clock(); + + if current_timepoint < snapshot { + return ProposalState::Pending; + } + + let deadline = self._proposal_deadline(proposal_id); + + if current_timepoint < deadline { + return ProposalState::Active; + } else if !self.quorum_reached(proposal_id) || !self.vote_succeeded(proposal_id) { + return ProposalState::Defeated; + } else if self._proposal_eta(proposal_id).is_zero() { + return ProposalState::Succeeded; + } else { + return ProposalState::Queued; + } } /// Internal propose mechanism. Returns the proposal id. @@ -532,9 +764,9 @@ pub mod GovernorComponent { description: @ByteArray, proposer: ContractAddress ) -> felt252 { - let proposal_id = self.hash_proposal(calls, description.hash()); + let proposal_id = self._hash_proposal(calls, description.hash()); - assert(self.proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL); + assert(self.Governor_proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL); let snapshot = self.clock() + self.voting_delay(); let duration = self.voting_period(); @@ -548,7 +780,7 @@ pub mod GovernorComponent { eta_seconds: 0 }; - self.proposals.write(proposal_id, proposal); + self.Governor_proposals.write(proposal_id, proposal); self .emit( @@ -566,106 +798,6 @@ pub mod GovernorComponent { proposal_id } - /// Queues a proposal. Some governors require this step to be performed before execution can - /// happen. If queuing is not necessary, this function may revert. - /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the - /// deadline to be reached. - /// - /// Returns the id of the proposal. - /// - /// Requirements: - /// - /// - The proposal must be in the `Succeeded` state. - /// - The queue operation must return a non-zero ETA. - /// - /// Emits a `ProposalQueued` event. - fn queue( - ref self: ComponentState, calls: Span, description_hash: felt252 - ) -> felt252 { - let proposal_id = self.hash_proposal(calls, description_hash); - self.validate_state(proposal_id, array![ProposalState::Succeeded].span()); - - let eta_seconds = self.queue_operations(proposal_id, calls, description_hash); - assert(eta_seconds > 0, Errors::QUEUE_NOT_IMPLEMENTED); - - let mut proposal = self.proposals.read(proposal_id); - proposal.eta_seconds = eta_seconds; - self.proposals.write(proposal_id, proposal); - - self.emit(ProposalQueued { proposal_id, eta_seconds }); - - proposal_id - } - - /// Executes a successful proposal. This requires the quorum to be reached, the vote to be - /// successful, and the deadline to be reached. Depending on the governor it might also be - /// required that the proposal was queued and that some delay passed. - /// - /// NOTE: Some modules can modify the requirements for execution, for example by adding an - /// additional timelock (See `timelock_controller`). - /// - /// Returns the id of the proposal. - /// - /// Requirements: - /// - /// - The proposal must be in the `Succeeded` or `Queued` state. - /// - /// Emits a `ProposalExecuted` event. - fn execute( - ref self: ComponentState, calls: Span, description_hash: felt252 - ) -> felt252 { - let proposal_id = self.hash_proposal(calls, description_hash); - self - .validate_state( - proposal_id, array![ProposalState::Succeeded, ProposalState::Queued].span() - ); - - // Mark proposal as executed to avoid reentrancy - let mut proposal = self.proposals.read(proposal_id); - proposal.executed = true; - self.proposals.write(proposal_id, proposal); - - let self_executor = self.executor() == starknet::get_contract_address(); - // Register governance call in queue before execution - if self_executor { // TODO: Save the calldatas in the governance_call queue - } - - self.execute_operations(proposal_id, calls, description_hash); - - // Clean up the governance call queue - if self_executor - && (@self).governance_call.deref().len() > 0 { // TODO: Clean up the queue - } - - self.emit(ProposalExecuted { proposal_id }); - - proposal_id - } - - /// Cancels a proposal. A proposal is cancellable by the proposer, but only while it is - /// Pending state, i.e. before the vote starts. - /// - /// Returns the id of the proposal. - /// - /// Requirements: - /// - /// - The proposal must be in the `Pending` state. - /// - /// Emits a `ProposalCanceled` event. - fn cancel( - ref self: ComponentState, calls: Span, description_hash: felt252 - ) -> felt252 { - let proposal_id = self.hash_proposal(calls, description_hash); - self.validate_state(proposal_id, array![ProposalState::Pending].span()); - - assert( - starknet::get_caller_address() == self.proposal_proposer(proposal_id), - Errors::PROPOSER_ONLY - ); - - self._cancel(proposal_id, calls, description_hash) - } - /// Internal cancel mechanism with minimal restrictions. Returns the id of the proposal. /// /// Requirements: @@ -689,66 +821,61 @@ pub mod GovernorComponent { ]; self.validate_state(proposal_id, valid_states.span()); - let mut proposal = self.proposals.read(proposal_id); + let mut proposal = self.Governor_proposals.read(proposal_id); proposal.canceled = true; - self.proposals.write(proposal_id, proposal); + self.Governor_proposals.write(proposal_id, proposal); self.emit(ProposalCanceled { proposal_id }); proposal_id } - /// Cast a vote. - fn cast_vote( - ref self: ComponentState, proposal_id: felt252, support: u8 - ) -> u256 { - 1 - } - - /// Cast a vote with a reason. - fn cast_vote_with_reason( - ref self: ComponentState, - proposal_id: felt252, - support: u8, - reason: ByteArray - ) -> u256 { - 1 - } - - /// Cast a vote with a reason and additional serialized parameters. - fn cast_vote_with_reason_and_params( + /// Internal wrapper for `GovernorCountingTrait::count_vote`. + fn _count_vote( ref self: ComponentState, proposal_id: felt252, + account: ContractAddress, support: u8, - reason: ByteArray, + total_weight: u256, params: Span ) -> u256 { - 1 + self.count_vote(proposal_id, account, support, total_weight, params) } + /// Internal vote casting mechanism. + /// + /// Checks that the vote is pending, that it has not been cast yet, retrieve + /// voting weight using `get_votes` and call the `_count_vote` internal function. + /// + /// Emits a `VoteCast` event. fn _cast_vote( ref self: ComponentState, proposal_id: felt252, - account: ContractAddress, - support: u8, - reason: ByteArray, - ) -> u256 { - 1 - } - - fn _cast_vote_with_params( - ref self: ComponentState, - proposal_id: felt252, - account: ContractAddress, + voter: ContractAddress, support: u8, reason: ByteArray, params: Span ) -> u256 { self.validate_state(proposal_id, array![ProposalState::Active].span()); - // let total_weight = self.get_votes(account, self.clock() - 1); + let snapshot = self._proposal_snapshot(proposal_id); + let total_weight = self._get_votes(voter, snapshot, params); + let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, params); - 1 + if params.is_empty() { + self.emit(VoteCast { voter, proposal_id, support, weight: voted_weight, reason }); + } else { + self + .emit( + VoteCastWithParams { + voter, proposal_id, support, weight: voted_weight, reason, params + } + ); + } + + // TODO: check if the tally hook must be used + + voted_weight } /// Checks if the proposer is authorized to submit a proposal with the given description. @@ -799,7 +926,7 @@ pub mod GovernorComponent { proposal_id: felt252, allowed_states: Span ) { - let current_state = self.state(proposal_id); + let current_state = self._state(proposal_id); let mut found = false; for state in allowed_states { if current_state == *state { diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index ff6563d98..dd89048ce 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (governance/governor/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (governance/governor/interface.cairo) use starknet::ContractAddress; use starknet::account::Call; pub const IGOVERNOR_ID: felt252 = 0x1f; // TODO: Update this value. +/// Interface for a contract that implements the ERC-6372 standard. #[starknet::interface] pub trait IERC6372 { /// Clock used for flagging checkpoints. @@ -68,7 +69,7 @@ pub trait IGovernor { fn state(self: @TState, proposal_id: felt252) -> ProposalState; /// The number of votes required in order for a voter to become a proposer. - fn proposal_threshold(self: @TState, proposal_id: felt252) -> u256; + fn proposal_threshold(self: @TState) -> u256; /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot is /// performed at the end of this block. Hence, voting for this proposal starts at the beginning diff --git a/packages/utils/src/structs/double_ended_queue.cairo b/packages/utils/src/structs/double_ended_queue.cairo index 615c9520f..db65a16d3 100644 --- a/packages/utils/src/structs/double_ended_queue.cairo +++ b/packages/utils/src/structs/double_ended_queue.cairo @@ -85,7 +85,7 @@ pub impl DoubleEndedQueueImpl of DoubleEndedQueueTrait { } /// Returns the number of items in the queue. - fn len(self: StoragePath) -> u32 { + fn len(self: StoragePath) -> felt252 { self._end.read() - self._begin.read() } From d7722c8f4875c5c8cf5f499f6d6977be4539f212 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 28 Oct 2024 17:11:46 +0100 Subject: [PATCH 38/76] feat: add votes quorum fractional extension main logic --- .../governance/src/governor/extensions.cairo | 2 + .../extensions/governor_counting_simple.cairo | 26 ++- .../governor_votes_quorum_fraction.cairo | 174 ++++++++++++++++++ .../src/governor/extensions/interface.cairo | 14 ++ .../governance/src/governor/governor.cairo | 4 +- 5 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo create mode 100644 packages/governance/src/governor/extensions/interface.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index 12dccb9c9..a43fd463e 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -1 +1,3 @@ pub mod governor_counting_simple; +pub mod governor_votes_quorum_fraction; +pub mod interface; diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 870508d03..74fa69c4c 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -8,7 +8,7 @@ #[starknet::component] pub mod GovernorCountingSimpleComponent { use crate::governor::GovernorComponent::{ - GovernorCountingTrait, InternalImpl, ComponentState as GovernorComponentState + InternalImpl, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use openzeppelin_introspection::src5::SRC5Component; @@ -20,7 +20,7 @@ pub mod GovernorCountingSimpleComponent { #[storage] pub struct Storage { - GovernorCountingSimple_proposals_votes: Map, + Governor_proposals_votes: Map, } /// Supported vote types. @@ -54,6 +54,10 @@ pub mod GovernorCountingSimpleComponent { pub const INVALID_VOTE_TYPE: felt252 = 'Invalid vote type'; } + // + // Extensions + // + impl GovernorCounting< TContractState, +GovernorComponent::HasComponent, @@ -66,7 +70,7 @@ pub mod GovernorCountingSimpleComponent { +SRC5Component::HasComponent, impl GovernorCountingSimple: HasComponent, +Drop - > of GovernorCountingTrait { + > of GovernorComponent::GovernorCountingTrait { /// See `GovernorComponent::GovernorCountingTrait::counting_mode`. fn counting_mode(self: @GovernorComponentState) -> ByteArray { return "support=bravo&quorum=for,abstain"; @@ -86,9 +90,7 @@ pub mod GovernorCountingSimpleComponent { let mut contract = self.get_contract_mut(); let mut this_component = GovernorCountingSimple::get_component_mut(ref contract); - let proposal_votes = this_component - .GovernorCountingSimple_proposals_votes - .entry(proposal_id); + let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id); assert(!proposal_votes.has_voted.read(account), Errors::ALREADY_CAST_VOTE); proposal_votes.has_voted.write(account, true); @@ -119,9 +121,7 @@ pub mod GovernorCountingSimpleComponent { ) -> bool { let contract = self.get_contract(); let this_component = GovernorCountingSimple::get_component(contract); - let proposal_votes = this_component - .GovernorCountingSimple_proposals_votes - .entry(proposal_id); + let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id); proposal_votes.has_voted.read(account) } @@ -133,9 +133,7 @@ pub mod GovernorCountingSimpleComponent { let contract = self.get_contract(); let this_component = GovernorCountingSimple::get_component(contract); - let proposal_votes = this_component - .GovernorCountingSimple_proposals_votes - .entry(proposal_id); + let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id); let snapshot = self._proposal_snapshot(proposal_id); self.quorum(snapshot) <= proposal_votes.for_votes.read() @@ -150,9 +148,7 @@ pub mod GovernorCountingSimpleComponent { ) -> bool { let contract = self.get_contract(); let this_component = GovernorCountingSimple::get_component(contract); - let proposal_votes = this_component - .GovernorCountingSimple_proposals_votes - .entry(proposal_id); + let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id); proposal_votes.for_votes.read() > proposal_votes.against_votes.read() } diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo new file mode 100644 index 000000000..755f6f8e7 --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_votes_quorum_fractional.cairo) + +/// # GovernorVotesQuorumFraction Component +/// +/// Extension of GovernorComponent for voting weight extraction from an ERC20 token with the Votes +/// extension and a quorum expressed as a fraction of the total supply. +#[starknet::component] +pub mod GovernorVotesQuorumFractionComponent { + use crate::governor::GovernorComponent::{ + InternalImpl as GovernorInternalImpl, ComponentState as GovernorComponentState + }; + use crate::governor::GovernorComponent; + use crate::governor::extensions::interface::IQuorumFraction; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; + use starknet::ContractAddress; + + #[storage] + pub struct Storage { + Governor_quorum_numerator_history: Trace, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + QuorumNumeratorUpdated: QuorumNumeratorUpdated + } + + /// Emitted when the quorum numerator is updated. + #[derive(Drop, starknet::Event)] + pub struct QuorumNumeratorUpdated { + pub old_quorum_numerator: u256, + pub new_quorum_numerator: u256 + } + + mod Errors { + pub const INVALID_QUORUM_FRACTION: felt252 = 'Invalid quorum fraction'; + } + + // + // Extensions + // + + impl GovernorVotes< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorQuorumTrait, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorExecuteTrait, + +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorProposeTrait, + +GovernorComponent::GovernorCountingTrait, + +SRC5Component::HasComponent, + impl GovernorVotesQuorumFraction: HasComponent, + +Drop + > of GovernorComponent::GovernorVotesTrait { + /// See `GovernorComponent::GovernorVotesTrait::clock`. + fn clock(self: @GovernorComponentState) -> u64 { + 1 + } + + /// See `GovernorComponent::GovernorVotesTrait::CLOCK_MODE`. + fn clock_mode(self: @GovernorComponentState) -> ByteArray { + "" + } + + /// See `GovernorComponent::GovernorVotesTrait::voting_delay`. + fn voting_delay(self: @GovernorComponentState) -> u64 { + 1 + } + + /// See `GovernorComponent::GovernorVotesTrait::voting_period`. + fn voting_period(self: @GovernorComponentState) -> u64 { + 1 + } + + /// See `GovernorComponent::GovernorVotesTrait::get_votes`. + fn get_votes( + self: @GovernorComponentState, + account: ContractAddress, + timepoint: u64, + params: Span + ) -> u256 { + 1 + } + } + + // + // External + // + + #[embeddable_as(QuorumFractionImpl)] + impl QuorumFraction< + TContractState, +HasComponent, +Drop + > of IQuorumFraction> { + /// Returns the current quorum numerator. + fn current_quorum_numerator(self: @ComponentState) -> u256 { + self.Governor_quorum_numerator_history.deref().latest() + } + + /// Returns the quorum numerator at a specific timepoint. + fn quorum_numerator(self: @ComponentState, timepoint: u64) -> u256 { + // Optimistic search: check the latest checkpoint. + // The initializer call ensures that there is at least one checkpoint in the history. + // + // NOTE: This optimization is specially helpful when the supply is not updated often. + let (_, key, value) = self + .Governor_quorum_numerator_history + .deref() + .latest_checkpoint(); + + if key <= timepoint { + return value; + } + + // Fallback to the binary search + self.Governor_quorum_numerator_history.deref().upper_lookup(timepoint) + } + + /// Returns the quorum denominator. + fn quorum_denominator(self: @ComponentState) -> u256 { + 100 + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +GovernorComponent::GovernorVotesTrait, + impl Governor: GovernorComponent::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the component by setting the initial quorum numerator value. + /// + /// Requirements: + /// + /// - `quorum_numerator` must be less than `quorum_denominator`. + /// + /// Emits a `QuorumNumeratorUpdated` event. + fn initialize(ref self: ComponentState, quorum_numerator: u256) { + self.update_quorum_numerator(quorum_numerator); + } + + /// Updates the quorum numerator. + /// + /// Requirements: + /// + /// - `new_quorum_numerator` must be less than `quorum_denominator`. + /// + /// Emits a `QuorumNumeratorUpdated` event. + fn update_quorum_numerator( + ref self: ComponentState, new_quorum_numerator: u256 + ) { + let denominator = self.quorum_denominator(); + + assert(new_quorum_numerator <= denominator, Errors::INVALID_QUORUM_FRACTION); + + let old_quorum_numerator = self.current_quorum_numerator(); + let governor_component = get_dep_component_mut!(ref self, Governor); + let clock = governor_component.clock(); + + self.Governor_quorum_numerator_history.deref().push(clock, new_quorum_numerator); + + self.emit(QuorumNumeratorUpdated { old_quorum_numerator, new_quorum_numerator }); + } + } +} diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo new file mode 100644 index 000000000..a88b3965e --- /dev/null +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 (governance/governor/extensions/interface.cairo) + +#[starknet::interface] +pub trait IQuorumFraction { + /// Returns the current quorum numerator. + fn current_quorum_numerator(self: @TState) -> u256; + + /// Returns the quorum numerator at a specific timepoint. + fn quorum_numerator(self: @TState, timepoint: u64) -> u256; + + /// Returns the quorum denominator. + fn quorum_denominator(self: @TState) -> u256; +} diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 4882fc2ad..07bd51369 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -766,7 +766,9 @@ pub mod GovernorComponent { ) -> felt252 { let proposal_id = self._hash_proposal(calls, description.hash()); - assert(self.Governor_proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL); + assert( + self.Governor_proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL + ); let snapshot = self.clock() + self.voting_delay(); let duration = self.voting_period(); From fab1a6d2ab4c3b47534c416a52339b172c5cad19 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 30 Oct 2024 13:35:54 +0100 Subject: [PATCH 39/76] feat: add settings extension --- .../governance/src/governor/extensions.cairo | 1 + .../extensions/governor_counting_simple.cairo | 6 +- .../extensions/governor_settings.cairo | 158 ++++++++++++++++++ .../governor_votes_quorum_fraction.cairo | 53 +++--- .../src/governor/extensions/interface.cairo | 17 ++ .../governance/src/governor/governor.cairo | 130 +++++++------- .../governance/src/governor/interface.cairo | 10 +- 7 files changed, 287 insertions(+), 88 deletions(-) create mode 100644 packages/governance/src/governor/extensions/governor_settings.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index a43fd463e..04dbd9972 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -1,3 +1,4 @@ pub mod governor_counting_simple; +pub mod governor_settings; pub mod governor_votes_quorum_fraction; pub mod interface; diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 74fa69c4c..c5d3afc33 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -60,12 +60,12 @@ pub mod GovernorCountingSimpleComponent { impl GovernorCounting< TContractState, + +GovernorComponent::ImmutableConfig, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorQuorumTrait, +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorQuorumTrait, +GovernorComponent::GovernorExecuteTrait, +GovernorComponent::GovernorQueueTrait, - +GovernorComponent::GovernorProposeTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, impl GovernorCountingSimple: HasComponent, @@ -85,7 +85,7 @@ pub mod GovernorCountingSimpleComponent { account: ContractAddress, support: u8, total_weight: u256, - params: Span + params: @ByteArray ) -> u256 { let mut contract = self.get_contract_mut(); let mut this_component = GovernorCountingSimple::get_component_mut(ref contract); diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo new file mode 100644 index 000000000..78d190c98 --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_settings.cairo) + +/// # GovernorSettings Component +/// +/// Extension of GovernorComponent for settings updatable through governance. +#[starknet::component] +pub mod GovernorSettingsComponent { + use crate::governor::GovernorComponent::{ + InternalImpl, ComponentState as GovernorComponentState + }; + use crate::governor::GovernorComponent; + use crate::governor::extensions::interface::ISetSettings; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + pub struct Storage { + Governor_voting_delay: u64, + Governor_voting_period: u64, + Governor_proposal_threshold: u256, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + VotingDelayUpdated: VotingDelayUpdated, + VotingPeriodUpdated: VotingPeriodUpdated, + ProposalThresholdUpdated: ProposalThresholdUpdated + } + + /// Emitted when `Governor_voting_delay` is updated. + #[derive(Drop, starknet::Event)] + pub struct VotingDelayUpdated { + pub old_voting_delay: u64, + pub new_voting_delay: u64 + } + + /// Emitted when `Governor_voting_period` is updated. + #[derive(Drop, starknet::Event)] + pub struct VotingPeriodUpdated { + pub old_voting_period: u64, + pub new_voting_period: u64 + } + + /// Emitted when `Governor_proposal_threshold` is updated. + #[derive(Drop, starknet::Event)] + pub struct ProposalThresholdUpdated { + pub old_proposal_threshold: u256, + pub new_proposal_threshold: u256 + } + + mod Errors { + pub const INVALID_VOTING_PERIOD: felt252 = 'Invalid voting period'; + } + + // + // Extensions + // + + impl GovernorSettings< + TContractState, + +GovernorComponent::ImmutableConfig, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorQuorumTrait, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorVotesTrait, + +GovernorComponent::GovernorExecuteTrait, + +GovernorComponent::GovernorQueueTrait, + +SRC5Component::HasComponent, + impl GovernorSettings: HasComponent, + +Drop + > of GovernorComponent::GovernorSettingsTrait { + /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`. + fn voting_delay(self: @GovernorComponentState) -> u64 { + let contract = self.get_contract(); + let this_component = GovernorSettings::get_component(contract); + + this_component.Governor_voting_delay.read() + } + + /// See `GovernorComponent::GovernorSettingsTrait::voting_period`. + fn voting_period(self: @GovernorComponentState) -> u64 { + let contract = self.get_contract(); + let this_component = GovernorSettings::get_component(contract); + + this_component.Governor_voting_period.read() + } + + /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`. + fn proposal_threshold(self: @GovernorComponentState) -> u256 { + let contract = self.get_contract(); + let this_component = GovernorSettings::get_component(contract); + + this_component.Governor_proposal_threshold.read() + } + } + + // + // External + // + + #[embeddable_as(SetSettingsImpl)] + impl SetSettings< + TContractState, +HasComponent, +Drop + > of ISetSettings> { + /// Sets the voting delay. + /// + /// Emits a `VotingDelayUpdated` event. + fn set_voting_delay(ref self: ComponentState, voting_delay: u64) { + self + .emit( + VotingDelayUpdated { + old_voting_delay: self.Governor_voting_delay.read(), + new_voting_delay: voting_delay + } + ); + self.Governor_voting_delay.write(voting_delay); + } + + /// Sets the voting period. + /// + /// Requirements: + /// + /// - `voting_period` must be greater than 0. + /// + /// Emits a `VotingPeriodUpdated` event. + fn set_voting_period(ref self: ComponentState, voting_period: u64) { + assert(voting_period > 0, Errors::INVALID_VOTING_PERIOD); + + self + .emit( + VotingPeriodUpdated { + old_voting_period: self.Governor_voting_period.read(), + new_voting_period: voting_period + } + ); + self.Governor_voting_period.write(voting_period); + } + + /// Sets the proposal threshold. + /// + /// Emits a `ProposalThresholdUpdated` event. + fn set_proposal_threshold( + ref self: ComponentState, proposal_threshold: u256 + ) { + self + .emit( + ProposalThresholdUpdated { + old_proposal_threshold: self.Governor_proposal_threshold.read(), + new_proposal_threshold: proposal_threshold + } + ); + self.Governor_proposal_threshold.write(proposal_threshold); + } + } +} diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 755f6f8e7..6bea513f9 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.18.0 -// (governance/governor/extensions/governor_votes_quorum_fractional.cairo) +// (governance/governor/extensions/governor_votes_quorum_fraction.cairo) /// # GovernorVotesQuorumFraction Component /// @@ -8,17 +8,21 @@ /// extension and a quorum expressed as a fraction of the total supply. #[starknet::component] pub mod GovernorVotesQuorumFractionComponent { + use core::num::traits::Zero; use crate::governor::GovernorComponent::{ InternalImpl as GovernorInternalImpl, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::IQuorumFraction; + use crate::votes::interface::{IVotesDispatcher, IVotesDispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait}; use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; #[storage] pub struct Storage { + Governor_token: ContractAddress, Governor_quorum_numerator_history: Trace, } @@ -37,6 +41,7 @@ pub mod GovernorVotesQuorumFractionComponent { mod Errors { pub const INVALID_QUORUM_FRACTION: felt252 = 'Invalid quorum fraction'; + pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; } // @@ -47,10 +52,8 @@ pub mod GovernorVotesQuorumFractionComponent { TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorQuorumTrait, - +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorExecuteTrait, +GovernorComponent::GovernorQueueTrait, - +GovernorComponent::GovernorProposeTrait, +GovernorComponent::GovernorCountingTrait, +SRC5Component::HasComponent, impl GovernorVotesQuorumFraction: HasComponent, @@ -58,22 +61,14 @@ pub mod GovernorVotesQuorumFractionComponent { > of GovernorComponent::GovernorVotesTrait { /// See `GovernorComponent::GovernorVotesTrait::clock`. fn clock(self: @GovernorComponentState) -> u64 { - 1 + // VotesComponent uses the block timestamp for tracking checkpoints. + // That should be updated in order to allow for more flexible clock modes. + starknet::get_block_timestamp() } /// See `GovernorComponent::GovernorVotesTrait::CLOCK_MODE`. fn clock_mode(self: @GovernorComponentState) -> ByteArray { - "" - } - - /// See `GovernorComponent::GovernorVotesTrait::voting_delay`. - fn voting_delay(self: @GovernorComponentState) -> u64 { - 1 - } - - /// See `GovernorComponent::GovernorVotesTrait::voting_period`. - fn voting_period(self: @GovernorComponentState) -> u64 { - 1 + "mode=timestamp&from=starknet::SN_MAIN" } /// See `GovernorComponent::GovernorVotesTrait::get_votes`. @@ -81,9 +76,15 @@ pub mod GovernorVotesQuorumFractionComponent { self: @GovernorComponentState, account: ContractAddress, timepoint: u64, - params: Span + params: @ByteArray ) -> u256 { - 1 + let contract = self.get_contract(); + let this_component = GovernorVotesQuorumFraction::get_component(contract); + + let token = this_component.Governor_token.read(); + let votes_dispatcher = IVotesDispatcher { contract_address: token }; + + votes_dispatcher.get_past_votes(account, timepoint) } } @@ -95,6 +96,11 @@ pub mod GovernorVotesQuorumFractionComponent { impl QuorumFraction< TContractState, +HasComponent, +Drop > of IQuorumFraction> { + /// Returns the token that voting power is sourced from. + fn token(self: @ComponentState) -> ContractAddress { + self.Governor_token.read() + } + /// Returns the current quorum numerator. fn current_quorum_numerator(self: @ComponentState) -> u256 { self.Governor_quorum_numerator_history.deref().latest() @@ -137,14 +143,23 @@ pub mod GovernorVotesQuorumFractionComponent { impl Governor: GovernorComponent::HasComponent, +Drop > of InternalTrait { - /// Initializes the component by setting the initial quorum numerator value. + /// Initializes the component by setting the votes token and the + /// initial quorum numerator value. /// /// Requirements: /// + /// - `votes_token` must not be zero. /// - `quorum_numerator` must be less than `quorum_denominator`. /// /// Emits a `QuorumNumeratorUpdated` event. - fn initialize(ref self: ComponentState, quorum_numerator: u256) { + fn initialize( + ref self: ComponentState, + votes_token: ContractAddress, + quorum_numerator: u256 + ) { + assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN); + + self.Governor_token.write(votes_token); self.update_quorum_numerator(quorum_numerator); } diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index a88b3965e..c05d7be64 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -1,8 +1,13 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.18.0 (governance/governor/extensions/interface.cairo) +use starknet::ContractAddress; + #[starknet::interface] pub trait IQuorumFraction { + /// Returns the token that voting power is sourced from. + fn token(self: @TState) -> ContractAddress; + /// Returns the current quorum numerator. fn current_quorum_numerator(self: @TState) -> u256; @@ -12,3 +17,15 @@ pub trait IQuorumFraction { /// Returns the quorum denominator. fn quorum_denominator(self: @TState) -> u256; } + +#[starknet::interface] +pub trait ISetSettings { + /// Sets the voting delay. + fn set_voting_delay(ref self: TState, voting_delay: u64); + + /// Sets the voting period. + fn set_voting_period(ref self: TState, voting_period: u64); + + /// Sets the proposal threshold. + fn set_proposal_threshold(ref self: TState, proposal_threshold: u256); +} diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 07bd51369..936c732a2 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -39,12 +39,12 @@ pub mod GovernorComponent { VoteCastWithParams: VoteCastWithParams } - // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC - // restriction. /// Emitted when `call` is scheduled as part of operation `id`. #[derive(Drop, starknet::Event)] pub struct ProposalCreated { + #[key] pub proposal_id: felt252, + #[key] pub proposer: ContractAddress, pub calls: Span, pub signatures: Span>, @@ -53,28 +53,28 @@ pub mod GovernorComponent { pub description: ByteArray } - // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is queued. #[derive(Drop, starknet::Event)] pub struct ProposalQueued { + #[key] pub proposal_id: felt252, pub eta_seconds: u64 } - // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is executed. #[derive(Drop, starknet::Event)] pub struct ProposalExecuted { + #[key] pub proposal_id: felt252 } - // TODO: maybe add indexed keys and rename members since we don't have the GovernorBravo BC // restriction. /// Emitted when a proposal is canceled. #[derive(Drop, starknet::Event)] pub struct ProposalCanceled { + #[key] pub proposal_id: felt252 } @@ -92,9 +92,8 @@ pub mod GovernorComponent { /// Emitted when a vote is cast with params. /// /// NOTE: `support` values should be seen as buckets. Their interpretation depends on the voting - /// module used. - /// `params` are additional encoded parameters. Their interpretation also depends on the voting - /// module used. + /// module used. `params` are additional encoded parameters. Their interpretation also + /// depends on the voting module used. #[derive(Drop, starknet::Event)] pub struct VoteCastWithParams { #[key] @@ -103,19 +102,28 @@ pub mod GovernorComponent { pub support: u8, pub weight: u256, pub reason: ByteArray, - pub params: Span + pub params: ByteArray } - // TODO: check prefix for errors mod Errors { - pub const EXECUTOR_ONLY: felt252 = 'Gov: executor only'; - pub const PROPOSER_ONLY: felt252 = 'Gov: proposer only'; - pub const NONEXISTENT_PROPOSAL: felt252 = 'Gov: nonexistent proposal'; - pub const EXISTENT_PROPOSAL: felt252 = 'Gov: existent proposal'; - pub const RESTRICTED_PROPOSER: felt252 = 'Gov: restricted proposer'; - pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Gov: insufficient votes'; - pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Gov: unexpected proposal state'; - pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Gov: queue not implemented'; + pub const EXECUTOR_ONLY: felt252 = 'Executor only'; + pub const PROPOSER_ONLY: felt252 = 'Proposer only'; + pub const NONEXISTENT_PROPOSAL: felt252 = 'Nonexistent proposal'; + pub const EXISTENT_PROPOSAL: felt252 = 'Existent proposal'; + pub const RESTRICTED_PROPOSER: felt252 = 'Restricted proposer'; + pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Insufficient votes'; + pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Unexpected proposal state'; + pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Queue not implemented'; + } + + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// - `DEFAULT_PARAMS`: Default additional encoded parameters used by cast_vote + /// methods that don't include them. + pub trait ImmutableConfig { + /// Temporary defined as a function since constant ByteArray is not supported yet. + fn DEFAULT_PARAMS() -> ByteArray; } // @@ -123,13 +131,15 @@ pub mod GovernorComponent { // pub trait GovernorSettingsTrait { - /// Default additional encoded parameters used by cast_vote methods that don't include them. - /// - /// Note: Should be defined by specific implementations to use an appropriate value, the - /// meaning of the additional params, in the context of that implementation - fn default_params(self: @ComponentState) -> Span; - } + /// See `interface::IGovernor::voting_delay`. + fn voting_delay(self: @ComponentState) -> u64; + /// See `interface::IGovernor::voting_period`. + fn voting_period(self: @ComponentState) -> u64; + + /// See `interface::IGovernor::proposal_threshold`. + fn proposal_threshold(self: @ComponentState) -> u256; + } pub trait GovernorQuorumTrait { /// See `interface::IGovernor::::quorum`. @@ -151,7 +161,7 @@ pub mod GovernorComponent { account: ContractAddress, support: u8, total_weight: u256, - params: Span + params: @ByteArray ) -> u256; /// See `interface::IGovernor::has_voted`. @@ -173,26 +183,15 @@ pub mod GovernorComponent { /// See `interface::IERC6372::CLOCK_MODE`. fn clock_mode(self: @ComponentState) -> ByteArray; - /// See `interface::IGovernor::voting_delay`. - fn voting_delay(self: @ComponentState) -> u64; - - /// See `interface::IGovernor::voting_period`. - fn voting_period(self: @ComponentState) -> u64; - /// See `interface::IGovernor::get_votes`. fn get_votes( self: @ComponentState, account: ContractAddress, timepoint: u64, - params: Span + params: @ByteArray ) -> u256; } - pub trait GovernorProposeTrait { - /// See `interface::IGovernor::proposal_threshold`. - fn proposal_threshold(self: @ComponentState) -> u256; - } - pub trait GovernorExecuteTrait { /// Address through which the governor executes action. /// Should be used to specify whether the module execute actions through another contract @@ -240,14 +239,14 @@ pub mod GovernorComponent { TContractState, +HasComponent, +SRC5Component::HasComponent, - +GovernorSettingsTrait, + +GovernorVotesTrait, +GovernorExecuteTrait, impl GovernorQuorum: GovernorQuorumTrait, impl GovernorCounting: GovernorCountingTrait, - impl GovernorPropose: GovernorProposeTrait, impl GovernorQueue: GovernorQueueTrait, - impl GovernorVotes: GovernorVotesTrait, + impl GovernorSettings: GovernorSettingsTrait, impl Metadata: SNIP12Metadata, + impl Immutable: ImmutableConfig, +Drop, > of IGovernor> { /// Name of the governor instance (used in building the SNIP-12 domain separator). @@ -345,7 +344,7 @@ pub mod GovernorComponent { /// This can be increased to leave time for users to buy voting power, or delegate it, /// before the voting of a proposal starts. fn voting_delay(self: @ComponentState) -> u64 { - GovernorVotes::voting_delay(self) + GovernorSettings::voting_delay(self) } /// Delay between the vote start and vote end. The unit this duration is expressed in @@ -357,7 +356,7 @@ pub mod GovernorComponent { /// NOTE: This value is stored when the proposal is submitted so that possible changes to /// the value do not affect proposals that have already been submitted. fn voting_period(self: @ComponentState) -> u64 { - GovernorVotes::voting_period(self) + GovernorSettings::voting_period(self) } /// Minimum number of cast voted required for a proposal to be successful. @@ -376,7 +375,7 @@ pub mod GovernorComponent { fn get_votes( self: @ComponentState, account: ContractAddress, timepoint: u64 ) -> u256 { - self._get_votes(account, timepoint, self.default_params()) + self._get_votes(account, timepoint, @Immutable::DEFAULT_PARAMS()) } /// Voting power of an `account` at a specific `timepoint` given additional encoded @@ -385,9 +384,9 @@ pub mod GovernorComponent { self: @ComponentState, account: ContractAddress, timepoint: u64, - params: Span + params: ByteArray ) -> u256 { - self._get_votes(account, timepoint, params) + self._get_votes(account, timepoint, @params) } /// Returns whether `account` has cast a vote on `proposal_id`. @@ -432,7 +431,8 @@ pub mod GovernorComponent { // Check proposal threshold let vote_threshold = self._proposal_threshold(); if vote_threshold > 0 { - let votes = self._get_votes(proposer, self.clock() - 1, self.default_params()); + let votes = self + ._get_votes(proposer, self.clock() - 1, @Immutable::DEFAULT_PARAMS()); assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); } @@ -554,7 +554,7 @@ pub mod GovernorComponent { ref self: ComponentState, proposal_id: felt252, support: u8 ) -> u256 { let voter = starknet::get_caller_address(); - self._cast_vote(proposal_id, voter, support, "", self.default_params()) + self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS()) } /// Cast a vote with a reason. @@ -571,7 +571,7 @@ pub mod GovernorComponent { reason: ByteArray ) -> u256 { let voter = starknet::get_caller_address(); - self._cast_vote(proposal_id, voter, support, reason, self.default_params()) + self._cast_vote(proposal_id, voter, support, reason, Immutable::DEFAULT_PARAMS()) } /// Cast a vote with a reason and additional serialized parameters. @@ -586,7 +586,7 @@ pub mod GovernorComponent { proposal_id: felt252, support: u8, reason: ByteArray, - params: Span + params: ByteArray ) -> u256 { let voter = starknet::get_caller_address(); self._cast_vote(proposal_id, voter, support, reason, params) @@ -610,7 +610,7 @@ pub mod GovernorComponent { support: u8, voter: ContractAddress, reason: ByteArray, - params: Span, + params: ByteArray, signature: Span ) -> u256 { 1 @@ -625,13 +625,13 @@ pub mod GovernorComponent { pub impl InternalImpl< TContractState, +HasComponent, - +GovernorSettingsTrait, +GovernorCountingTrait, +GovernorExecuteTrait, +GovernorQueueTrait, - impl GovernorPropose: GovernorProposeTrait, + impl GovernorSettings: GovernorSettingsTrait, impl GovernorVotes: GovernorVotesTrait, impl SRC5: SRC5Component::HasComponent, + impl Immutable: ImmutableConfig, +Drop > of InternalTrait { /// Initializes the contract by registering the supported interface Ids. @@ -678,14 +678,14 @@ pub mod GovernorComponent { self: @ComponentState, account: ContractAddress, timepoint: u64, - params: Span + params: @ByteArray ) -> u256 { GovernorVotes::get_votes(self, account, timepoint, params) } /// Internal wrapper for `GovernorProposeTrait::proposal_threshold`. fn _proposal_threshold(self: @ComponentState) -> u256 { - GovernorPropose::proposal_threshold(self) + GovernorSettings::proposal_threshold(self) } /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot @@ -839,7 +839,7 @@ pub mod GovernorComponent { account: ContractAddress, support: u8, total_weight: u256, - params: Span + params: @ByteArray ) -> u256 { self.count_vote(proposal_id, account, support, total_weight, params) } @@ -856,15 +856,15 @@ pub mod GovernorComponent { voter: ContractAddress, support: u8, reason: ByteArray, - params: Span + params: ByteArray ) -> u256 { self.validate_state(proposal_id, array![ProposalState::Active].span()); let snapshot = self._proposal_snapshot(proposal_id); - let total_weight = self._get_votes(voter, snapshot, params); - let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, params); + let total_weight = self._get_votes(voter, snapshot, @params); + let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, @params); - if params.is_empty() { + if params.len().is_zero() { self.emit(VoteCast { voter, proposal_id, support, weight: voted_weight, reason }); } else { self @@ -940,3 +940,15 @@ pub mod GovernorComponent { } } } + +/// Implementation of the default ERC2981Component ImmutableConfig. +/// +/// See +/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation +/// +/// The default fee denominator is set to `DEFAULT_FEE_DENOMINATOR`. +pub impl DefaultConfig of GovernorComponent::ImmutableConfig { + fn DEFAULT_PARAMS() -> ByteArray { + "" + } +} diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index dd89048ce..7cd7dcede 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -123,7 +123,7 @@ pub trait IGovernor { /// Voting power of an `account` at a specific `timepoint` given additional encoded parameters. fn get_votes_with_params( - self: @TState, account: ContractAddress, timepoint: u64, params: Span + self: @TState, account: ContractAddress, timepoint: u64, params: ByteArray ) -> u256; /// Returns whether `account` has cast a vote on `proposal_id`. @@ -176,11 +176,7 @@ pub trait IGovernor { /// Cast a vote with a reason and additional serialized parameters. fn cast_vote_with_reason_and_params( - ref self: TState, - proposal_id: felt252, - support: u8, - reason: ByteArray, - params: Span + ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray, params: ByteArray ) -> u256; /// Cast a vote using the voter's signature. @@ -199,7 +195,7 @@ pub trait IGovernor { support: u8, voter: ContractAddress, reason: ByteArray, - params: Span, + params: ByteArray, signature: Span ) -> u256; } From 86d595454c1c883363d0466b06afc247b4e5de65 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 30 Oct 2024 14:12:34 +0100 Subject: [PATCH 40/76] feat: add governor votes extension --- .../governance/src/governor/extensions.cairo | 1 + .../extensions/governor_settings.cairo | 4 +- .../governor/extensions/governor_votes.cairo | 108 ++++++++++++++++++ .../governor_votes_quorum_fraction.cairo | 37 +++++- .../src/governor/extensions/interface.cairo | 6 + .../governance/src/governor/governor.cairo | 2 +- 6 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 packages/governance/src/governor/extensions/governor_votes.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index 04dbd9972..ad1544f3c 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -1,4 +1,5 @@ pub mod governor_counting_simple; pub mod governor_settings; +pub mod governor_votes; pub mod governor_votes_quorum_fraction; pub mod interface; diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 78d190c98..a05830efc 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -7,9 +7,7 @@ /// Extension of GovernorComponent for settings updatable through governance. #[starknet::component] pub mod GovernorSettingsComponent { - use crate::governor::GovernorComponent::{ - InternalImpl, ComponentState as GovernorComponentState - }; + use crate::governor::GovernorComponent::ComponentState as GovernorComponentState; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::ISetSettings; use openzeppelin_introspection::src5::SRC5Component; diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo new file mode 100644 index 000000000..aee0d47ca --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_votes.cairo) + +/// # GovernorVotes Component +/// +/// Extension of GovernorComponent for voting weight extraction from a token with the Votes +/// extension. +#[starknet::component] +pub mod GovernorVotesComponent { + use core::num::traits::Zero; + use crate::governor::GovernorComponent::ComponentState as GovernorComponentState; + use crate::governor::GovernorComponent; + use crate::governor::extensions::interface::IVotesToken; + use crate::votes::interface::{IVotesDispatcher, IVotesDispatcherTrait}; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + pub struct Storage { + Governor_token: ContractAddress + } + + mod Errors { + pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; + } + + // + // Extensions + // + + impl GovernorVotes< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorExecuteTrait, + +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorCountingTrait, + +SRC5Component::HasComponent, + impl GovernorVotesQuorumFraction: HasComponent, + +Drop + > of GovernorComponent::GovernorVotesTrait { + /// See `GovernorComponent::GovernorVotesTrait::clock`. + fn clock(self: @GovernorComponentState) -> u64 { + // VotesComponent uses the block timestamp for tracking checkpoints. + // That should be updated in order to allow for more flexible clock modes. + starknet::get_block_timestamp() + } + + /// See `GovernorComponent::GovernorVotesTrait::CLOCK_MODE`. + fn clock_mode(self: @GovernorComponentState) -> ByteArray { + "mode=timestamp&from=starknet::SN_MAIN" + } + + /// See `GovernorComponent::GovernorVotesTrait::get_votes`. + fn get_votes( + self: @GovernorComponentState, + account: ContractAddress, + timepoint: u64, + params: @ByteArray + ) -> u256 { + let contract = self.get_contract(); + let this_component = GovernorVotesQuorumFraction::get_component(contract); + + let token = this_component.Governor_token.read(); + let votes_dispatcher = IVotesDispatcher { contract_address: token }; + + votes_dispatcher.get_past_votes(account, timepoint) + } + } + + // + // External + // + + #[embeddable_as(VotesTokenImpl)] + impl VotesToken< + TContractState, +HasComponent, +Drop + > of IVotesToken> { + /// Returns the token that voting power is sourced from. + fn token(self: @ComponentState) -> ContractAddress { + self.Governor_token.read() + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +GovernorComponent::GovernorVotesTrait, + impl Governor: GovernorComponent::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the component by setting the votes token. + /// + /// Requirements: + /// + /// - `votes_token` must not be zero. + fn initialize(ref self: ComponentState, votes_token: ContractAddress) { + assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN); + self.Governor_token.write(votes_token); + } + } +} diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 6bea513f9..1e3afd200 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -4,14 +4,12 @@ /// # GovernorVotesQuorumFraction Component /// -/// Extension of GovernorComponent for voting weight extraction from an ERC20 token with the Votes +/// Extension of GovernorComponent for voting weight extraction from a token with the Votes /// extension and a quorum expressed as a fraction of the total supply. #[starknet::component] pub mod GovernorVotesQuorumFractionComponent { use core::num::traits::Zero; - use crate::governor::GovernorComponent::{ - InternalImpl as GovernorInternalImpl, ComponentState as GovernorComponentState - }; + use crate::governor::GovernorComponent::ComponentState as GovernorComponentState; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::IQuorumFraction; use crate::votes::interface::{IVotesDispatcher, IVotesDispatcherTrait}; @@ -48,10 +46,36 @@ pub mod GovernorVotesQuorumFractionComponent { // Extensions // + impl GovernorQuorum< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorVotesTrait, + +GovernorComponent::GovernorExecuteTrait, + +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorCountingTrait, + +SRC5Component::HasComponent, + impl GovernorVotesQuorumFraction: HasComponent, + +Drop + > of GovernorComponent::GovernorQuorumTrait { + /// See `GovernorComponent::GovernorQuorumTrait::quorum`. + fn quorum(self: @GovernorComponentState, timepoint: u64) -> u256 { + let contract = self.get_contract(); + let this_component = GovernorVotesQuorumFraction::get_component(contract); + + let token = this_component.Governor_token.read(); + let votes_dispatcher = IVotesDispatcher { contract_address: token }; + + let past_total_supply = votes_dispatcher.get_past_total_supply(timepoint); + let quorum_numerator = this_component.quorum_numerator(timepoint); + let quorum_denominator = this_component.quorum_denominator(); + + past_total_supply * quorum_numerator / quorum_denominator + } + } + impl GovernorVotes< TContractState, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorQuorumTrait, +GovernorComponent::GovernorExecuteTrait, +GovernorComponent::GovernorQueueTrait, +GovernorComponent::GovernorCountingTrait, @@ -178,7 +202,8 @@ pub mod GovernorVotesQuorumFractionComponent { assert(new_quorum_numerator <= denominator, Errors::INVALID_QUORUM_FRACTION); let old_quorum_numerator = self.current_quorum_numerator(); - let governor_component = get_dep_component_mut!(ref self, Governor); + let governor_component = get_dep_component!(@self, Governor); + let clock = governor_component.clock(); self.Governor_quorum_numerator_history.deref().push(clock, new_quorum_numerator); diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index c05d7be64..f1b9458f0 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -18,6 +18,12 @@ pub trait IQuorumFraction { fn quorum_denominator(self: @TState) -> u256; } +#[starknet::interface] +pub trait IVotesToken { + /// Returns the token that voting power is sourced from. + fn token(self: @TState) -> ContractAddress; +} + #[starknet::interface] pub trait ISetSettings { /// Sets the voting delay. diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 936c732a2..4b7dc9af9 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -142,7 +142,7 @@ pub mod GovernorComponent { } pub trait GovernorQuorumTrait { - /// See `interface::IGovernor::::quorum`. + /// See `interface::IGovernor::quorum`. fn quorum(self: @ComponentState, timepoint: u64) -> u256; } From 7335f0f579c541176423d079f546712681f93578 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 30 Oct 2024 21:27:22 +0100 Subject: [PATCH 41/76] feat: add core execution extension --- .../governance/src/governor/extensions.cairo | 1 + .../extensions/governor_core_execution.cairo | 129 ++++++++++++++++++ .../extensions/governor_counting_simple.cairo | 3 +- .../extensions/governor_settings.cairo | 3 +- .../governor/extensions/governor_votes.cairo | 3 +- .../governor_votes_quorum_fraction.cairo | 6 +- .../governance/src/governor/governor.cairo | 50 +++---- 7 files changed, 154 insertions(+), 41 deletions(-) create mode 100644 packages/governance/src/governor/extensions/governor_core_execution.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index ad1544f3c..d5e37b5f8 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -1,3 +1,4 @@ +pub mod governor_core_execution; pub mod governor_counting_simple; pub mod governor_settings; pub mod governor_votes; diff --git a/packages/governance/src/governor/extensions/governor_core_execution.cairo b/packages/governance/src/governor/extensions/governor_core_execution.cairo new file mode 100644 index 000000000..1cbed3342 --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_core_execution.cairo @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_core_execution.cairo) + +/// # GovernorCoreExecution Component +/// +/// Extension of GovernorComponent providing an execution mechanism directly through +/// the Governor itself. For a timelocked execution mechanism, see +/// GovernorTimelockExecutionComponent. +#[starknet::component] +pub mod GovernorCoreExecutionComponent { + use core::num::traits::Zero; + use crate::governor::GovernorComponent::{ + InternalImpl as GovernorInternalImpl, ComponentState as GovernorComponentState + }; + use crate::governor::GovernorComponent; + use crate::governor::extensions::interface::IVotesToken; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::ContractAddress; + use starknet::SyscallResultTrait; + use starknet::account::Call; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + pub struct Storage { + Governor_token: ContractAddress + } + + mod Errors { + pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; + } + + // + // Extensions + // + + impl GovernorExecution< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + impl GovernorCoreExecution: HasComponent, + +Drop + > of GovernorComponent::GovernorExecutionTrait { + /// See `GovernorComponent::GovernorExecutionTrait::executor`. + fn executor(self: @GovernorComponentState) -> ContractAddress { + starknet::get_contract_address() + } + + /// See `GovernorComponent::GovernorExecutionTrait::execute_operations`. + fn execute_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) { + for call in calls { + let Call { to, selector, calldata } = *call; + starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall(); + }; + } + + /// See `GovernorComponent::GovernorExecutionTrait::queue_operations`. + fn queue_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) -> u64 { + 0 + } + + /// See `GovernorComponent::GovernorExecutionTrait::proposal_needs_queuing`. + fn proposal_needs_queuing( + self: @GovernorComponentState, proposal_id: felt252 + ) -> bool { + false + } + + /// See `GovernorComponent::GovernorExecutionTrait::cancel_operations`. + fn cancel_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + description_hash: felt252 + ) { + self._cancel(proposal_id, description_hash); + } + } + + // + // External + // + + #[embeddable_as(VotesTokenImpl)] + impl VotesToken< + TContractState, +HasComponent, +Drop + > of IVotesToken> { + /// Returns the token that voting power is sourced from. + fn token(self: @ComponentState) -> ContractAddress { + self.Governor_token.read() + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +GovernorComponent::GovernorVotesTrait, + impl Governor: GovernorComponent::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the component by setting the votes token. + /// + /// Requirements: + /// + /// - `votes_token` must not be zero. + fn initialize(ref self: ComponentState, votes_token: ContractAddress) { + assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN); + self.Governor_token.write(votes_token); + } + } +} diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index c5d3afc33..9a9470fb3 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -64,8 +64,7 @@ pub mod GovernorCountingSimpleComponent { +GovernorComponent::HasComponent, +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorQuorumTrait, - +GovernorComponent::GovernorExecuteTrait, - +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorExecutionTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, impl GovernorCountingSimple: HasComponent, diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index a05830efc..7dda1144a 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -64,8 +64,7 @@ pub mod GovernorSettingsComponent { +GovernorComponent::GovernorQuorumTrait, +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorVotesTrait, - +GovernorComponent::GovernorExecuteTrait, - +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorExecutionTrait, +SRC5Component::HasComponent, impl GovernorSettings: HasComponent, +Drop diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index aee0d47ca..1a918e8d4 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -33,8 +33,7 @@ pub mod GovernorVotesComponent { impl GovernorVotes< TContractState, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorExecuteTrait, - +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorExecutionTrait, +GovernorComponent::GovernorCountingTrait, +SRC5Component::HasComponent, impl GovernorVotesQuorumFraction: HasComponent, diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 1e3afd200..9520c58f6 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -50,8 +50,7 @@ pub mod GovernorVotesQuorumFractionComponent { TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorVotesTrait, - +GovernorComponent::GovernorExecuteTrait, - +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorExecutionTrait, +GovernorComponent::GovernorCountingTrait, +SRC5Component::HasComponent, impl GovernorVotesQuorumFraction: HasComponent, @@ -76,8 +75,7 @@ pub mod GovernorVotesQuorumFractionComponent { impl GovernorVotes< TContractState, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorExecuteTrait, - +GovernorComponent::GovernorQueueTrait, + +GovernorComponent::GovernorExecutionTrait, +GovernorComponent::GovernorCountingTrait, +SRC5Component::HasComponent, impl GovernorVotesQuorumFraction: HasComponent, diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 4b7dc9af9..1d2141692 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -192,7 +192,7 @@ pub mod GovernorComponent { ) -> u256; } - pub trait GovernorExecuteTrait { + pub trait GovernorExecutionTrait { /// Address through which the governor executes action. /// Should be used to specify whether the module execute actions through another contract /// such as a timelock. @@ -206,9 +206,7 @@ pub mod GovernorComponent { calls: Span, description_hash: felt252 ); - } - pub trait GovernorQueueTrait { /// Queuing mechanism. Can be used to modify the way queuing is /// performed (for example adding a vault/timelock). /// @@ -228,6 +226,14 @@ pub mod GovernorComponent { fn proposal_needs_queuing( self: @ComponentState, proposal_id: felt252 ) -> bool; + + /// Cancel mechanism. Can be used to modify the way canceling is + /// performed (for example adding a vault/timelock). + fn cancel_operations( + ref self: ComponentState, + proposal_id: felt252, + description_hash: felt252 + ); } // @@ -240,10 +246,9 @@ pub mod GovernorComponent { +HasComponent, +SRC5Component::HasComponent, +GovernorVotesTrait, - +GovernorExecuteTrait, impl GovernorQuorum: GovernorQuorumTrait, impl GovernorCounting: GovernorCountingTrait, - impl GovernorQueue: GovernorQueueTrait, + impl GovernorExecution: GovernorExecutionTrait, impl GovernorSettings: GovernorSettingsTrait, impl Metadata: SNIP12Metadata, impl Immutable: ImmutableConfig, @@ -335,7 +340,7 @@ pub mod GovernorComponent { fn proposal_needs_queuing( self: @ComponentState, proposal_id: felt252 ) -> bool { - GovernorQueue::proposal_needs_queuing(self, proposal_id) + GovernorExecution::proposal_needs_queuing(self, proposal_id) } /// Delay between the proposal is created and the vote starts. The unit this duration is @@ -539,8 +544,11 @@ pub mod GovernorComponent { starknet::get_caller_address() == self.proposal_proposer(proposal_id), Errors::PROPOSER_ONLY ); + self.cancel_operations(proposal_id, description_hash); + + self.emit(ProposalCanceled { proposal_id }); - self._cancel(proposal_id, calls, description_hash) + proposal_id } /// Cast a vote. @@ -626,8 +634,7 @@ pub mod GovernorComponent { TContractState, +HasComponent, +GovernorCountingTrait, - +GovernorExecuteTrait, - +GovernorQueueTrait, + +GovernorExecutionTrait, impl GovernorSettings: GovernorSettingsTrait, impl GovernorVotes: GovernorVotesTrait, impl SRC5: SRC5Component::HasComponent, @@ -800,36 +807,17 @@ pub mod GovernorComponent { proposal_id } - /// Internal cancel mechanism with minimal restrictions. Returns the id of the proposal. - /// - /// Requirements: - /// - /// - A proposal can be cancelled in any state other than - /// Canceled, Expired, or Executed. Once cancelled a proposal can't be re-submitted. + /// Internal cancel mechanism with minimal restrictions.. /// - /// Emits a `ProposalCanceled` event. + /// NOTE: Once cancelled a proposal can't be re-submitted. fn _cancel( ref self: ComponentState, proposal_id: felt252, - calls: Span, description_hash: felt252 - ) -> felt252 { - let valid_states = array![ - ProposalState::Pending, - ProposalState::Active, - ProposalState::Defeated, - ProposalState::Succeeded, - ProposalState::Queued - ]; - self.validate_state(proposal_id, valid_states.span()); - + ) { let mut proposal = self.Governor_proposals.read(proposal_id); proposal.canceled = true; self.Governor_proposals.write(proposal_id, proposal); - - self.emit(ProposalCanceled { proposal_id }); - - proposal_id } /// Internal wrapper for `GovernorCountingTrait::count_vote`. From 7e45786e6d95a1888c6c82945337544a05503b18 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 30 Oct 2024 23:38:29 +0100 Subject: [PATCH 42/76] feat: add timelock execution extension --- .../governance/src/governor/extensions.cairo | 1 + .../extensions/governor_core_execution.cairo | 61 +--- .../extensions/governor_counting_simple.cairo | 2 +- .../extensions/governor_settings.cairo | 108 +++++-- .../governor_timelock_execution.cairo | 297 ++++++++++++++++++ .../src/governor/extensions/interface.cairo | 15 +- .../governance/src/governor/governor.cairo | 35 ++- 7 files changed, 424 insertions(+), 95 deletions(-) create mode 100644 packages/governance/src/governor/extensions/governor_timelock_execution.cairo diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index d5e37b5f8..3d80c1c1e 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -1,6 +1,7 @@ pub mod governor_core_execution; pub mod governor_counting_simple; pub mod governor_settings; +pub mod governor_timelock_execution; pub mod governor_votes; pub mod governor_votes_quorum_fraction; pub mod interface; diff --git a/packages/governance/src/governor/extensions/governor_core_execution.cairo b/packages/governance/src/governor/extensions/governor_core_execution.cairo index 1cbed3342..ca5c9ad61 100644 --- a/packages/governance/src/governor/extensions/governor_core_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_core_execution.cairo @@ -9,26 +9,17 @@ /// GovernorTimelockExecutionComponent. #[starknet::component] pub mod GovernorCoreExecutionComponent { - use core::num::traits::Zero; use crate::governor::GovernorComponent::{ - InternalImpl as GovernorInternalImpl, ComponentState as GovernorComponentState + InternalTrait as GovernorInternalImpl, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; - use crate::governor::extensions::interface::IVotesToken; + use crate::governor::interface::ProposalState; use openzeppelin_introspection::src5::SRC5Component; - use starknet::ContractAddress; - use starknet::SyscallResultTrait; use starknet::account::Call; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, SyscallResultTrait}; #[storage] - pub struct Storage { - Governor_token: ContractAddress - } - - mod Errors { - pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; - } + pub struct Storage {} // // Extensions @@ -45,6 +36,13 @@ pub mod GovernorCoreExecutionComponent { impl GovernorCoreExecution: HasComponent, +Drop > of GovernorComponent::GovernorExecutionTrait { + /// See `GovernorComponent::GovernorExecutionTrait::state`. + fn state( + self: @GovernorComponentState, proposal_id: felt252 + ) -> ProposalState { + self._state(proposal_id) + } + /// See `GovernorComponent::GovernorExecutionTrait::executor`. fn executor(self: @GovernorComponentState) -> ContractAddress { starknet::get_contract_address() @@ -89,41 +87,4 @@ pub mod GovernorCoreExecutionComponent { self._cancel(proposal_id, description_hash); } } - - // - // External - // - - #[embeddable_as(VotesTokenImpl)] - impl VotesToken< - TContractState, +HasComponent, +Drop - > of IVotesToken> { - /// Returns the token that voting power is sourced from. - fn token(self: @ComponentState) -> ContractAddress { - self.Governor_token.read() - } - } - - // - // Internal - // - - #[generate_trait] - pub impl InternalImpl< - TContractState, - +HasComponent, - +GovernorComponent::GovernorVotesTrait, - impl Governor: GovernorComponent::HasComponent, - +Drop - > of InternalTrait { - /// Initializes the component by setting the votes token. - /// - /// Requirements: - /// - /// - `votes_token` must not be zero. - fn initialize(ref self: ComponentState, votes_token: ContractAddress) { - assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN); - self.Governor_token.write(votes_token); - } - } } diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 9a9470fb3..548ee1a1e 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -8,7 +8,7 @@ #[starknet::component] pub mod GovernorCountingSimpleComponent { use crate::governor::GovernorComponent::{ - InternalImpl, ComponentState as GovernorComponentState + InternalTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use openzeppelin_introspection::src5::SRC5Component; diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 7dda1144a..5c52a26ca 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -7,7 +7,9 @@ /// Extension of GovernorComponent for settings updatable through governance. #[starknet::component] pub mod GovernorSettingsComponent { - use crate::governor::GovernorComponent::ComponentState as GovernorComponentState; + use crate::governor::GovernorComponent::{ + InternalTrait as GovernorInternalTrait, ComponentState as GovernorComponentState + }; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::ISetSettings; use openzeppelin_introspection::src5::SRC5Component; @@ -100,56 +102,98 @@ pub mod GovernorSettingsComponent { #[embeddable_as(SetSettingsImpl)] impl SetSettings< - TContractState, +HasComponent, +Drop + TContractState, + +HasComponent, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorExecutionTrait, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + +Drop > of ISetSettings> { /// Sets the voting delay. /// /// Emits a `VotingDelayUpdated` event. - fn set_voting_delay(ref self: ComponentState, voting_delay: u64) { - self - .emit( - VotingDelayUpdated { - old_voting_delay: self.Governor_voting_delay.read(), - new_voting_delay: voting_delay - } - ); - self.Governor_voting_delay.write(voting_delay); + fn set_voting_delay(ref self: ComponentState, new_voting_delay: u64) { + self.assert_only_governance(); + + let old_voting_delay = self.Governor_voting_delay.read(); + self.emit(VotingDelayUpdated { old_voting_delay, new_voting_delay }); + self.Governor_voting_delay.write(new_voting_delay); } /// Sets the voting period. /// /// Requirements: /// - /// - `voting_period` must be greater than 0. + /// - `new_voting_period` must be greater than 0. /// /// Emits a `VotingPeriodUpdated` event. - fn set_voting_period(ref self: ComponentState, voting_period: u64) { - assert(voting_period > 0, Errors::INVALID_VOTING_PERIOD); - - self - .emit( - VotingPeriodUpdated { - old_voting_period: self.Governor_voting_period.read(), - new_voting_period: voting_period - } - ); - self.Governor_voting_period.write(voting_period); + fn set_voting_period(ref self: ComponentState, new_voting_period: u64) { + self.assert_only_governance(); + assert(new_voting_period > 0, Errors::INVALID_VOTING_PERIOD); + + let old_voting_period = self.Governor_voting_period.read(); + self.emit(VotingPeriodUpdated { old_voting_period, new_voting_period }); + self.Governor_voting_period.write(new_voting_period); } /// Sets the proposal threshold. /// /// Emits a `ProposalThresholdUpdated` event. fn set_proposal_threshold( - ref self: ComponentState, proposal_threshold: u256 + ref self: ComponentState, new_proposal_threshold: u256 ) { - self - .emit( - ProposalThresholdUpdated { - old_proposal_threshold: self.Governor_proposal_threshold.read(), - new_proposal_threshold: proposal_threshold - } - ); - self.Governor_proposal_threshold.write(proposal_threshold); + self.assert_only_governance(); + + let old_proposal_threshold = self.Governor_proposal_threshold.read(); + self.emit(ProposalThresholdUpdated { old_proposal_threshold, new_proposal_threshold }); + self.Governor_proposal_threshold.write(new_proposal_threshold); + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorExecutionTrait, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + impl Governor: GovernorComponent::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the component by setting the default values. + /// + /// Requirements: + /// + /// - `new_voting_period` must be greater than 0. + /// + /// Emits a `VotingDelayUpdated`, `VotingPeriodUpdated`, and `ProposalThresholdUpdated` + /// event. + fn initialize( + ref self: ComponentState, + new_voting_delay: u64, + new_voting_period: u64, + new_proposal_threshold: u256 + ) { + self.set_voting_delay(new_voting_delay); + self.set_voting_period(new_voting_period); + self.set_proposal_threshold(new_proposal_threshold); + } + + /// Wrapper for `Governor::assert_only_governance`. + fn assert_only_governance(self: @ComponentState) { + let governor_component = get_dep_component!(self, Governor); + governor_component.assert_only_governance(); } } } diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo new file mode 100644 index 000000000..ce3589693 --- /dev/null +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.18.0 +// (governance/governor/extensions/governor_core_execution.cairo) + +/// # GovernorTimelockExecution Component +/// +/// Extension of GovernorComponent that binds the execution process to an instance of a contract +/// implementing TimelockControllerComponent. This adds a delay, enforced by the TimelockController +/// to all successful proposal (in addition to the voting duration). The Governor needs the proposer +/// (and ideally the executor and canceller) roles for the Governor to work properly. +/// +/// Using this model means the proposal will be operated by the TimelockController and not by the +/// Governor. Thus, the assets and permissions must be attached to the TimelockController. Any asset +/// sent to the Governor will be inaccessible from a proposal, unless executed via +/// `Governor::relay`. +/// +/// WARNING: Setting up the TimelockController to have additional proposers or cancellers besides +/// the governor is very risky, as it grants them the ability to: 1) execute operations as the +/// timelock, and thus possibly performing operations or accessing funds that are expected to only +/// be accessible through a vote, and 2) block governance proposals that have been approved by the +/// voters, effectively executing a Denial of Service attack. +#[starknet::component] +pub mod GovernorTimelockExecutionComponent { + use core::num::traits::Zero; + use crate::governor::GovernorComponent::{ + InternalTrait as GovernorInternalTrait, ComponentState as GovernorComponentState + }; + use crate::governor::GovernorComponent; + use crate::governor::extensions::interface::ITimelockController; + use crate::governor::interface::ProposalState; + use crate::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; + use openzeppelin_introspection::src5::SRC5Component; + use starknet::ContractAddress; + use starknet::account::Call; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + type ProposalId = felt252; + type TimelockProposalId = felt252; + + #[storage] + pub struct Storage { + Governor_timelock_controller: ContractAddress, + Governor_timelock_ids: Map + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + TimelockUpdated: TimelockUpdated, + } + + /// Emitted when the timelock controller used for proposal execution is modified. + #[derive(Drop, starknet::Event)] + pub struct TimelockUpdated { + pub old_timelock: ContractAddress, + pub new_timelock: ContractAddress + } + + mod Errors { + pub const INVALID_TIMELOCK_CONTROLLER: felt252 = 'Invalid timelock controller'; + } + + // + // Extensions + // + + impl GovernorExecution< + TContractState, + +GovernorComponent::HasComponent, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + impl GovernorCoreExecution: HasComponent, + +Drop + > of GovernorComponent::GovernorExecutionTrait { + /// See `GovernorComponent::GovernorExecutionTrait::state`. + fn state( + self: @GovernorComponentState, proposal_id: felt252 + ) -> ProposalState { + let current_state = self._state(proposal_id); + + if current_state != ProposalState::Queued { + return current_state; + } + + let contract = self.get_contract(); + let this_component = GovernorCoreExecution::get_component(contract); + + let queue_id = this_component.Governor_timelock_ids.read(proposal_id); + let timelock_controller = this_component.timelock(); + let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + + if timelock_dispatcher.is_operation_pending(queue_id) { + ProposalState::Queued + } else if timelock_dispatcher.is_operation_done(queue_id) { + // This can happen if the proposal is executed directly on the timelock. + ProposalState::Executed + } else { + // This can happen if the proposal is canceled directly on the timelock. + ProposalState::Canceled + } + } + + /// See `GovernorComponent::GovernorExecutionTrait::executor`. + /// + /// In this module, the executor is the timelock controller. + fn executor(self: @GovernorComponentState) -> ContractAddress { + let contract = self.get_contract(); + let this_component = GovernorCoreExecution::get_component(contract); + + this_component.timelock() + } + + /// See `GovernorComponent::GovernorExecutionTrait::execute_operations`. + /// + /// Runs the already queued proposal through the timelock. + fn execute_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) { + let mut contract = self.get_contract_mut(); + let mut this_component = GovernorCoreExecution::get_component_mut(ref contract); + + let timelock_controller = this_component.Governor_timelock_controller.read(); + let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + + timelock_dispatcher + .execute_batch(calls, 0, this_component.timelock_salt(description_hash)); + + // Cleanup + this_component.Governor_timelock_ids.write(proposal_id, 0); + } + + /// See `GovernorComponent::GovernorExecutionTrait::queue_operations`. + /// + /// Queue a proposal to the timelock. + fn queue_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + calls: Span, + description_hash: felt252 + ) -> u64 { + let mut contract = self.get_contract_mut(); + let mut this_component = GovernorCoreExecution::get_component_mut(ref contract); + + let timelock_controller = this_component.timelock(); + let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + + let delay = timelock_dispatcher.get_min_delay(); + let salt = this_component.timelock_salt(description_hash); + + let queue_id = timelock_dispatcher.hash_operation_batch(calls, 0, salt); + this_component.Governor_timelock_ids.write(proposal_id, queue_id); + + timelock_dispatcher.schedule_batch(calls, 0, salt, delay); + + starknet::get_block_timestamp() + delay + } + + /// See `GovernorComponent::GovernorExecutionTrait::proposal_needs_queuing`. + fn proposal_needs_queuing( + self: @GovernorComponentState, proposal_id: felt252 + ) -> bool { + true + } + + /// See `GovernorComponent::GovernorExecutionTrait::cancel_operations`. + /// + /// Cancels the timelocked proposal if it has already been queued. + /// + /// NOTE: This function can reenter through the external call to the timelock, but we assume + /// the timelock is trusted and well behaved (according to TimelockController) and this will + /// not happen. + fn cancel_operations( + ref self: GovernorComponentState, + proposal_id: felt252, + description_hash: felt252 + ) { + self._cancel(proposal_id, description_hash); + + let mut contract = self.get_contract_mut(); + let mut this_component = GovernorCoreExecution::get_component_mut(ref contract); + + let timelock_id = this_component.Governor_timelock_ids.read(proposal_id); + if timelock_id.is_non_zero() { + let timelock_controller = this_component.timelock(); + let timelock_dispatcher = ITimelockDispatcher { + contract_address: timelock_controller + }; + + timelock_dispatcher.cancel(timelock_id); + this_component.Governor_timelock_ids.write(proposal_id, 0); + } + } + } + + // + // External + // + + #[embeddable_as(TimelockControllerImpl)] + impl TimelockController< + TContractState, + +HasComponent, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + +GovernorComponent::HasComponent, + +Drop + > of ITimelockController> { + /// Returns the token that voting power is sourced from. + fn timelock(self: @ComponentState) -> ContractAddress { + self.Governor_timelock_controller.read() + } + + /// Updates the associated timelock. + /// + /// Requirements: + /// + /// - Caller must be the governance. + /// + /// Emits a `TimelockUpdated` event. + fn update_timelock( + ref self: ComponentState, new_timelock: ContractAddress + ) { + self.assert_only_governance(); + self._update_timelock(new_timelock); + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +GovernorComponent::GovernorSettingsTrait, + +GovernorComponent::GovernorCountingTrait, + +GovernorComponent::GovernorVotesTrait, + +SRC5Component::HasComponent, + +GovernorComponent::ImmutableConfig, + impl Governor: GovernorComponent::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the component by setting the timelock contract address. + /// + /// Requirements: + /// + /// - `timelock_controller` must not be zero. + fn initialize( + ref self: ComponentState, timelock_controller: ContractAddress + ) { + assert(timelock_controller.is_non_zero(), Errors::INVALID_TIMELOCK_CONTROLLER); + self._update_timelock(timelock_controller); + } + + /// Wrapper for `Governor::assert_only_governance`. + fn assert_only_governance(self: @ComponentState) { + let governor_component = get_dep_component!(self, Governor); + governor_component.assert_only_governance(); + } + + /// Computes the `TimelockController` operation salt. + /// + /// It is computed with the governor address itself to avoid collisions across + /// governor instances using the same timelock. + fn timelock_salt( + self: @ComponentState, description_hash: felt252 + ) -> felt252 { + let description_hash: u256 = description_hash.into(); + let this: felt252 = starknet::get_contract_address().into(); + + // Unwrap is safe since the u256 value came from a felt252. + (this.into() ^ description_hash).try_into().unwrap() + } + + /// Updates the timelock contract address. + /// + /// Emits a `TimelockUpdated` event. + fn _update_timelock( + ref self: ComponentState, new_timelock: ContractAddress + ) { + let old_timelock = self.Governor_timelock_controller.read(); + self.emit(TimelockUpdated { old_timelock, new_timelock }); + self.Governor_timelock_controller.write(new_timelock); + } + } +} diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index f1b9458f0..c4129214c 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -24,14 +24,23 @@ pub trait IVotesToken { fn token(self: @TState) -> ContractAddress; } +#[starknet::interface] +pub trait ITimelockController { + /// Returns address of the associated timelock. + fn timelock(self: @TState) -> ContractAddress; + + /// Updates the associated timelock. + fn update_timelock(ref self: TState, new_timelock: ContractAddress); +} + #[starknet::interface] pub trait ISetSettings { /// Sets the voting delay. - fn set_voting_delay(ref self: TState, voting_delay: u64); + fn set_voting_delay(ref self: TState, new_voting_delay: u64); /// Sets the voting period. - fn set_voting_period(ref self: TState, voting_period: u64); + fn set_voting_period(ref self: TState, new_voting_period: u64); /// Sets the proposal threshold. - fn set_proposal_threshold(ref self: TState, proposal_threshold: u256); + fn set_proposal_threshold(ref self: TState, new_proposal_threshold: u256); } diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 1d2141692..cb47a6e20 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -105,7 +105,7 @@ pub mod GovernorComponent { pub params: ByteArray } - mod Errors { + pub mod Errors { pub const EXECUTOR_ONLY: felt252 = 'Executor only'; pub const PROPOSER_ONLY: felt252 = 'Proposer only'; pub const NONEXISTENT_PROPOSAL: felt252 = 'Nonexistent proposal'; @@ -193,6 +193,9 @@ pub mod GovernorComponent { } pub trait GovernorExecutionTrait { + /// See `interface::IGovernor::state`. + fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState; + /// Address through which the governor executes action. /// Should be used to specify whether the module execute actions through another contract /// such as a timelock. @@ -301,7 +304,7 @@ pub mod GovernorComponent { /// Returns the state of a proposal, given its id. fn state(self: @ComponentState, proposal_id: felt252) -> ProposalState { - self._state(proposal_id) + GovernorExecution::state(self, proposal_id) } /// The number of votes required in order for a voter to become a proposer. @@ -649,7 +652,7 @@ pub mod GovernorComponent { src5_component.register_interface(IERC1155_RECEIVER_ID); } - fn assert_only_governance(ref self: ComponentState) { + fn assert_only_governance(self: @ComponentState) { let executor = self.executor(); assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY); @@ -658,6 +661,13 @@ pub mod GovernorComponent { () } + /// Returns the proposal object given its id. + fn get_proposal( + self: @ComponentState, proposal_id: felt252 + ) -> ProposalCore { + self.Governor_proposals.read(proposal_id) + } + /// Returns a hash of the proposal using the Poseidon algorithm. /// TODO: check if we should be using Pedersen hash instead of Poseidon. fn _hash_proposal( @@ -725,22 +735,19 @@ pub mod GovernorComponent { /// Returns the state of a proposal, given its id. fn _state(self: @ComponentState, proposal_id: felt252) -> ProposalState { - let proposal = self.Governor_proposals.read(proposal_id); + let proposal = self.get_proposal(proposal_id); if proposal.executed { return ProposalState::Executed; } - if proposal.canceled { return ProposalState::Canceled; } let snapshot = self._proposal_snapshot(proposal_id); - assert(snapshot.is_non_zero(), Errors::NONEXISTENT_PROPOSAL); let current_timepoint = self.clock(); - if current_timepoint < snapshot { return ProposalState::Pending; } @@ -807,7 +814,8 @@ pub mod GovernorComponent { proposal_id } - /// Internal cancel mechanism with minimal restrictions.. + /// Internal cancel mechanism with minimal restrictions. + /// A proposal can be cancelled in any state other than Canceled, Expired, or Executed. /// /// NOTE: Once cancelled a proposal can't be re-submitted. fn _cancel( @@ -815,6 +823,15 @@ pub mod GovernorComponent { proposal_id: felt252, description_hash: felt252 ) { + let valid_states = array![ + ProposalState::Pending, + ProposalState::Active, + ProposalState::Defeated, + ProposalState::Succeeded, + ProposalState::Queued + ]; + self.validate_state(proposal_id, valid_states.span()); + let mut proposal = self.Governor_proposals.read(proposal_id); proposal.canceled = true; self.Governor_proposals.write(proposal_id, proposal); @@ -916,7 +933,7 @@ pub mod GovernorComponent { proposal_id: felt252, allowed_states: Span ) { - let current_state = self._state(proposal_id); + let current_state = self.state(proposal_id); let mut found = false; for state in allowed_states { if current_state == *state { From 50f29c833a26ae737b50f548e643839d1d40863a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 4 Nov 2024 13:38:48 +0100 Subject: [PATCH 43/76] refactor: dependencies --- packages/governance/src/governor.cairo | 2 +- .../governance/src/governor/extensions.cairo | 7 + .../extensions/governor_core_execution.cairo | 4 +- .../extensions/governor_counting_simple.cairo | 6 +- .../extensions/governor_settings.cairo | 13 +- .../governor_timelock_execution.cairo | 7 +- .../governor/extensions/governor_votes.cairo | 12 +- .../governor_votes_quorum_fraction.cairo | 6 +- .../governance/src/governor/governor.cairo | 151 +++++++++--------- packages/governance/src/tests.cairo | 1 + packages/test_common/src/mocks.cairo | 2 +- 11 files changed, 103 insertions(+), 108 deletions(-) diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo index 3b9e99fac..aea36a537 100644 --- a/packages/governance/src/governor.cairo +++ b/packages/governance/src/governor.cairo @@ -3,5 +3,5 @@ pub mod governor; pub mod interface; pub mod proposal_core; -pub use governor::GovernorComponent; +pub use governor::{GovernorComponent, DefaultConfig}; pub use proposal_core::ProposalCore; diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo index 3d80c1c1e..c3364c162 100644 --- a/packages/governance/src/governor/extensions.cairo +++ b/packages/governance/src/governor/extensions.cairo @@ -5,3 +5,10 @@ pub mod governor_timelock_execution; pub mod governor_votes; pub mod governor_votes_quorum_fraction; pub mod interface; + +pub use governor_core_execution::GovernorCoreExecutionComponent; +pub use governor_counting_simple::GovernorCountingSimpleComponent; +pub use governor_settings::GovernorSettingsComponent; +pub use governor_timelock_execution::GovernorTimelockExecutionComponent; +pub use governor_votes::GovernorVotesComponent; +pub use governor_votes_quorum_fraction::GovernorVotesQuorumFractionComponent; diff --git a/packages/governance/src/governor/extensions/governor_core_execution.cairo b/packages/governance/src/governor/extensions/governor_core_execution.cairo index ca5c9ad61..f315dc5a4 100644 --- a/packages/governance/src/governor/extensions/governor_core_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_core_execution.cairo @@ -10,7 +10,7 @@ #[starknet::component] pub mod GovernorCoreExecutionComponent { use crate::governor::GovernorComponent::{ - InternalTrait as GovernorInternalImpl, ComponentState as GovernorComponentState + InternalExtendedTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use crate::governor::interface::ProposalState; @@ -25,7 +25,7 @@ pub mod GovernorCoreExecutionComponent { // Extensions // - impl GovernorExecution< + pub impl GovernorExecution< TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorCountingTrait, diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 548ee1a1e..92a1e82bb 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -58,14 +58,10 @@ pub mod GovernorCountingSimpleComponent { // Extensions // - impl GovernorCounting< + pub impl GovernorCounting< TContractState, - +GovernorComponent::ImmutableConfig, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorQuorumTrait, - +GovernorComponent::GovernorExecutionTrait, - +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, impl GovernorCountingSimple: HasComponent, +Drop diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 5c52a26ca..faf0cdeb0 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -8,7 +8,7 @@ #[starknet::component] pub mod GovernorSettingsComponent { use crate::governor::GovernorComponent::{ - InternalTrait as GovernorInternalTrait, ComponentState as GovernorComponentState + InternalExtendedTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::ISetSettings; @@ -59,14 +59,9 @@ pub mod GovernorSettingsComponent { // Extensions // - impl GovernorSettings< + pub impl GovernorSettings< TContractState, - +GovernorComponent::ImmutableConfig, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorQuorumTrait, - +GovernorComponent::GovernorCountingTrait, - +GovernorComponent::GovernorVotesTrait, - +GovernorComponent::GovernorExecutionTrait, +SRC5Component::HasComponent, impl GovernorSettings: HasComponent, +Drop @@ -107,10 +102,8 @@ pub mod GovernorSettingsComponent { +GovernorComponent::HasComponent, +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorExecutionTrait, - +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, +Drop > of ISetSettings> { /// Sets the voting delay. @@ -164,10 +157,8 @@ pub mod GovernorSettingsComponent { +HasComponent, +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorExecutionTrait, - +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, impl Governor: GovernorComponent::HasComponent, +Drop > of InternalTrait { diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index ce3589693..72cee86ac 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -23,7 +23,7 @@ pub mod GovernorTimelockExecutionComponent { use core::num::traits::Zero; use crate::governor::GovernorComponent::{ - InternalTrait as GovernorInternalTrait, ComponentState as GovernorComponentState + InternalExtendedTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; use crate::governor::extensions::interface::ITimelockController; @@ -65,14 +65,13 @@ pub mod GovernorTimelockExecutionComponent { // Extensions // - impl GovernorExecution< + pub impl GovernorExecution< TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, impl GovernorCoreExecution: HasComponent, +Drop > of GovernorComponent::GovernorExecutionTrait { @@ -211,7 +210,6 @@ pub mod GovernorTimelockExecutionComponent { +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, +GovernorComponent::HasComponent, +Drop > of ITimelockController> { @@ -247,7 +245,6 @@ pub mod GovernorTimelockExecutionComponent { +GovernorComponent::GovernorCountingTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, impl Governor: GovernorComponent::HasComponent, +Drop > of InternalTrait { diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index 1a918e8d4..428788952 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -30,19 +30,17 @@ pub mod GovernorVotesComponent { // Extensions // - impl GovernorVotes< + pub impl GovernorVotes< TContractState, +GovernorComponent::HasComponent, - +GovernorComponent::GovernorExecutionTrait, - +GovernorComponent::GovernorCountingTrait, +SRC5Component::HasComponent, - impl GovernorVotesQuorumFraction: HasComponent, + impl GovernorVotes: HasComponent, +Drop > of GovernorComponent::GovernorVotesTrait { /// See `GovernorComponent::GovernorVotesTrait::clock`. fn clock(self: @GovernorComponentState) -> u64 { // VotesComponent uses the block timestamp for tracking checkpoints. - // That should be updated in order to allow for more flexible clock modes. + // That must be updated in order to allow for more flexible clock modes. starknet::get_block_timestamp() } @@ -59,7 +57,7 @@ pub mod GovernorVotesComponent { params: @ByteArray ) -> u256 { let contract = self.get_contract(); - let this_component = GovernorVotesQuorumFraction::get_component(contract); + let this_component = GovernorVotes::get_component(contract); let token = this_component.Governor_token.read(); let votes_dispatcher = IVotesDispatcher { contract_address: token }; @@ -90,8 +88,8 @@ pub mod GovernorVotesComponent { pub impl InternalImpl< TContractState, +HasComponent, + +GovernorComponent::HasComponent, +GovernorComponent::GovernorVotesTrait, - impl Governor: GovernorComponent::HasComponent, +Drop > of InternalTrait { /// Initializes the component by setting the votes token. diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 9520c58f6..f1bdf0a3d 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -46,7 +46,7 @@ pub mod GovernorVotesQuorumFractionComponent { // Extensions // - impl GovernorQuorum< + pub impl GovernorQuorum< TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorVotesTrait, @@ -72,7 +72,7 @@ pub mod GovernorVotesQuorumFractionComponent { } } - impl GovernorVotes< + pub impl GovernorVotesImpl< TContractState, +GovernorComponent::HasComponent, +GovernorComponent::GovernorExecutionTrait, @@ -84,7 +84,7 @@ pub mod GovernorVotesQuorumFractionComponent { /// See `GovernorComponent::GovernorVotesTrait::clock`. fn clock(self: @GovernorComponentState) -> u64 { // VotesComponent uses the block timestamp for tracking checkpoints. - // That should be updated in order to allow for more flexible clock modes. + // That must be updated in order to allow for more flexible clock modes. starknet::get_block_timestamp() } diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index cb47a6e20..23c79ed3d 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -636,12 +636,7 @@ pub mod GovernorComponent { pub impl InternalImpl< TContractState, +HasComponent, - +GovernorCountingTrait, - +GovernorExecutionTrait, - impl GovernorSettings: GovernorSettingsTrait, - impl GovernorVotes: GovernorVotesTrait, impl SRC5: SRC5Component::HasComponent, - impl Immutable: ImmutableConfig, +Drop > of InternalTrait { /// Initializes the contract by registering the supported interface Ids. @@ -652,15 +647,6 @@ pub mod GovernorComponent { src5_component.register_interface(IERC1155_RECEIVER_ID); } - fn assert_only_governance(self: @ComponentState) { - let executor = self.executor(); - assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY); - - // TODO: either check that the calldata matches the whitelist or assume the Executor - // can't execute proposals not created from the Governor itself. - () - } - /// Returns the proposal object given its id. fn get_proposal( self: @ComponentState, proposal_id: felt252 @@ -668,8 +654,49 @@ pub mod GovernorComponent { self.Governor_proposals.read(proposal_id) } + /// Checks if the proposer is authorized to submit a proposal with the given description. + /// + /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address + /// written as a hex string (case insensitive), then the submission of this proposal will + /// only be authorized to said address. + /// + /// This is used for frontrunning protection. By adding this pattern at the end of their + /// proposal, one can ensure that no other address can submit the same proposal. An attacker + /// would have to either remove or change that part, which would result in a different + /// proposal id. + /// + /// If the description does not match this pattern, it is unrestricted and anyone can submit + /// it. This includes: + /// - If the `0x???` part is not a valid hex string. + /// - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. + /// - If it ends with the expected suffix followed by newlines or other whitespace. + /// - If it ends with some other similar suffix, e.g. `#other=abc`. + /// - If it does not end with any such suffix. + fn is_valid_description_for_proposer( + self: @ComponentState, + proposer: ContractAddress, + description: @ByteArray + ) -> bool { + let length = description.len(); + + // Length is too short to contain a valid proposer suffix + if description.len() < 52 { + return true; + } + + // Extract what would be the `#proposer=` marker beginning the suffix + let marker = description.read_n_bytes(length - 52, 10); + + // If the marker is not found, there is no proposer suffix to check + if marker != "#proposer=" { + return true; + } + + let expected_address = description.read_n_bytes(length - 42, 42); + proposer.to_byte_array(16, 64) == expected_address + } + /// Returns a hash of the proposal using the Poseidon algorithm. - /// TODO: check if we should be using Pedersen hash instead of Poseidon. fn _hash_proposal( self: @ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { @@ -690,21 +717,6 @@ pub mod GovernorComponent { poseidon_hash_span(hashed_calls.span()) } - /// Internal wrapper for `GovernorVotesTrait::get_votes`. - fn _get_votes( - self: @ComponentState, - account: ContractAddress, - timepoint: u64, - params: @ByteArray - ) -> u256 { - GovernorVotes::get_votes(self, account, timepoint, params) - } - - /// Internal wrapper for `GovernorProposeTrait::proposal_threshold`. - fn _proposal_threshold(self: @ComponentState) -> u256 { - GovernorSettings::proposal_threshold(self) - } - /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot /// is performed at the end of this block. Hence, voting for this proposal starts at the /// beginning of the following block. @@ -732,6 +744,42 @@ pub mod GovernorComponent { fn _proposal_eta(self: @ComponentState, proposal_id: felt252) -> u64 { self.Governor_proposals.read(proposal_id).eta_seconds } + } + + #[generate_trait] + pub impl InternalExtendedImpl< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +GovernorCountingTrait, + +GovernorExecutionTrait, + impl GovernorSettings: GovernorSettingsTrait, + impl GovernorVotes: GovernorVotesTrait, + +Drop + > of InternalExtendedTrait { + fn assert_only_governance(self: @ComponentState) { + let executor = self.executor(); + assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY); + + // TODO: either check that the calldata matches the whitelist or assume the Executor + // can't execute proposals not created from the Governor itself. + () + } + + /// Internal wrapper for `GovernorVotesTrait::get_votes`. + fn _get_votes( + self: @ComponentState, + account: ContractAddress, + timepoint: u64, + params: @ByteArray + ) -> u256 { + GovernorVotes::get_votes(self, account, timepoint, params) + } + + /// Internal wrapper for `GovernorProposeTrait::proposal_threshold`. + fn _proposal_threshold(self: @ComponentState) -> u256 { + GovernorSettings::proposal_threshold(self) + } /// Returns the state of a proposal, given its id. fn _state(self: @ComponentState, proposal_id: felt252) -> ProposalState { @@ -797,7 +845,6 @@ pub mod GovernorComponent { }; self.Governor_proposals.write(proposal_id, proposal); - self .emit( ProposalCreated { @@ -885,48 +932,6 @@ pub mod GovernorComponent { voted_weight } - /// Checks if the proposer is authorized to submit a proposal with the given description. - /// - /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address - /// written as a hex string (case insensitive), then the submission of this proposal will - /// only be authorized to said address. - /// - /// This is used for frontrunning protection. By adding this pattern at the end of their - /// proposal, one can ensure that no other address can submit the same proposal. An attacker - /// would have to either remove or change that part, which would result in a different - /// proposal id. - /// - /// If the description does not match this pattern, it is unrestricted and anyone can submit - /// it. This includes: - /// - If the `0x???` part is not a valid hex string. - /// - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. - /// - If it ends with the expected suffix followed by newlines or other whitespace. - /// - If it ends with some other similar suffix, e.g. `#other=abc`. - /// - If it does not end with any such suffix. - fn is_valid_description_for_proposer( - self: @ComponentState, - proposer: ContractAddress, - description: @ByteArray - ) -> bool { - let length = description.len(); - - // Length is too short to contain a valid proposer suffix - if description.len() < 52 { - return true; - } - - // Extract what would be the `#proposer=` marker beginning the suffix - let marker = description.read_n_bytes(length - 52, 10); - - // If the marker is not found, there is no proposer suffix to check - if marker != "#proposer=" { - return true; - } - - let expected_address = description.read_n_bytes(length - 42, 42); - proposer.to_byte_array(16, 64) == expected_address - } - /// Validates that the proposal is in one of the expected states. fn validate_state( self: @ComponentState, diff --git a/packages/governance/src/tests.cairo b/packages/governance/src/tests.cairo index c71f711c4..9e4688ed6 100644 --- a/packages/governance/src/tests.cairo +++ b/packages/governance/src/tests.cairo @@ -1,3 +1,4 @@ +mod governor; mod test_timelock; mod test_utils; mod test_votes; diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo index 027f99625..2939aa95c 100644 --- a/packages/test_common/src/mocks.cairo +++ b/packages/test_common/src/mocks.cairo @@ -5,6 +5,7 @@ pub mod erc1155; pub mod erc20; pub mod erc2981; pub mod erc721; +pub mod governor; pub mod non_implementing; pub mod nonces; pub mod security; @@ -15,4 +16,3 @@ pub mod timelock; pub mod upgrades; pub mod vesting; pub mod votes; - From c666a1224a77a81c5abdf74bffc1c939194e4342 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 4 Nov 2024 14:20:37 +0100 Subject: [PATCH 44/76] feat: add mock --- .../extensions/governor_core_execution.cairo | 1 - .../extensions/governor_counting_simple.cairo | 4 +- .../extensions/governor_settings.cairo | 2 +- .../governor_timelock_execution.cairo | 2 +- .../governor/extensions/governor_votes.cairo | 2 +- .../governor_votes_quorum_fraction.cairo | 2 +- .../governance/src/governor/governor.cairo | 34 ++--- packages/test_common/src/mocks/governor.cairo | 118 ++++++++++++++++++ 8 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 packages/test_common/src/mocks/governor.cairo diff --git a/packages/governance/src/governor/extensions/governor_core_execution.cairo b/packages/governance/src/governor/extensions/governor_core_execution.cairo index f315dc5a4..178808210 100644 --- a/packages/governance/src/governor/extensions/governor_core_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_core_execution.cairo @@ -32,7 +32,6 @@ pub mod GovernorCoreExecutionComponent { +GovernorComponent::GovernorSettingsTrait, +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, - +GovernorComponent::ImmutableConfig, impl GovernorCoreExecution: HasComponent, +Drop > of GovernorComponent::GovernorExecutionTrait { diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 92a1e82bb..c49b4d40b 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -24,7 +24,7 @@ pub mod GovernorCountingSimpleComponent { } /// Supported vote types. - enum VoteType { + pub enum VoteType { Against, For, Abstain, @@ -42,7 +42,7 @@ pub mod GovernorCountingSimpleComponent { } #[starknet::storage_node] - struct ProposalVote { + pub struct ProposalVote { against_votes: u256, for_votes: u256, abstain_votes: u256, diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index faf0cdeb0..eb79ec35f 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -170,7 +170,7 @@ pub mod GovernorSettingsComponent { /// /// Emits a `VotingDelayUpdated`, `VotingPeriodUpdated`, and `ProposalThresholdUpdated` /// event. - fn initialize( + fn initializer( ref self: ComponentState, new_voting_delay: u64, new_voting_period: u64, diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index 72cee86ac..ca5d89369 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -253,7 +253,7 @@ pub mod GovernorTimelockExecutionComponent { /// Requirements: /// /// - `timelock_controller` must not be zero. - fn initialize( + fn initializer( ref self: ComponentState, timelock_controller: ContractAddress ) { assert(timelock_controller.is_non_zero(), Errors::INVALID_TIMELOCK_CONTROLLER); diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index 428788952..58fd3b974 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -97,7 +97,7 @@ pub mod GovernorVotesComponent { /// Requirements: /// /// - `votes_token` must not be zero. - fn initialize(ref self: ComponentState, votes_token: ContractAddress) { + fn initializer(ref self: ComponentState, votes_token: ContractAddress) { assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN); self.Governor_token.write(votes_token); } diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index f1bdf0a3d..650a2f7a7 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -174,7 +174,7 @@ pub mod GovernorVotesQuorumFractionComponent { /// - `quorum_numerator` must be less than `quorum_denominator`. /// /// Emits a `QuorumNumeratorUpdated` event. - fn initialize( + fn initializer( ref self: ComponentState, votes_token: ContractAddress, quorum_numerator: u256 diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 23c79ed3d..68f09ad2e 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -766,6 +766,23 @@ pub mod GovernorComponent { () } + /// Validates that the proposal is in one of the expected states. + fn validate_state( + self: @ComponentState, + proposal_id: felt252, + allowed_states: Span + ) { + let current_state = self.state(proposal_id); + let mut found = false; + for state in allowed_states { + if current_state == *state { + found = true; + break; + } + }; + assert(found, Errors::UNEXPECTED_PROPOSAL_STATE); + } + /// Internal wrapper for `GovernorVotesTrait::get_votes`. fn _get_votes( self: @ComponentState, @@ -931,23 +948,6 @@ pub mod GovernorComponent { voted_weight } - - /// Validates that the proposal is in one of the expected states. - fn validate_state( - self: @ComponentState, - proposal_id: felt252, - allowed_states: Span - ) { - let current_state = self.state(proposal_id); - let mut found = false; - for state in allowed_states { - if current_state == *state { - found = true; - break; - } - }; - assert(found, Errors::UNEXPECTED_PROPOSAL_STATE); - } } } diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo new file mode 100644 index 000000000..e0fcea475 --- /dev/null +++ b/packages/test_common/src/mocks/governor.cairo @@ -0,0 +1,118 @@ +#[starknet::contract] +pub mod Governor { + use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait; + use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait; + use openzeppelin_governance::governor::extensions::{ + GovernorVotesComponent, GovernorCountingSimpleComponent, GovernorCoreExecutionComponent + }; + use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig}; + use openzeppelin_introspection::src5::SRC5Component; + use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + use starknet::ContractAddress; + + component!(path: GovernorComponent, storage: governor, event: GovernorEvent); + component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent); + component!( + path: GovernorCountingSimpleComponent, + storage: governor_counting_simple, + event: GovernorCountingSimpleEvent + ); + component!( + path: GovernorCoreExecutionComponent, + storage: governor_core_execution, + event: GovernorCoreExecutionEvent + ); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Governor + #[abi(embed_v0)] + impl GovernorImpl = GovernorComponent::GovernorImpl; + + // Extensions + #[abi(embed_v0)] + impl VotesTokenImpl = GovernorVotesComponent::VotesTokenImpl; + impl GovernorVotesImpl = GovernorVotesComponent::GovernorVotes; + impl GovernorCountingSimpleImpl = + GovernorCountingSimpleComponent::GovernorCounting; + impl GovernorCoreExecutionImpl = + GovernorCoreExecutionComponent::GovernorExecution; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + #[substorage(v0)] + governor: GovernorComponent::Storage, + #[substorage(v0)] + governor_votes: GovernorVotesComponent::Storage, + #[substorage(v0)] + governor_counting_simple: GovernorCountingSimpleComponent::Storage, + #[substorage(v0)] + governor_core_execution: GovernorCoreExecutionComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + GovernorEvent: GovernorComponent::Event, + #[flat] + GovernorVotesEvent: GovernorVotesComponent::Event, + #[flat] + GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event, + #[flat] + GovernorCoreExecutionEvent: GovernorCoreExecutionComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, votes_token: ContractAddress) { + self.governor.initializer(); + self.governor_votes.initializer(votes_token); + } + + // SNIP12 Metadata + + impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'APP_NAME' + } + + fn version() -> felt252 { + 'APP_VERSION' + } + } + + // + // Locally implemented extensions + // + + impl GovernorQuorum of GovernorComponent::GovernorQuorumTrait { + /// See `GovernorComponent::GovernorQuorumTrait::quorum`. + fn quorum(self: @GovernorComponent::ComponentState, timepoint: u64) -> u256 { + 100_000_000 + } + } + + pub impl GovernorSettings of GovernorComponent::GovernorSettingsTrait { + /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`. + fn voting_delay(self: @GovernorComponent::ComponentState) -> u64 { + 86400 // 1 day + } + + /// See `GovernorComponent::GovernorSettingsTrait::voting_period`. + fn voting_period(self: @GovernorComponent::ComponentState) -> u64 { + 432_000 // 1 week + } + + /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`. + fn proposal_threshold(self: @GovernorComponent::ComponentState) -> u256 { + 0 + } + } +} From e893503d927fdca2f7807258013872bcd3502149 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 4 Nov 2024 14:23:40 +0100 Subject: [PATCH 45/76] fix: linter --- packages/governance/src/governor/governor.cairo | 2 +- packages/test_common/src/mocks/governor.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 68f09ad2e..92316d842 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -430,7 +430,7 @@ pub mod GovernorComponent { ) -> felt252 { let proposer = starknet::get_caller_address(); - // Check descrption for restricted proposer + // Check description for restricted proposer assert( self.is_valid_description_for_proposer(proposer, @description), Errors::RESTRICTED_PROPOSER diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index e0fcea475..df845d6aa 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -1,7 +1,7 @@ #[starknet::contract] pub mod Governor { - use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait; use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait; + use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait; use openzeppelin_governance::governor::extensions::{ GovernorVotesComponent, GovernorCountingSimpleComponent, GovernorCoreExecutionComponent }; From 941ea682131a5eb11d1f78dee884e6bf4e2f602f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 4 Nov 2024 14:30:52 +0100 Subject: [PATCH 46/76] fix: comment --- packages/test_common/src/mocks/governor.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index df845d6aa..f0bfdf3b8 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -76,7 +76,9 @@ pub mod Governor { self.governor_votes.initializer(votes_token); } + // // SNIP12 Metadata + // impl SNIP12MetadataImpl of SNIP12Metadata { fn name() -> felt252 { From 8774e5909a660562ba996679bf71f9db9c1cbcb4 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 6 Nov 2024 14:48:27 +0100 Subject: [PATCH 47/76] feat: add some tests --- Scarb.lock | 8 +- Scarb.toml | 2 +- .../extensions/governor_counting_simple.cairo | 2 +- .../extensions/governor_settings.cairo | 6 +- .../governor_timelock_execution.cairo | 4 +- .../governor/extensions/governor_votes.cairo | 2 +- .../governor_votes_quorum_fraction.cairo | 4 +- .../governance/src/governor/governor.cairo | 29 ++- .../src/governor/proposal_core.cairo | 2 +- packages/governance/src/tests/governor.cairo | 1 + .../src/tests/governor/test_governor.cairo | 168 ++++++++++++++++++ packages/test_common/src/mocks/governor.cairo | 12 +- packages/utils/src/bytearray.cairo | 8 +- 13 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 packages/governance/src/tests/governor.cairo create mode 100644 packages/governance/src/tests/governor/test_governor.cairo diff --git a/Scarb.lock b/Scarb.lock index 4b28da297..8a48a9cf3 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -165,15 +165,15 @@ dependencies = [ [[package]] name = "snforge_scarb_plugin" -version = "0.31.0" +version = "0.32.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:1fce075fcbf7fce1b0935f6f9a034549704837fb221da212d3b6e9134cebfdaa" +checksum = "sha256:e5a0e80294b1f5f00955c614ee3fc94c843ff0d27935693c3598d0ac8d79250a" [[package]] name = "snforge_std" -version = "0.31.0" +version = "0.32.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:60ac980b297281f9a59a5f1668cb56bdea1b28fd2f8008008270f9a3c91ad3ba" +checksum = "sha256:0e3cb45c6276334fd142a77212f0592d55744f1c022b7a63f20bcd79d0ce3927" dependencies = [ "snforge_scarb_plugin", ] diff --git a/Scarb.toml b/Scarb.toml index a16da9a73..2ca7601f6 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -42,7 +42,7 @@ keywords = [ [workspace.dependencies] assert_macros = "2.8.4" starknet = "2.8.4" -snforge_std = "0.31.0" +snforge_std = "0.32.0" [dependencies] starknet.workspace = true diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index c49b4d40b..8c001efa2 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -20,7 +20,7 @@ pub mod GovernorCountingSimpleComponent { #[storage] pub struct Storage { - Governor_proposals_votes: Map, + pub Governor_proposals_votes: Map, } /// Supported vote types. diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index eb79ec35f..8d4f97b27 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -17,9 +17,9 @@ pub mod GovernorSettingsComponent { #[storage] pub struct Storage { - Governor_voting_delay: u64, - Governor_voting_period: u64, - Governor_proposal_threshold: u256, + pub Governor_voting_delay: u64, + pub Governor_voting_period: u64, + pub Governor_proposal_threshold: u256, } #[event] diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index ca5d89369..889dbf0fd 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -40,8 +40,8 @@ pub mod GovernorTimelockExecutionComponent { #[storage] pub struct Storage { - Governor_timelock_controller: ContractAddress, - Governor_timelock_ids: Map + pub Governor_timelock_controller: ContractAddress, + pub Governor_timelock_ids: Map } #[event] diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index 58fd3b974..1bbcbcb19 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -19,7 +19,7 @@ pub mod GovernorVotesComponent { #[storage] pub struct Storage { - Governor_token: ContractAddress + pub Governor_token: ContractAddress } mod Errors { diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 650a2f7a7..6ee26f641 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -20,8 +20,8 @@ pub mod GovernorVotesQuorumFractionComponent { #[storage] pub struct Storage { - Governor_token: ContractAddress, - Governor_quorum_numerator_history: Trace, + pub Governor_token: ContractAddress, + pub Governor_quorum_numerator_history: Trace, } #[event] diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 92316d842..073c460d3 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -8,9 +8,10 @@ pub mod GovernorComponent { use core::hash::{HashStateTrait, HashStateExTrait}; use core::num::traits::Zero; - use core::poseidon::{PoseidonTrait, poseidon_hash_span}; + use core::pedersen::PedersenTrait; use crate::governor::ProposalCore; use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; + use crate::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; @@ -24,8 +25,8 @@ pub mod GovernorComponent { #[storage] pub struct Storage { - Governor_proposals: Map, - Governor_governance_call: DoubleEndedQueue + pub Governor_proposals: Map, + pub Governor_governance_call: DoubleEndedQueue } #[event] @@ -665,6 +666,10 @@ pub mod GovernorComponent { /// would have to either remove or change that part, which would result in a different /// proposal id. /// + /// NOTE: In Starknet, the Sequencer ensures the orders of transactions, but frontrunning + /// can still be achieved by nodes, and potentially other actors in the future with + /// sequencer decentralization. + /// /// If the description does not match this pattern, it is unrestricted and anyone can submit /// it. This includes: /// - If the `0x???` part is not a valid hex string. @@ -696,25 +701,11 @@ pub mod GovernorComponent { proposer.to_byte_array(16, 64) == expected_address } - /// Returns a hash of the proposal using the Poseidon algorithm. + /// Returns a hash of the proposal using the Pedersen hashing algorithm. fn _hash_proposal( self: @ComponentState, calls: Span, description_hash: felt252 ) -> felt252 { - let mut hashed_calls = array![]; - - for call in calls { - let hash_state = PoseidonTrait::new(); - let hash = hash_state - .update_with(*call.to) - .update_with(*call.selector) - .update_with(poseidon_hash_span(*call.calldata)) - .finalize(); - - hashed_calls.append(hash); - }; - hashed_calls.append(description_hash); - - poseidon_hash_span(hashed_calls.span()) + PedersenTrait::new(0).update_with(calls).update_with(description_hash).finalize() } /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index 83beb150d..d4261539c 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -4,7 +4,7 @@ use starknet::ContractAddress; use starknet::storage_access::StorePacking; -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, PartialEq, Debug)] pub struct ProposalCore { pub proposer: ContractAddress, pub vote_start: u64, diff --git a/packages/governance/src/tests/governor.cairo b/packages/governance/src/tests/governor.cairo new file mode 100644 index 000000000..302f2d4c1 --- /dev/null +++ b/packages/governance/src/tests/governor.cairo @@ -0,0 +1 @@ +mod test_governor; diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo new file mode 100644 index 000000000..a59a0fa95 --- /dev/null +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -0,0 +1,168 @@ +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::pedersen::PedersenTrait; +use crate::governor::GovernorComponent::InternalImpl; +use crate::governor::interface::IGOVERNOR_ID; +use crate::governor::{GovernorComponent, ProposalCore}; +use crate::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl}; +use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::governor::GovernorMock; +use openzeppelin_testing::constants::{ADMIN, ZERO}; +use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; +use openzeppelin_utils::bytearray::ByteArrayExtTrait; +use starknet::account::Call; +use starknet::ContractAddress; +use starknet::storage::StorageMapWriteAccess; + +type ComponentState = GovernorComponent::ComponentState; + +fn CONTRACT_STATE() -> GovernorMock::ContractState { + GovernorMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + GovernorComponent::component_state_for_testing() +} + +// +// Internal +// + +#[test] +fn test_initializer() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + + state.initializer(); + + assert!(contract_state.supports_interface(IGOVERNOR_ID)); + assert!(contract_state.supports_interface(IERC721_RECEIVER_ID)); + assert!(contract_state.supports_interface(IERC1155_RECEIVER_ID)); +} + +// +// get_proposal +// + +#[test] +fn test_get_empty_proposal() { + let mut state = COMPONENT_STATE(); + + let proposal = state.get_proposal(0); + + assert_eq!(proposal.proposer, ZERO()); + assert_eq!(proposal.vote_start, 0); + assert_eq!(proposal.vote_duration, 0); + assert_eq!(proposal.executed, false); + assert_eq!(proposal.canceled, false); + assert_eq!(proposal.eta_seconds, 0); +} + +#[test] +fn test_get_proposal() { + let mut state = COMPONENT_STATE(); + let (_, expected_proposal) = get_proposal_info(); + + state.Governor_proposals.write(1, expected_proposal); + + let proposal = state.get_proposal(1); + assert_eq!(proposal, expected_proposal); +} + +// +// is_valid_description_for_proposer +// + +#[test] +fn test_is_valid_description_too_short() { + let state = COMPONENT_STATE(); + let short_description: ByteArray = "fffffffffffffffffffffffffffffffffffffffffffffffffff"; + assert_eq!(short_description.len(), 51); + + let is_valid = state.is_valid_description_for_proposer(ADMIN(), @short_description); + assert!(is_valid); +} + +#[test] +fn test_is_valid_description_wrong_suffix() { + let state = COMPONENT_STATE(); + let description = "?proposer=0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + + let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); + assert!(is_valid); +} + +#[test] +fn test_is_valid_description_wrong_proposer() { + let state = COMPONENT_STATE(); + let description = "#proposer=0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + + let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); + assert!(!is_valid); +} + +#[test] +fn test_is_valid_description_valid_proposer() { + let state = COMPONENT_STATE(); + let address = ADMIN().to_byte_array(16, 64); + let mut description: ByteArray = "#proposer=0x"; + + description.append(@address); + + let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); + assert!(is_valid); +} + +// +// _hash_proposal +// + +#[test] +fn test__hash_proposal() { + let state = COMPONENT_STATE(); + let calls = get_calls(ZERO()); + let description = @"proposal description"; + let description_hash = description.hash(); + + let expected_hash = hash_proposal(calls, description_hash); + let hash = state._hash_proposal(calls, description_hash); + + assert_eq!(hash, expected_hash); +} + +// +// Helpers +// + +fn get_proposal_info() -> (felt252, ProposalCore) { + get_proposal_with_id(array![].span(), @"") +} + +fn get_proposal_with_id(calls: Span, description: @ByteArray) -> (felt252, ProposalCore) { + let timestamp = starknet::get_block_timestamp(); + let vote_start = timestamp + GovernorMock::VOTING_DELAY; + let vote_duration = GovernorMock::VOTING_PERIOD; + + let proposal_id = hash_proposal(calls, description.hash()); + let proposal = ProposalCore { + proposer: ADMIN(), + vote_start, + vote_duration, + executed: false, + canceled: false, + eta_seconds: 0 + }; + + (proposal_id, proposal) +} + +fn hash_proposal(calls: Span, description_hash: felt252) -> felt252 { + PedersenTrait::new(0).update_with(calls).update_with(description_hash).finalize() +} + +fn get_calls(to: ContractAddress) -> Span { + let call1 = Call { to, selector: selector!(""), calldata: array![].span() }; + let call2 = Call { to, selector: selector!(""), calldata: array![].span() }; + + array![call1, call2].span() +} diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index f0bfdf3b8..e1d19fd34 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -pub mod Governor { +pub mod GovernorMock { use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait; use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait; use openzeppelin_governance::governor::extensions::{ @@ -10,6 +10,10 @@ pub mod Governor { use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use starknet::ContractAddress; + pub const VOTING_DELAY: u64 = 86400; // 1 day + pub const VOTING_PERIOD: u64 = 432_000; // 1 week + pub const PROPOSAL_THRESHOLD: u256 = 0; + component!(path: GovernorComponent, storage: governor, event: GovernorEvent); component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent); component!( @@ -104,17 +108,17 @@ pub mod Governor { pub impl GovernorSettings of GovernorComponent::GovernorSettingsTrait { /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`. fn voting_delay(self: @GovernorComponent::ComponentState) -> u64 { - 86400 // 1 day + VOTING_DELAY } /// See `GovernorComponent::GovernorSettingsTrait::voting_period`. fn voting_period(self: @GovernorComponent::ComponentState) -> u64 { - 432_000 // 1 week + VOTING_PERIOD } /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`. fn proposal_threshold(self: @GovernorComponent::ComponentState) -> u256 { - 0 + PROPOSAL_THRESHOLD } } } diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index 8f8829df2..586db8ba3 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -35,15 +35,17 @@ pub fn to_byte_array, +Copy>( if padding.into() > byte_array.len() { let mut padding = padding.into() - byte_array.len(); while padding > 0 { - byte_array += "0"; + byte_array = "0" + byte_array; padding -= 1; }; }; byte_array } -/// Hashes a byte array using the Poseidon hash algorithm. -/// Encodes the byte array as a ´Span´ by serializing ´data´. +/// Returns a unique hash given a ByteArray. +/// +/// The hash is computed by serializing the data into a span of felts, and +/// then hashing the span using the Poseidon hash algorithm. pub fn hash_byte_array(data: @ByteArray) -> felt252 { let mut serialized = array![]; data.serialize(ref serialized); From a4ee53fbbf27b0ea6248b05c5916f55865799007 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 6 Nov 2024 14:58:02 +0100 Subject: [PATCH 48/76] fix: linter --- packages/governance/src/tests/governor/test_governor.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index a59a0fa95..b904f61f4 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -10,8 +10,8 @@ use openzeppelin_testing::constants::{ADMIN, ZERO}; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; -use starknet::account::Call; use starknet::ContractAddress; +use starknet::account::Call; use starknet::storage::StorageMapWriteAccess; type ComponentState = GovernorComponent::ComponentState; From 001715d4f5fe90995dbe83ebc1bff785059a2924 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 8 Nov 2024 13:33:48 +0100 Subject: [PATCH 49/76] refactor: comments --- packages/governance/src/governor/governor.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 46244f4aa..318680654 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -54,7 +54,6 @@ pub mod GovernorComponent { pub description: ByteArray } - // restriction. /// Emitted when a proposal is queued. #[derive(Drop, starknet::Event)] pub struct ProposalQueued { @@ -63,7 +62,6 @@ pub mod GovernorComponent { pub eta_seconds: u64 } - // restriction. /// Emitted when a proposal is executed. #[derive(Drop, starknet::Event)] pub struct ProposalExecuted { @@ -71,7 +69,6 @@ pub mod GovernorComponent { pub proposal_id: felt252 } - // restriction. /// Emitted when a proposal is canceled. #[derive(Drop, starknet::Event)] pub struct ProposalCanceled { From 35e0319ca3aa7166e239fb8195bf92f6046d8d6c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 8 Nov 2024 13:37:05 +0100 Subject: [PATCH 50/76] fix: import path --- packages/governance/src/governor/governor.cairo | 2 +- packages/governance/src/tests/governor/test_governor.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 318680654..d58440083 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -11,7 +11,7 @@ pub mod GovernorComponent { use core::pedersen::PedersenTrait; use crate::governor::ProposalCore; use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; - use crate::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl}; + use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index b904f61f4..d0ad733d6 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -3,7 +3,7 @@ use core::pedersen::PedersenTrait; use crate::governor::GovernorComponent::InternalImpl; use crate::governor::interface::IGOVERNOR_ID; use crate::governor::{GovernorComponent, ProposalCore}; -use crate::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl}; +use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::mocks::governor::GovernorMock; use openzeppelin_testing::constants::{ADMIN, ZERO}; From 42a70b51a0ff1e59b4861225313e50aa9e2a4efc Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 8 Nov 2024 13:58:47 +0100 Subject: [PATCH 51/76] fix: test --- .../governance/src/governor/governor.cairo | 11 +++++----- .../src/tests/governor/test_governor.cairo | 7 ++++--- sncast_scripts/Scarb.lock | 20 +++++++------------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index d58440083..e6a224bf8 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -663,14 +663,14 @@ pub mod GovernorComponent { /// would have to either remove or change that part, which would result in a different /// proposal id. /// - /// NOTE: In Starknet, the Sequencer ensures the orders of transactions, but frontrunning + /// NOTE: In Starknet, the Sequencer ensures the order of transactions, but frontrunning /// can still be achieved by nodes, and potentially other actors in the future with /// sequencer decentralization. /// /// If the description does not match this pattern, it is unrestricted and anyone can submit /// it. This includes: /// - If the `0x???` part is not a valid hex string. - /// - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. + /// - If the `0x???` part is a valid hex string, but does not contain exactly 64 hex digits. /// - If it ends with the expected suffix followed by newlines or other whitespace. /// - If it ends with some other similar suffix, e.g. `#other=abc`. /// - If it does not end with any such suffix. @@ -682,19 +682,20 @@ pub mod GovernorComponent { let length = description.len(); // Length is too short to contain a valid proposer suffix - if description.len() < 52 { + if description.len() < 76 { return true; } // Extract what would be the `#proposer=` marker beginning the suffix - let marker = description.read_n_bytes(length - 52, 10); + let marker = description.read_n_bytes(length - 76, 10); // If the marker is not found, there is no proposer suffix to check if marker != "#proposer=" { return true; } - let expected_address = description.read_n_bytes(length - 42, 42); + let expected_address = description.read_n_bytes(length - 64, 64); + proposer.to_byte_array(16, 64) == expected_address } diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index d0ad733d6..b841e6e76 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -76,8 +76,9 @@ fn test_get_proposal() { #[test] fn test_is_valid_description_too_short() { let state = COMPONENT_STATE(); - let short_description: ByteArray = "fffffffffffffffffffffffffffffffffffffffffffffffffff"; - assert_eq!(short_description.len(), 51); + let short_description: ByteArray + = "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + assert_eq!(short_description.len(), 75); let is_valid = state.is_valid_description_for_proposer(ADMIN(), @short_description); assert!(is_valid); @@ -95,7 +96,7 @@ fn test_is_valid_description_wrong_suffix() { #[test] fn test_is_valid_description_wrong_proposer() { let state = COMPONENT_STATE(); - let description = "#proposer=0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + let description = "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); assert!(!is_valid); diff --git a/sncast_scripts/Scarb.lock b/sncast_scripts/Scarb.lock index fc192541d..58357c836 100644 --- a/sncast_scripts/Scarb.lock +++ b/sncast_scripts/Scarb.lock @@ -25,14 +25,6 @@ dependencies = [ "openzeppelin_token", ] -[[package]] -name = "openzeppelin_governance" -version = "0.19.0" -dependencies = [ - "openzeppelin_access", - "openzeppelin_introspection", -] - [[package]] name = "openzeppelin_introspection" version = "0.19.0" @@ -47,6 +39,7 @@ dependencies = [ "openzeppelin_introspection", "openzeppelin_token", "openzeppelin_upgrades", + "openzeppelin_utils", ] [[package]] @@ -60,9 +53,10 @@ dependencies = [ name = "openzeppelin_token" version = "0.19.0" dependencies = [ + "openzeppelin_access", "openzeppelin_account", - "openzeppelin_governance", "openzeppelin_introspection", + "openzeppelin_utils", ] [[package]] @@ -90,15 +84,15 @@ checksum = "sha256:cfd7c73a6f9984880249babfa8664b69c5f7209c737b1081156a284061ccd [[package]] name = "snforge_scarb_plugin" -version = "0.2.0" +version = "0.32.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:2e4ce3ebe3f49548bd26908391b5d78537a765d827df0d96c32aeb88941d0d67" +checksum = "sha256:e5a0e80294b1f5f00955c614ee3fc94c843ff0d27935693c3598d0ac8d79250a" [[package]] name = "snforge_std" -version = "0.30.0" +version = "0.32.0" source = "registry+https://scarbs.xyz/" -checksum = "sha256:2f3c4846881813ac0f5d1460981249c9f5e2a6831e752beedf9b70975495b4ec" +checksum = "sha256:0e3cb45c6276334fd142a77212f0592d55744f1c022b7a63f20bcd79d0ce3927" dependencies = [ "snforge_scarb_plugin", ] From 1d2e4005f703a36a52a38b84f5d494fad6689799 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 8 Nov 2024 14:29:12 +0100 Subject: [PATCH 52/76] feat: add relay mechanism --- .../extensions/governor_timelock_execution.cairo | 13 +++++++------ .../src/governor/extensions/interface.cairo | 2 +- packages/governance/src/governor/governor.cairo | 16 +++++++++++++++- packages/governance/src/governor/interface.cairo | 9 +++++++++ .../src/tests/governor/test_governor.cairo | 7 ++++--- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index 141bb7fa5..46576dc97 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -6,8 +6,9 @@ /// /// Extension of GovernorComponent that binds the execution process to an instance of a contract /// implementing TimelockControllerComponent. This adds a delay, enforced by the TimelockController -/// to all successful proposal (in addition to the voting duration). The Governor needs the proposer -/// (and ideally the executor and canceller) roles for the Governor to work properly. +/// to all successful proposal (in addition to the voting duration). +/// +/// NOTE: The Governor needs the PROPOSER, EXECUTOR, and CANCELLER roles to work properly. /// /// Using this model means the proposal will be operated by the TimelockController and not by the /// Governor. Thus, the assets and permissions must be attached to the TimelockController. Any asset @@ -26,7 +27,7 @@ pub mod GovernorTimelockExecutionComponent { InternalExtendedTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; - use crate::governor::extensions::interface::ITimelockController; + use crate::governor::extensions::interface::ITimelocked; use crate::governor::interface::ProposalState; use crate::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component; @@ -202,8 +203,8 @@ pub mod GovernorTimelockExecutionComponent { // External // - #[embeddable_as(TimelockControllerImpl)] - impl TimelockController< + #[embeddable_as(TimelockedImpl)] + impl Timelocked< TContractState, +HasComponent, +GovernorComponent::GovernorSettingsTrait, @@ -212,7 +213,7 @@ pub mod GovernorTimelockExecutionComponent { +SRC5Component::HasComponent, +GovernorComponent::HasComponent, +Drop - > of ITimelockController> { + > of ITimelocked> { /// Returns the token that voting power is sourced from. fn timelock(self: @ComponentState) -> ContractAddress { self.Governor_timelock_controller.read() diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index 5e59da8cb..3a959843c 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -25,7 +25,7 @@ pub trait IVotesToken { } #[starknet::interface] -pub trait ITimelockController { +pub trait ITimelocked { /// Returns address of the associated timelock. fn timelock(self: @TState) -> ContractAddress; diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index e6a224bf8..f4c33d677 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -19,9 +19,9 @@ pub mod GovernorComponent { use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use openzeppelin_utils::structs::{DoubleEndedQueue, DoubleEndedQueueTrait}; - use starknet::ContractAddress; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, SyscallResultTrait}; #[storage] pub struct Storage { @@ -624,6 +624,20 @@ pub mod GovernorComponent { ) -> u256 { 1 } + + /// Relays a transaction or function call to an arbitrary target. + /// + /// In cases where the governance executor is some contract other than the governor itself, + /// like when using a timelock, this function can be invoked in a governance proposal to + /// recover tokens that was sent to the governor contract by mistake. + /// + /// NOTE: If the executor is simply the governor itself, use of `relay` is redundant. + fn relay(ref self: ComponentState, call: Call) { + self.assert_only_governance(); + + let Call { to, selector, calldata } = call; + starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall(); + } } // diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 26ab4977b..adf32dbde 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -198,4 +198,13 @@ pub trait IGovernor { params: ByteArray, signature: Span ) -> u256; + + /// Relays a transaction or function call to an arbitrary target. + /// + /// In cases where the governance executor is some contract other than the governor itself, like + /// when using a timelock, this function can be invoked in a governance proposal to recover + /// tokens that was sent to the governor contract by mistake. + /// + /// NOTE: If the executor is simply the governor itself, use of `relay` is redundant. + fn relay(ref self: TState, call: Call); } diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index b841e6e76..7913b2fc7 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -76,8 +76,8 @@ fn test_get_proposal() { #[test] fn test_is_valid_description_too_short() { let state = COMPONENT_STATE(); - let short_description: ByteArray - = "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + let short_description: ByteArray = + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; assert_eq!(short_description.len(), 75); let is_valid = state.is_valid_description_for_proposer(ADMIN(), @short_description); @@ -96,7 +96,8 @@ fn test_is_valid_description_wrong_suffix() { #[test] fn test_is_valid_description_wrong_proposer() { let state = COMPONENT_STATE(); - let description = "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + let description = + "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); assert!(!is_valid); From baee8161d51393a0de854b1f6e86695b01869522 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 8 Nov 2024 14:51:59 +0100 Subject: [PATCH 53/76] fix: conditions --- packages/governance/src/governor/governor.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index f4c33d677..121d6ae33 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -506,13 +506,13 @@ pub mod GovernorComponent { let self_executor = self.executor() == starknet::get_contract_address(); // Register governance call in queue before execution - if self_executor { // TODO: save the calldatas in the governance_call queue + if !self_executor { // TODO: save the calldatas in the governance_call queue } self.execute_operations(proposal_id, calls, description_hash); // Clean up the governance call queue - if self_executor + if !self_executor && (@self) .Governor_governance_call .deref() From 40ad6a3ff2fbf50d70ea64ba5b6b77d92e468c7f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 11 Nov 2024 18:08:56 +0100 Subject: [PATCH 54/76] feat: add more tests --- packages/governance/Scarb.toml | 1 + .../extensions/governor_counting_simple.cairo | 8 +- .../governance/src/governor/governor.cairo | 6 +- .../governance/src/governor/interface.cairo | 3 +- .../src/tests/governor/test_governor.cairo | 747 +++++++++++++++++- packages/test_common/src/mocks/governor.cairo | 10 +- packages/test_common/src/mocks/votes.cairo | 105 ++- 7 files changed, 807 insertions(+), 73 deletions(-) diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index d32d87a15..73967928a 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -46,6 +46,7 @@ casm = false name = "openzeppelin_governance_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::account::SnakeAccountMock", + "openzeppelin_test_common::mocks::governor::GovernorMock", "openzeppelin_test_common::mocks::timelock::TimelockControllerMock", "openzeppelin_test_common::mocks::timelock::MockContract", "openzeppelin_test_common::mocks::timelock::TimelockAttackerMock", diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 3d608e5e8..52d91cf24 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -43,10 +43,10 @@ pub mod GovernorCountingSimpleComponent { #[starknet::storage_node] pub struct ProposalVote { - against_votes: u256, - for_votes: u256, - abstain_votes: u256, - has_voted: Map + pub against_votes: u256, + pub for_votes: u256, + pub abstain_votes: u256, + pub has_voted: Map } mod Errors { diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 121d6ae33..77f3826fa 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -822,7 +822,7 @@ pub mod GovernorComponent { let deadline = self._proposal_deadline(proposal_id); - if current_timepoint < deadline { + if current_timepoint <= deadline { return ProposalState::Active; } else if !self.quorum_reached(proposal_id) || !self.vote_succeeded(proposal_id) { return ProposalState::Defeated; @@ -882,7 +882,7 @@ pub mod GovernorComponent { } /// Internal cancel mechanism with minimal restrictions. - /// A proposal can be cancelled in any state other than Canceled, Expired, or Executed. + /// A proposal can be cancelled in any state other than Canceled or Executed. /// /// NOTE: Once cancelled a proposal can't be re-submitted. fn _cancel( @@ -947,7 +947,7 @@ pub mod GovernorComponent { ); } - // TODO: check if the tally hook must be used + // TODO: add tally hook when the PreventLateQuorum extension gets added voted_weight } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index adf32dbde..7cbec8490 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -18,7 +18,7 @@ pub trait IERC6372 { fn CLOCK_MODE(self: @TState) -> ByteArray; } -#[derive(Copy, PartialEq, Drop, Serde)] +#[derive(Copy, PartialEq, Drop, Serde, Debug)] pub enum ProposalState { Pending, Active, @@ -26,7 +26,6 @@ pub enum ProposalState { Defeated, Succeeded, Queued, - Expired, Executed } diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 7913b2fc7..95ab37b52 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -1,18 +1,25 @@ use core::hash::{HashStateTrait, HashStateExTrait}; +use core::num::traits::Zero; use core::pedersen::PedersenTrait; -use crate::governor::GovernorComponent::InternalImpl; -use crate::governor::interface::IGOVERNOR_ID; +use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl, GovernorQuorumTrait}; +use crate::governor::interface::{ + IGOVERNOR_ID, ProposalState +}; use crate::governor::{GovernorComponent, ProposalCore}; use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::mocks::governor::GovernorMock; -use openzeppelin_testing::constants::{ADMIN, ZERO}; +use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO}; +use openzeppelin_testing::events::EventSpyExt; use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; +use snforge_std::EventSpy; +use snforge_std::{spy_events, test_address}; +use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp_global, start_mock_call}; use starknet::ContractAddress; use starknet::account::Call; -use starknet::storage::StorageMapWriteAccess; +use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess}; type ComponentState = GovernorComponent::ComponentState; @@ -132,6 +139,487 @@ fn test__hash_proposal() { assert_eq!(hash, expected_hash); } +// +// Proposal info +// + +#[test] +fn test__proposal_snapshot() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let snapshot = state._proposal_snapshot(id); + let expected = proposal.vote_start; + assert_eq!(snapshot, expected); +} + +#[test] +fn test__proposal_deadline() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let deadline = state._proposal_deadline(id); + let expected = proposal.vote_start + proposal.vote_duration; + assert_eq!(deadline, expected); +} + +#[test] +fn test__proposal_proposer() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let proposer = state._proposal_proposer(id); + let expected = proposal.proposer; + assert_eq!(proposer, expected); +} + +#[test] +fn test__proposal_eta() { + let mut state = COMPONENT_STATE(); + let (_, proposal) = get_proposal_info(); + + state.Governor_proposals.write(1, proposal); + + let eta = state._proposal_eta(1); + let expected = proposal.eta_seconds; + assert_eq!(eta, expected); +} + +// +// assert_only_governance +// + +#[test] +fn test_assert_only_governance() { + let mut state = COMPONENT_STATE(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, contract_address); + + state.assert_only_governance(); +} + +#[test] +#[should_panic(expected: 'Executor only')] +fn test_assert_only_governance_not_executor() { + let mut state = COMPONENT_STATE(); + let contract_address = test_address(); + + start_cheat_caller_address(contract_address, OTHER()); + + state.assert_only_governance(); +} + +// +// validate_state +// + +#[test] +fn test_validate_state() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + // Current should be Pending + let current_state = state._state(id); + assert_eq!(current_state, ProposalState::Pending); + + let valid_states = array![ProposalState::Pending]; + state.validate_state(id, valid_states.span()); + + let valid_states = array![ProposalState::Pending, ProposalState::Active]; + state.validate_state(id, valid_states.span()); + + let valid_states = array![ + ProposalState::Executed, ProposalState::Active, ProposalState::Pending + ]; + state.validate_state(id, valid_states.span()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_validate_state_invalid() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + // Current should be Pending + let current_state = state._state(id); + assert_eq!(current_state, ProposalState::Pending); + + let valid_states = array![ProposalState::Active].span(); + state.validate_state(id, valid_states); +} + +// +// _state +// + +#[test] +fn test__state_executed() { + let mut state = COMPONENT_STATE(); + + // The getter already asserts the state + get_executed_proposal(ref state); +} + +#[test] +fn test__state_canceled() { + let mut state = COMPONENT_STATE(); + + // The getter already asserts the state + get_canceled_proposal(ref state); +} + +#[test] +#[should_panic(expected: 'Nonexistent proposal')] +fn test__state_non_existent() { + let state = COMPONENT_STATE(); + + state._state(1); +} + +#[test] +fn test__state_pending() { + let mut state = COMPONENT_STATE(); + + // The getter already asserts the state + get_pending_proposal(ref state); +} + +#[test] +fn test__state_active() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Active; + + // Is active before deadline + start_cheat_block_timestamp_global(deadline - 1); + let current_state = state._state(id); + assert_eq!(current_state, expected); + + // Is active in deadline + start_cheat_block_timestamp_global(deadline); + let current_state = state._state(id); + assert_eq!(current_state, expected); +} + +#[test] +fn test__state_defeated_quorum_not_reached() { + let mut mock_state = CONTRACT_STATE(); + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Defeated; + + start_cheat_block_timestamp_global(deadline + 1); + + // Quorum not reached + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum - 1); + + let current_state = mock_state.governor._state(id); + assert_eq!(current_state, expected); +} + +#[test] +fn test__state_defeated_vote_not_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Defeated; + + start_cheat_block_timestamp_global(deadline + 1); + + // Quorum reached + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum + 1); + + // Vote not succeeded + proposal_votes.against_votes.write(quorum + 1); + + let current_state = mock_state.governor._state(id); + assert_eq!(current_state, expected); +} + +#[test] +fn test__state_queued() { + let mut mock_state = CONTRACT_STATE(); + + // The getter already asserts the state + get_queued_proposal(ref mock_state); +} + +#[test] +fn test__state_succeeded() { + let mut mock_state = CONTRACT_STATE(); + + // The getter already asserts the state + get_succeeded_proposal(ref mock_state); +} + +// +// _propose +// + +#[test] +fn test__propose() { + let mut state = COMPONENT_STATE(); + let mut spy = spy_events(); + let contract_address = test_address(); + + let calls = get_calls(OTHER()); + let description = @"proposal description"; + let proposer = ADMIN(); + let vote_start = starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY; + let vote_end = vote_start + GovernorMock::VOTING_PERIOD; + + let id = state._propose(calls, description, proposer); + + // Check id + let expected_id = hash_proposal(calls, description.hash()); + assert_eq!(id, expected_id); + + // Check event + spy + .assert_only_event_proposal_created( + contract_address, + expected_id, + proposer, + calls, + array![].span(), + vote_start, + vote_end, + description + ); + + // Check proposal + let proposal = state.get_proposal(id); + let expected = ProposalCore { + proposer: ADMIN(), + vote_start: starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY, + vote_duration: GovernorMock::VOTING_PERIOD, + executed: false, + canceled: false, + eta_seconds: 0 + }; + + assert_eq!(proposal, expected); +} + +#[test] +#[should_panic(expected: 'Existent proposal')] +fn test__propose_existent_proposal() { + let mut state = COMPONENT_STATE(); + let calls = get_calls(OTHER()); + let description = @"proposal description"; + let proposer = ADMIN(); + + let id = state._propose(calls, description, proposer); + let expected_id = hash_proposal(calls, description.hash()); + assert_eq!(id, expected_id); + + // Propose again + state._propose(calls, description, proposer); +} + +// +// _cancel +// + +#[test] +fn test__cancel_pending() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_pending_proposal(ref state); + + state._cancel(id, 0); + + let canceled_proposal = state.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test__cancel_active() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_active_proposal(ref state); + + state._cancel(id, 0); + + let canceled_proposal = state.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test__cancel_defeated() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_defeated_proposal(ref mock_state); + + mock_state.governor._cancel(id, 0); + + let canceled_proposal = mock_state.governor.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test__cancel_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_succeeded_proposal(ref mock_state); + + mock_state.governor._cancel(id, 0); + + let canceled_proposal = mock_state.governor.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test__cancel_queued() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_queued_proposal(ref mock_state); + + mock_state.governor._cancel(id, 0); + + let canceled_proposal = mock_state.governor.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cancel_canceled() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_canceled_proposal(ref state); + + // Cancel again + state._cancel(id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cancel_executed() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_executed_proposal(ref state); + + state._cancel(id, 0); +} + +// +// _cast_vote +// + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_pending() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_pending_proposal(ref state); + + state._cast_vote(id, OTHER(), 0, "", ""); +} + +#[test] +fn test__cast_vote_active_no_params() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_active_proposal(ref state); + let mut spy = spy_events(); + let contract_address = test_address(); + + let reason = "reason"; + let expected_weight = 100; + + // Mock the get past votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + let weight = state._cast_vote(id, OTHER(), 0, reason, ""); + assert_eq!(weight, expected_weight); + + spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @"reason"); +} + +#[test] +fn test__cast_vote_active_with_params() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_active_proposal(ref state); + let mut spy = spy_events(); + let contract_address = test_address(); + + let reason = "reason"; + let params = "params"; + let expected_weight = 100; + + // Mock the get past votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + let weight = state._cast_vote(id, OTHER(), 0, reason, params); + assert_eq!(weight, expected_weight); + + spy + .assert_event_vote_cast_with_params( + contract_address, OTHER(), id, 0, expected_weight, @"reason", @"params" + ); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_defeated() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_defeated_proposal(ref mock_state); + + mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_succeeded_proposal(ref mock_state); + + mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_queued() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = get_queued_proposal(ref mock_state); + + mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_canceled() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_canceled_proposal(ref state); + + state._cast_vote(id, OTHER(), 0, "", ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test__cast_vote_executed() { + let mut state = COMPONENT_STATE(); + let (id, _) = get_executed_proposal(ref state); + + state._cast_vote(id, OTHER(), 0, "", ""); +} + // // Helpers // @@ -163,8 +651,255 @@ fn hash_proposal(calls: Span, description_hash: felt252) -> felt252 { } fn get_calls(to: ContractAddress) -> Span { - let call1 = Call { to, selector: selector!(""), calldata: array![].span() }; - let call2 = Call { to, selector: selector!(""), calldata: array![].span() }; + let call1 = Call { to, selector: selector!("test1"), calldata: array![].span() }; + let call2 = Call { to, selector: selector!("test2"), calldata: array![].span() }; array![call1, call2].span() } + +fn get_pending_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let state = state._state(id); + let expected = ProposalState::Pending; + + assert_eq!(state, expected); + + (id, proposal) +} + +fn get_active_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Active; + + // Is active before deadline + start_cheat_block_timestamp_global(deadline - 1); + let current_state = state._state(id); + assert_eq!(current_state, expected); + + (id, proposal) +} + +fn get_queued_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { + let (id, mut proposal) = get_proposal_info(); + + proposal.eta_seconds = 1; + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + + // Quorum reached + start_cheat_block_timestamp_global(deadline + 1); + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum + 1); + + // Vote succeeded + proposal_votes.against_votes.write(quorum); + + let expected = ProposalState::Queued; + let current_state = mock_state.governor._state(id); + assert_eq!(current_state, expected); + + (id, proposal) +} + +fn get_canceled_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + state._cancel(id, 0); + + let expected = ProposalState::Canceled; + let current_state = state._state(id); + assert_eq!(current_state, expected); + + (id, proposal) +} + +fn get_defeated_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + + // Quorum not reached + start_cheat_block_timestamp_global(deadline + 1); + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum - 1); + + let expected = ProposalState::Defeated; + let current_state = mock_state.governor._state(id); + assert_eq!(current_state, expected); + + (id, proposal) +} + +fn get_succeeded_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Succeeded; + + start_cheat_block_timestamp_global(deadline + 1); + + // Quorum reached + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum + 1); + + // Vote succeeded + proposal_votes.against_votes.write(quorum); + + let current_state = mock_state.governor._state(id); + assert_eq!(current_state, expected); + + (id, proposal) +} + +fn get_executed_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { + let (id, mut proposal) = get_proposal_info(); + + proposal.executed = true; + state.Governor_proposals.write(id, proposal); + + let state = state._state(id); + let expected = ProposalState::Executed; + + assert_eq!(state, expected); + + (id, proposal) +} + +// +// Event helpers +// + +#[generate_trait] +pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers { + fn assert_event_proposal_created( + ref self: EventSpy, + contract: ContractAddress, + proposal_id: felt252, + proposer: ContractAddress, + calls: Span, + signatures: Span>, + vote_start: u64, + vote_end: u64, + description: @ByteArray + ) { + let expected = GovernorComponent::Event::ProposalCreated( + GovernorComponent::ProposalCreated { + proposal_id, + proposer, + calls, + signatures, + vote_start, + vote_end, + description: description.clone() + } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_proposal_created( + ref self: EventSpy, + contract: ContractAddress, + proposal_id: felt252, + proposer: ContractAddress, + calls: Span, + signatures: Span>, + vote_start: u64, + vote_end: u64, + description: @ByteArray + ) { + self + .assert_event_proposal_created( + contract, + proposal_id, + proposer, + calls, + signatures, + vote_start, + vote_end, + description + ); + self.assert_no_events_left_from(contract); + } + + fn assert_event_vote_cast( + ref self: EventSpy, + contract: ContractAddress, + voter: ContractAddress, + proposal_id: felt252, + support: u8, + weight: u256, + reason: @ByteArray + ) { + let expected = GovernorComponent::Event::VoteCast( + GovernorComponent::VoteCast { + voter, proposal_id, support, weight, reason: reason.clone() + } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_vote_cast( + ref self: EventSpy, + contract: ContractAddress, + voter: ContractAddress, + proposal_id: felt252, + support: u8, + weight: u256, + reason: @ByteArray + ) { + self.assert_event_vote_cast(contract, voter, proposal_id, support, weight, reason); + self.assert_no_events_left_from(contract); + } + + fn assert_event_vote_cast_with_params( + ref self: EventSpy, + contract: ContractAddress, + voter: ContractAddress, + proposal_id: felt252, + support: u8, + weight: u256, + reason: @ByteArray, + params: @ByteArray + ) { + let expected = GovernorComponent::Event::VoteCastWithParams( + GovernorComponent::VoteCastWithParams { + voter, proposal_id, support, weight, reason: reason.clone(), params: params.clone() + } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_vote_cast_with_params( + ref self: EventSpy, + contract: ContractAddress, + voter: ContractAddress, + proposal_id: felt252, + support: u8, + weight: u256, + reason: @ByteArray, + params: @ByteArray + ) { + self + .assert_event_vote_cast_with_params( + contract, voter, proposal_id, support, weight, reason, params + ); + self.assert_no_events_left_from(contract); + } +} diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index e1d19fd34..3c7b94db2 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -48,15 +48,15 @@ pub mod GovernorMock { #[storage] struct Storage { #[substorage(v0)] - governor: GovernorComponent::Storage, + pub governor: GovernorComponent::Storage, #[substorage(v0)] - governor_votes: GovernorVotesComponent::Storage, + pub governor_votes: GovernorVotesComponent::Storage, #[substorage(v0)] - governor_counting_simple: GovernorCountingSimpleComponent::Storage, + pub governor_counting_simple: GovernorCountingSimpleComponent::Storage, #[substorage(v0)] - governor_core_execution: GovernorCoreExecutionComponent::Storage, + pub governor_core_execution: GovernorCoreExecutionComponent::Storage, #[substorage(v0)] - src5: SRC5Component::Storage, + pub src5: SRC5Component::Storage, } #[event] diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index fa4cb0261..fce9fde9c 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -1,26 +1,24 @@ #[starknet::contract] -pub mod ERC721VotesMock { +pub mod ERC20VotesMock { use openzeppelin_governance::votes::VotesComponent; - use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc721::ERC721Component; + use openzeppelin_token::erc20::ERC20Component; 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: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); - // Votes + // Votes and ERC20Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; impl VotesInternalImpl = VotesComponent::InternalImpl; - // ERC721 + // ERC20 #[abi(embed_v0)] - impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; - impl ERC721InternalImpl = ERC721Component::InternalImpl; + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; // Nonces #[abi(embed_v0)] @@ -29,11 +27,9 @@ pub mod ERC721VotesMock { #[storage] pub struct Storage { #[substorage(v0)] - pub erc721_votes: VotesComponent::Storage, - #[substorage(v0)] - pub erc721: ERC721Component::Storage, + pub erc20_votes: VotesComponent::Storage, #[substorage(v0)] - pub src5: SRC5Component::Storage, + pub erc20: ERC20Component::Storage, #[substorage(v0)] pub nonces: NoncesComponent::Storage } @@ -42,11 +38,9 @@ pub mod ERC721VotesMock { #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC721VotesEvent: VotesComponent::Event, - #[flat] - ERC721Event: ERC721Component::Event, + ERC20VotesEvent: VotesComponent::Event, #[flat] - SRC5Event: SRC5Component::Event, + ERC20Event: ERC20Component::Event, #[flat] NoncesEvent: NoncesComponent::Event } @@ -61,51 +55,47 @@ pub mod ERC721VotesMock { } } - 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, - to: ContractAddress, - token_id: u256, - auth: ContractAddress + impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 ) { let mut contract_state = self.get_contract_mut(); - - // 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); - contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1); + contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); } } #[constructor] fn constructor(ref self: ContractState) { - self.erc721.initializer("MyToken", "MTK", ""); + self.erc20.initializer("MyToken", "MTK"); } } #[starknet::contract] -pub mod ERC20VotesMock { +pub mod ERC721VotesMock { use openzeppelin_governance::votes::VotesComponent; - use openzeppelin_token::erc20::ERC20Component; + 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: erc20_votes, event: ERC20VotesEvent); - component!(path: ERC20Component, storage: erc20, event: ERC20Event); + 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 ERC20Votes + // Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; impl VotesInternalImpl = VotesComponent::InternalImpl; - // ERC20 + // ERC721 #[abi(embed_v0)] - impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; // Nonces #[abi(embed_v0)] @@ -114,9 +104,11 @@ pub mod ERC20VotesMock { #[storage] pub struct Storage { #[substorage(v0)] - pub erc20_votes: VotesComponent::Storage, + pub erc721_votes: VotesComponent::Storage, #[substorage(v0)] - pub erc20: ERC20Component::Storage, + pub erc721: ERC721Component::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage, #[substorage(v0)] pub nonces: NoncesComponent::Storage } @@ -125,9 +117,11 @@ pub mod ERC20VotesMock { #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC20VotesEvent: VotesComponent::Event, + ERC721VotesEvent: VotesComponent::Event, #[flat] - ERC20Event: ERC20Component::Event, + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, #[flat] NoncesEvent: NoncesComponent::Event } @@ -142,21 +136,26 @@ pub mod ERC20VotesMock { } } - impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { - fn after_update( - ref self: ERC20Component::ComponentState, - from: ContractAddress, - recipient: ContractAddress, - amount: u256 + 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, + to: ContractAddress, + token_id: u256, + auth: ContractAddress ) { let mut contract_state = self.get_contract_mut(); - contract_state.erc20_votes.transfer_voting_units(from, recipient, amount); + + // 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); + contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1); } } #[constructor] fn constructor(ref self: ContractState) { - self.erc20.initializer("MyToken", "MTK"); + self.erc721.initializer("MyToken", "MTK", ""); } } - From f749245fc2482e19249deb28d5fcab2703161ff3 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Mon, 11 Nov 2024 18:09:07 +0100 Subject: [PATCH 55/76] fix: linter --- packages/governance/src/tests/governor/test_governor.cairo | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 95ab37b52..0d270ffaf 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -2,9 +2,7 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::num::traits::Zero; use core::pedersen::PedersenTrait; use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl, GovernorQuorumTrait}; -use crate::governor::interface::{ - IGOVERNOR_ID, ProposalState -}; +use crate::governor::interface::{IGOVERNOR_ID, ProposalState}; use crate::governor::{GovernorComponent, ProposalCore}; use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; From 2ec909888d9aa97b363d65984979f83f52929735 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 00:50:34 +0100 Subject: [PATCH 56/76] feat: add more tests --- .../governance/src/governor/governor.cairo | 4 +- .../src/tests/governor/test_governor.cairo | 511 ++++++++++++++---- packages/test_common/src/mocks/governor.cairo | 7 +- 3 files changed, 414 insertions(+), 108 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 77f3826fa..c00a7645b 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -954,12 +954,12 @@ pub mod GovernorComponent { } } -/// Implementation of the default ERC2981Component ImmutableConfig. +/// Implementation of the default Governor ImmutableConfig. /// /// See /// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation /// -/// The default fee denominator is set to `DEFAULT_FEE_DENOMINATOR`. +/// The `DEFAULT_PARAMS` is set to `""`. pub impl DefaultConfig of GovernorComponent::ImmutableConfig { fn DEFAULT_PARAMS() -> ByteArray { "" diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 0d270ffaf..53f6488d6 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -1,11 +1,12 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::num::traits::Zero; use core::pedersen::PedersenTrait; -use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl, GovernorQuorumTrait}; -use crate::governor::interface::{IGOVERNOR_ID, ProposalState}; -use crate::governor::{GovernorComponent, ProposalCore}; +use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl}; +use crate::governor::interface::{IGovernor, IGOVERNOR_ID, ProposalState}; +use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore}; use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::governor::GovernorMock; use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO}; use openzeppelin_testing::events::EventSpyExt; @@ -29,6 +30,303 @@ fn COMPONENT_STATE() -> ComponentState { GovernorComponent::component_state_for_testing() } +// +// External +// + +#[test] +fn test_name() { + let state = @COMPONENT_STATE(); + let name = state.name(); + assert_eq!(name, 'APP_NAME'); +} + +#[test] +fn test_version() { + let state = COMPONENT_STATE(); + let version = state.version(); + assert_eq!(version, 'APP_VERSION'); +} + +#[test] +fn test_counting_mode() { + let state = COMPONENT_STATE(); + let counting_mode = state.COUNTING_MODE(); + assert_eq!(counting_mode, "support=bravo&quorum=for,abstain"); +} + +#[test] +fn test_hash_proposal() { + let state = COMPONENT_STATE(); + let calls = get_calls(ZERO()); + let description = @"proposal description"; + let description_hash = description.hash(); + + let expected_hash = hash_proposal(calls, description_hash); + let hash = state.hash_proposal(calls, description_hash); + + assert_eq!(hash, expected_hash); +} + +// +// state +// + +#[test] +fn test_state_executed() { + let mut state = COMPONENT_STATE(); + + // The function already asserts the state + setup_executed_proposal(ref state, true); +} + +#[test] +fn test_state_canceled() { + let mut state = COMPONENT_STATE(); + + // The function already asserts the state + setup_canceled_proposal(ref state, true); +} + +#[test] +#[should_panic(expected: 'Nonexistent proposal')] +fn test_state_non_existent() { + let state = COMPONENT_STATE(); + + state._state(1); +} + +#[test] +fn test_state_pending() { + let mut state = COMPONENT_STATE(); + + // The function already asserts the state + setup_pending_proposal(ref state, true); +} + +fn test_state_active_external_version(external_state_version: bool) { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Active; + + // Is active before deadline + start_cheat_block_timestamp_global(deadline - 1); + let current_state = get_state(@state, id, external_state_version); + assert_eq!(current_state, expected); + + // Is active in deadline + start_cheat_block_timestamp_global(deadline); + let current_state = get_state(@state, id, external_state_version); + assert_eq!(current_state, expected); +} + +#[test] +fn test_state_active() { + test_state_active_external_version(true); +} + +fn test_state_defeated_quorum_not_reached_external_version(external_state_version: bool) { + let mut mock_state = CONTRACT_STATE(); + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Defeated; + + start_cheat_block_timestamp_global(deadline + 1); + + // Quorum not reached + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum - 1); + + let current_state = get_mock_state(@mock_state, id, external_state_version); + assert_eq!(current_state, expected); +} + +#[test] +fn test_state_defeated_quorum_not_reached() { + test_state_defeated_quorum_not_reached_external_version(true); +} + +fn test_state_defeated_vote_not_succeeded_external_version(external_state_version: bool) { + let mut mock_state = CONTRACT_STATE(); + let (id, proposal) = get_proposal_info(); + + mock_state.governor.Governor_proposals.write(id, proposal); + + let deadline = proposal.vote_start + proposal.vote_duration; + let expected = ProposalState::Defeated; + + start_cheat_block_timestamp_global(deadline + 1); + + // Quorum reached + let quorum = mock_state.governor.quorum(0); + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum + 1); + + // Vote not succeeded + proposal_votes.against_votes.write(quorum + 1); + + let current_state = get_mock_state(@mock_state, id, external_state_version); + assert_eq!(current_state, expected); +} + +#[test] +fn test_state_defeated_vote_not_succeeded() { + test_state_defeated_vote_not_succeeded_external_version(true); +} + +#[test] +fn test_state_queued() { + let mut mock_state = CONTRACT_STATE(); + + // The function already asserts the state + setup_queued_proposal(ref mock_state, true); +} + +#[test] +fn test_state_succeeded() { + let mut mock_state = CONTRACT_STATE(); + + // The function already asserts the state + setup_succeeded_proposal(ref mock_state, true); +} + +// +// Proposal info +// + +#[test] +fn test_proposal_threshold() { + let mut state = COMPONENT_STATE(); + + let threshold = state.proposal_threshold(); + let expected = GovernorMock::PROPOSAL_THRESHOLD; + assert_eq!(threshold, expected); +} + +#[test] +fn test_proposal_snapshot() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let snapshot = state.proposal_snapshot(id); + let expected = proposal.vote_start; + assert_eq!(snapshot, expected); +} + +#[test] +fn test_proposal_deadline() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let deadline = state.proposal_deadline(id); + let expected = proposal.vote_start + proposal.vote_duration; + assert_eq!(deadline, expected); +} + +#[test] +fn test_proposal_proposer() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let proposer = state.proposal_proposer(id); + let expected = proposal.proposer; + assert_eq!(proposer, expected); +} + +#[test] +fn test_proposal_eta() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let eta = state.proposal_eta(id); + let expected = proposal.eta_seconds; + assert_eq!(eta, expected); +} + +#[test] +fn test_proposal_needs_queuing() { + let mut state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let needs_queuing = state.proposal_needs_queuing(id); + assert_eq!(needs_queuing, false); +} + +#[test] +fn test_voting_delay() { + let mut state = COMPONENT_STATE(); + + let threshold = state.voting_delay(); + let expected = GovernorMock::VOTING_DELAY; + assert_eq!(threshold, expected); +} + +#[test] +fn test_voting_period() { + let mut state = COMPONENT_STATE(); + + let threshold = state.voting_period(); + let expected = GovernorMock::VOTING_PERIOD; + assert_eq!(threshold, expected); +} + +#[test] +fn test_quorum(timepoint: u64) { + let mut state = COMPONENT_STATE(); + + let threshold = state.quorum(timepoint); + let expected = GovernorMock::QUORUM; + assert_eq!(threshold, expected); +} + +// +// get_votes +// + +#[test] +fn test_get_votes() { + let mut state = COMPONENT_STATE(); + let timepoint = 0; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + let votes = state.get_votes(OTHER(), timepoint); + assert_eq!(votes, expected_weight); +} + +#[test] +fn test_get_votes_with_params() { + let mut state = COMPONENT_STATE(); + let timepoint = 0; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + let votes = state.get_votes_with_params(OTHER(), timepoint, @"params"); + assert_eq!(votes, expected_weight); +} + // // Internal // @@ -141,6 +439,15 @@ fn test__hash_proposal() { // Proposal info // +#[test] +fn test__proposal_threshold() { + let mut state = COMPONENT_STATE(); + + let threshold = state._proposal_threshold(); + let expected = GovernorMock::PROPOSAL_THRESHOLD; + assert_eq!(threshold, expected); +} + #[test] fn test__proposal_snapshot() { let mut state = COMPONENT_STATE(); @@ -180,11 +487,11 @@ fn test__proposal_proposer() { #[test] fn test__proposal_eta() { let mut state = COMPONENT_STATE(); - let (_, proposal) = get_proposal_info(); + let (id, proposal) = get_proposal_info(); - state.Governor_proposals.write(1, proposal); + state.Governor_proposals.write(id, proposal); - let eta = state._proposal_eta(1); + let eta = state._proposal_eta(id); let expected = proposal.eta_seconds; assert_eq!(eta, expected); } @@ -257,6 +564,23 @@ fn test_validate_state_invalid() { state.validate_state(id, valid_states); } +// +// _get_votes +// + +#[test] +fn test__get_votes() { + let mut state = COMPONENT_STATE(); + let timepoint = 0; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + let votes = state._get_votes(OTHER(), timepoint, @"params"); + assert_eq!(votes, expected_weight); +} + // // _state // @@ -265,16 +589,16 @@ fn test_validate_state_invalid() { fn test__state_executed() { let mut state = COMPONENT_STATE(); - // The getter already asserts the state - get_executed_proposal(ref state); + // The function already asserts the state + setup_executed_proposal(ref state, false); } #[test] fn test__state_canceled() { let mut state = COMPONENT_STATE(); - // The getter already asserts the state - get_canceled_proposal(ref state); + // The function already asserts the state + setup_canceled_proposal(ref state, false); } #[test] @@ -289,90 +613,39 @@ fn test__state_non_existent() { fn test__state_pending() { let mut state = COMPONENT_STATE(); - // The getter already asserts the state - get_pending_proposal(ref state); + // The function already asserts the state + setup_pending_proposal(ref state, false); } #[test] fn test__state_active() { - let mut state = COMPONENT_STATE(); - let (id, proposal) = get_proposal_info(); - - state.Governor_proposals.write(id, proposal); - - let deadline = proposal.vote_start + proposal.vote_duration; - let expected = ProposalState::Active; - - // Is active before deadline - start_cheat_block_timestamp_global(deadline - 1); - let current_state = state._state(id); - assert_eq!(current_state, expected); - - // Is active in deadline - start_cheat_block_timestamp_global(deadline); - let current_state = state._state(id); - assert_eq!(current_state, expected); + test_state_active_external_version(false); } #[test] fn test__state_defeated_quorum_not_reached() { - let mut mock_state = CONTRACT_STATE(); - let (id, proposal) = get_proposal_info(); - - mock_state.governor.Governor_proposals.write(id, proposal); - - let deadline = proposal.vote_start + proposal.vote_duration; - let expected = ProposalState::Defeated; - - start_cheat_block_timestamp_global(deadline + 1); - - // Quorum not reached - let quorum = mock_state.governor.quorum(0); - let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); - proposal_votes.for_votes.write(quorum - 1); - - let current_state = mock_state.governor._state(id); - assert_eq!(current_state, expected); + test_state_defeated_quorum_not_reached_external_version(false); } #[test] fn test__state_defeated_vote_not_succeeded() { - let mut mock_state = CONTRACT_STATE(); - let (id, proposal) = get_proposal_info(); - - mock_state.governor.Governor_proposals.write(id, proposal); - - let deadline = proposal.vote_start + proposal.vote_duration; - let expected = ProposalState::Defeated; - - start_cheat_block_timestamp_global(deadline + 1); - - // Quorum reached - let quorum = mock_state.governor.quorum(0); - let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); - proposal_votes.for_votes.write(quorum + 1); - - // Vote not succeeded - proposal_votes.against_votes.write(quorum + 1); - - let current_state = mock_state.governor._state(id); - assert_eq!(current_state, expected); + test_state_defeated_vote_not_succeeded_external_version(false); } #[test] fn test__state_queued() { let mut mock_state = CONTRACT_STATE(); - // The getter already asserts the state - get_queued_proposal(ref mock_state); + // The function already asserts the state + setup_queued_proposal(ref mock_state, false); } #[test] fn test__state_succeeded() { let mut mock_state = CONTRACT_STATE(); - // The getter already asserts the state - get_succeeded_proposal(ref mock_state); + // The function already asserts the state + setup_succeeded_proposal(ref mock_state, false); } // @@ -447,7 +720,7 @@ fn test__propose_existent_proposal() { #[test] fn test__cancel_pending() { let mut state = COMPONENT_STATE(); - let (id, _) = get_pending_proposal(ref state); + let (id, _) = setup_pending_proposal(ref state, false); state._cancel(id, 0); @@ -458,7 +731,7 @@ fn test__cancel_pending() { #[test] fn test__cancel_active() { let mut state = COMPONENT_STATE(); - let (id, _) = get_active_proposal(ref state); + let (id, _) = setup_active_proposal(ref state, false); state._cancel(id, 0); @@ -469,7 +742,7 @@ fn test__cancel_active() { #[test] fn test__cancel_defeated() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_defeated_proposal(ref mock_state); + let (id, _) = setup_defeated_proposal(ref mock_state, false); mock_state.governor._cancel(id, 0); @@ -480,7 +753,7 @@ fn test__cancel_defeated() { #[test] fn test__cancel_succeeded() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_succeeded_proposal(ref mock_state); + let (id, _) = setup_succeeded_proposal(ref mock_state, false); mock_state.governor._cancel(id, 0); @@ -491,7 +764,7 @@ fn test__cancel_succeeded() { #[test] fn test__cancel_queued() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_queued_proposal(ref mock_state); + let (id, _) = setup_queued_proposal(ref mock_state, false); mock_state.governor._cancel(id, 0); @@ -503,7 +776,7 @@ fn test__cancel_queued() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cancel_canceled() { let mut state = COMPONENT_STATE(); - let (id, _) = get_canceled_proposal(ref state); + let (id, _) = setup_canceled_proposal(ref state, false); // Cancel again state._cancel(id, 0); @@ -513,7 +786,7 @@ fn test__cancel_canceled() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cancel_executed() { let mut state = COMPONENT_STATE(); - let (id, _) = get_executed_proposal(ref state); + let (id, _) = setup_executed_proposal(ref state, false); state._cancel(id, 0); } @@ -526,7 +799,7 @@ fn test__cancel_executed() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_pending() { let mut state = COMPONENT_STATE(); - let (id, _) = get_pending_proposal(ref state); + let (id, _) = setup_pending_proposal(ref state, false); state._cast_vote(id, OTHER(), 0, "", ""); } @@ -534,14 +807,14 @@ fn test__cast_vote_pending() { #[test] fn test__cast_vote_active_no_params() { let mut state = COMPONENT_STATE(); - let (id, _) = get_active_proposal(ref state); + let (id, _) = setup_active_proposal(ref state, false); let mut spy = spy_events(); let contract_address = test_address(); let reason = "reason"; let expected_weight = 100; - // Mock the get past votes call + // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); let weight = state._cast_vote(id, OTHER(), 0, reason, ""); @@ -553,7 +826,7 @@ fn test__cast_vote_active_no_params() { #[test] fn test__cast_vote_active_with_params() { let mut state = COMPONENT_STATE(); - let (id, _) = get_active_proposal(ref state); + let (id, _) = setup_active_proposal(ref state, false); let mut spy = spy_events(); let contract_address = test_address(); @@ -561,7 +834,7 @@ fn test__cast_vote_active_with_params() { let params = "params"; let expected_weight = 100; - // Mock the get past votes call + // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); let weight = state._cast_vote(id, OTHER(), 0, reason, params); @@ -577,7 +850,7 @@ fn test__cast_vote_active_with_params() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_defeated() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_defeated_proposal(ref mock_state); + let (id, _) = setup_defeated_proposal(ref mock_state, false); mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); } @@ -586,7 +859,7 @@ fn test__cast_vote_defeated() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_succeeded() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_succeeded_proposal(ref mock_state); + let (id, _) = setup_succeeded_proposal(ref mock_state, false); mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); } @@ -595,7 +868,7 @@ fn test__cast_vote_succeeded() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_queued() { let mut mock_state = CONTRACT_STATE(); - let (id, _) = get_queued_proposal(ref mock_state); + let (id, _) = setup_queued_proposal(ref mock_state, false); mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); } @@ -604,7 +877,7 @@ fn test__cast_vote_queued() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_canceled() { let mut state = COMPONENT_STATE(); - let (id, _) = get_canceled_proposal(ref state); + let (id, _) = setup_canceled_proposal(ref state, false); state._cast_vote(id, OTHER(), 0, "", ""); } @@ -613,7 +886,7 @@ fn test__cast_vote_canceled() { #[should_panic(expected: 'Unexpected proposal state')] fn test__cast_vote_executed() { let mut state = COMPONENT_STATE(); - let (id, _) = get_executed_proposal(ref state); + let (id, _) = setup_executed_proposal(ref state, false); state._cast_vote(id, OTHER(), 0, "", ""); } @@ -655,20 +928,24 @@ fn get_calls(to: ContractAddress) -> Span { array![call1, call2].span() } -fn get_pending_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { +fn setup_pending_proposal( + ref state: ComponentState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, proposal) = get_proposal_info(); state.Governor_proposals.write(id, proposal); - let state = state._state(id); + let current_state = get_state(@state, id, external_state_version); let expected = ProposalState::Pending; - assert_eq!(state, expected); + assert_eq!(current_state, expected); (id, proposal) } -fn get_active_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { +fn setup_active_proposal( + ref state: ComponentState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, proposal) = get_proposal_info(); state.Governor_proposals.write(id, proposal); @@ -678,13 +955,15 @@ fn get_active_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { // Is active before deadline start_cheat_block_timestamp_global(deadline - 1); - let current_state = state._state(id); + let current_state = get_state(@state, id, external_state_version); assert_eq!(current_state, expected); (id, proposal) } -fn get_queued_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { +fn setup_queued_proposal( + ref mock_state: GovernorMock::ContractState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, mut proposal) = get_proposal_info(); proposal.eta_seconds = 1; @@ -702,13 +981,15 @@ fn get_queued_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, proposal_votes.against_votes.write(quorum); let expected = ProposalState::Queued; - let current_state = mock_state.governor._state(id); + let current_state = get_mock_state(@mock_state, id, external_state_version); assert_eq!(current_state, expected); (id, proposal) } -fn get_canceled_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { +fn setup_canceled_proposal( + ref state: ComponentState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, proposal) = get_proposal_info(); state.Governor_proposals.write(id, proposal); @@ -716,13 +997,15 @@ fn get_canceled_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { state._cancel(id, 0); let expected = ProposalState::Canceled; - let current_state = state._state(id); + let current_state = get_state(@state, id, external_state_version); assert_eq!(current_state, expected); (id, proposal) } -fn get_defeated_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { +fn setup_defeated_proposal( + ref mock_state: GovernorMock::ContractState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, proposal) = get_proposal_info(); mock_state.governor.Governor_proposals.write(id, proposal); @@ -736,13 +1019,15 @@ fn get_defeated_proposal(ref mock_state: GovernorMock::ContractState) -> (felt25 proposal_votes.for_votes.write(quorum - 1); let expected = ProposalState::Defeated; - let current_state = mock_state.governor._state(id); + let current_state = get_mock_state(@mock_state, id, external_state_version); assert_eq!(current_state, expected); (id, proposal) } -fn get_succeeded_proposal(ref mock_state: GovernorMock::ContractState) -> (felt252, ProposalCore) { +fn setup_succeeded_proposal( + ref mock_state: GovernorMock::ContractState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, proposal) = get_proposal_info(); mock_state.governor.Governor_proposals.write(id, proposal); @@ -760,26 +1045,46 @@ fn get_succeeded_proposal(ref mock_state: GovernorMock::ContractState) -> (felt2 // Vote succeeded proposal_votes.against_votes.write(quorum); - let current_state = mock_state.governor._state(id); + let current_state = get_mock_state(@mock_state, id, external_state_version); assert_eq!(current_state, expected); (id, proposal) } -fn get_executed_proposal(ref state: ComponentState) -> (felt252, ProposalCore) { +fn setup_executed_proposal( + ref state: ComponentState, external_state_version: bool +) -> (felt252, ProposalCore) { let (id, mut proposal) = get_proposal_info(); proposal.executed = true; state.Governor_proposals.write(id, proposal); - let state = state._state(id); + let current_state = get_state(@state, id, external_state_version); let expected = ProposalState::Executed; - assert_eq!(state, expected); + assert_eq!(current_state, expected); (id, proposal) } +fn get_state(state: @ComponentState, id: felt252, external_state_version: bool) -> ProposalState { + if external_state_version { + state.state(id) + } else { + state._state(id) + } +} + +fn get_mock_state( + mock_state: @GovernorMock::ContractState, id: felt252, external_state_version: bool +) -> ProposalState { + if external_state_version { + mock_state.governor.state(id) + } else { + mock_state.governor._state(id) + } +} + // // Event helpers // diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index 3c7b94db2..127fa9c3e 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -12,7 +12,8 @@ pub mod GovernorMock { pub const VOTING_DELAY: u64 = 86400; // 1 day pub const VOTING_PERIOD: u64 = 432_000; // 1 week - pub const PROPOSAL_THRESHOLD: u256 = 0; + pub const PROPOSAL_THRESHOLD: u256 = 10; + pub const QUORUM: u256 = 100_000_000; component!(path: GovernorComponent, storage: governor, event: GovernorEvent); component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent); @@ -84,7 +85,7 @@ pub mod GovernorMock { // SNIP12 Metadata // - impl SNIP12MetadataImpl of SNIP12Metadata { + pub impl SNIP12MetadataImpl of SNIP12Metadata { fn name() -> felt252 { 'APP_NAME' } @@ -101,7 +102,7 @@ pub mod GovernorMock { impl GovernorQuorum of GovernorComponent::GovernorQuorumTrait { /// See `GovernorComponent::GovernorQuorumTrait::quorum`. fn quorum(self: @GovernorComponent::ComponentState, timepoint: u64) -> u256 { - 100_000_000 + QUORUM } } From b928ecf69ffc0d82e5971b4e5c10d6fbea1f0323 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 00:55:59 +0100 Subject: [PATCH 57/76] feat: add more tests --- packages/governance/src/tests/governor/test_governor.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 53f6488d6..597fca377 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -323,7 +323,7 @@ fn test_get_votes_with_params() { // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); - let votes = state.get_votes_with_params(OTHER(), timepoint, @"params"); + let votes = state.get_votes_with_params(OTHER(), timepoint, "params"); assert_eq!(votes, expected_weight); } From 2bc99614676ade9c79b519324d92dfb6f809f393 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 15:37:21 +0100 Subject: [PATCH 58/76] Update packages/governance/src/governor/interface.cairo Co-authored-by: Andrew Fleming --- packages/governance/src/governor/interface.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 7cbec8490..c74db0221 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -110,7 +110,7 @@ pub trait IGovernor { /// Minimum number of cast voted required for a proposal to be successful. /// /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This - /// allows to scale the quorum depending on values such as the total supply of a token at this + /// allows the quorum to scale depending on values such as the total supply of a token at this /// timepoint. fn quorum(self: @TState, timepoint: u64) -> u256; From f01ad5c237e17d2f1fcee7614f0da0b475312b2a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 15:50:45 +0100 Subject: [PATCH 59/76] Update packages/governance/src/governor/interface.cairo Co-authored-by: Andrew Fleming --- packages/governance/src/governor/interface.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index c74db0221..a3e412465 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -202,7 +202,7 @@ pub trait IGovernor { /// /// In cases where the governance executor is some contract other than the governor itself, like /// when using a timelock, this function can be invoked in a governance proposal to recover - /// tokens that was sent to the governor contract by mistake. + /// tokens that were sent to the governor contract by mistake. /// /// NOTE: If the executor is simply the governor itself, use of `relay` is redundant. fn relay(ref self: TState, call: Call); From 5feb1573961831c5b4e24f33d469105bfb277350 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 15:57:49 +0100 Subject: [PATCH 60/76] Update packages/governance/src/governor/governor.cairo Co-authored-by: Andrew Fleming --- packages/governance/src/governor/governor.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index c00a7645b..b6980b582 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -365,7 +365,7 @@ pub mod GovernorComponent { GovernorSettings::voting_period(self) } - /// Minimum number of cast voted required for a proposal to be successful. + /// Minimum number of casted votes required for a proposal to be successful. /// /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This /// allows to scale the quorum depending on values such as the total supply of a token at From c5fdf4557977d15b50d9164b09d42143747ed4e5 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 15:58:07 +0100 Subject: [PATCH 61/76] Update packages/governance/src/governor/governor.cairo Co-authored-by: Andrew Fleming --- packages/governance/src/governor/governor.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index b6980b582..3abe381b0 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -367,8 +367,8 @@ pub mod GovernorComponent { /// Minimum number of casted votes required for a proposal to be successful. /// - /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This - /// allows to scale the quorum depending on values such as the total supply of a token at + /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting votes. This + /// allows the quorum to scale depending on values such as the total supply of a token at /// this timepoint. fn quorum(self: @ComponentState, timepoint: u64) -> u256 { GovernorQuorum::quorum(self, timepoint) From c717405e59affb4f45ca504c4f96af04a6117b60 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 15:58:26 +0100 Subject: [PATCH 62/76] Update packages/governance/src/governor/governor.cairo Co-authored-by: Andrew Fleming --- packages/governance/src/governor/governor.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 3abe381b0..90f11bd2b 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -402,7 +402,7 @@ pub mod GovernorComponent { GovernorCounting::has_voted(self, proposal_id, account) } - /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and + /// Creates a new proposal. Voting starts after the delay specified by `voting_delay` and /// lasts for a duration specified by `voting_period`. Returns the id of the proposal. /// /// This function has opt-in frontrunning protection, described in From 0152aa94378f56e278a672580dc7b41badfce491 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 16:48:33 +0100 Subject: [PATCH 63/76] Update packages/governance/src/governor/proposal_core.cairo Co-authored-by: Andrew Fleming --- .../governance/src/governor/proposal_core.cairo | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index eeeb189d0..ffc385533 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -27,15 +27,15 @@ const _1_BIT_MASK: u256 = 0x1; /// /// The packing is done as follows: /// -/// - The first felt of the tuple contains `proposer` serialized. -/// - The second felt of the tuple contains `vote_start`, `vote_duration`, `executed`, `canceled` -/// and `eta_seconds` organized as presented next. -/// - `vote_start` is stored at range [4,67] bits (0-indexed), taking the most significant usable +/// 1. The first felt of the tuple contains `proposer` serialized. +/// 2. The second felt of the tuple contains `vote_start`, `vote_duration`, `executed`, `canceled` +/// and `eta_seconds` organized as: +/// - `vote_start` is stored at range [4,67] bits (0-indexed), taking the most significant usable /// bits. -/// - `vote_duration` is stored at range [68, 131], following `vote_start`. -/// - `executed` is stored at range [132, 132], following `vote_duration`. -/// - `canceled` is stored at range [133, 133], following `executed`. -/// - `eta_seconds` is stored at range [134, 197], following `canceled`. +/// - `vote_duration` is stored at range [68, 131], following `vote_start`. +/// - `executed` is stored at range [132, 132], following `vote_duration`. +/// - `canceled` is stored at range [133, 133], following `executed`. +/// - `eta_seconds` is stored at range [134, 197], following `canceled`. /// /// NOTE: In the second felt252, the first four bits are skipped to avoid representation errors due /// to `felt252` max value being a bit less than a 252 bits number max value From 1be0449f7b92b8213e82f2ce9a98e06f71bb11ee Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 17:08:11 +0100 Subject: [PATCH 64/76] feat: apply review updates --- .../extensions/governor_counting_simple.cairo | 4 +- .../extensions/governor_settings.cairo | 4 +- .../governor_timelock_execution.cairo | 20 ++++----- .../governor/extensions/governor_votes.cairo | 2 +- .../governor_votes_quorum_fraction.cairo | 4 +- .../src/governor/extensions/interface.cairo | 2 +- .../governance/src/governor/governor.cairo | 42 +++++++++---------- .../governance/src/governor/interface.cairo | 12 ++++-- .../src/tests/governor/test_governor.cairo | 41 ++++++++++-------- packages/test_common/src/mocks/votes.cairo | 2 +- packages/utils/src/bytearray.cairo | 10 ++++- packages/utils/src/lib.cairo | 3 -- 12 files changed, 77 insertions(+), 69 deletions(-) diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index 52d91cf24..ba8c0f7a8 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -4,7 +4,7 @@ /// # GovernorCountingSimple Component /// -/// Extension of GovernorComponent for simple, 3 options, vote counting. +/// Extension of GovernorComponent for simple vote counting with three options. #[starknet::component] pub mod GovernorCountingSimpleComponent { use crate::governor::GovernorComponent::{ @@ -80,7 +80,7 @@ pub mod GovernorCountingSimpleComponent { account: ContractAddress, support: u8, total_weight: u256, - params: @ByteArray + params: Span ) -> u256 { let mut contract = self.get_contract_mut(); let mut this_component = GovernorCountingSimple::get_component_mut(ref contract); diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 9881ed2da..9e8b8f3a4 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -11,7 +11,7 @@ pub mod GovernorSettingsComponent { InternalExtendedTrait, ComponentState as GovernorComponentState }; use crate::governor::GovernorComponent; - use crate::governor::extensions::interface::ISetSettings; + use crate::governor::extensions::interface::IGovernorSettingsAdmin; use openzeppelin_introspection::src5::SRC5Component; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; @@ -105,7 +105,7 @@ pub mod GovernorSettingsComponent { +GovernorComponent::GovernorVotesTrait, +SRC5Component::HasComponent, +Drop - > of ISetSettings> { + > of IGovernorSettingsAdmin> { /// Sets the voting delay. /// /// Emits a `VotingDelayUpdated` event. diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index 46576dc97..185963990 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -90,8 +90,7 @@ pub mod GovernorTimelockExecutionComponent { let this_component = GovernorCoreExecution::get_component(contract); let queue_id = this_component.Governor_timelock_ids.read(proposal_id); - let timelock_controller = this_component.timelock(); - let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + let timelock_dispatcher = this_component.get_timelock_dispatcher(); if timelock_dispatcher.is_operation_pending(queue_id) { ProposalState::Queued @@ -126,8 +125,7 @@ pub mod GovernorTimelockExecutionComponent { let mut contract = self.get_contract_mut(); let mut this_component = GovernorCoreExecution::get_component_mut(ref contract); - let timelock_controller = this_component.Governor_timelock_controller.read(); - let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + let timelock_dispatcher = this_component.get_timelock_dispatcher(); timelock_dispatcher .execute_batch(calls, 0, this_component.timelock_salt(description_hash)); @@ -148,8 +146,7 @@ pub mod GovernorTimelockExecutionComponent { let mut contract = self.get_contract_mut(); let mut this_component = GovernorCoreExecution::get_component_mut(ref contract); - let timelock_controller = this_component.timelock(); - let timelock_dispatcher = ITimelockDispatcher { contract_address: timelock_controller }; + let timelock_dispatcher = this_component.get_timelock_dispatcher(); let delay = timelock_dispatcher.get_min_delay(); let salt = this_component.timelock_salt(description_hash); @@ -188,10 +185,7 @@ pub mod GovernorTimelockExecutionComponent { let timelock_id = this_component.Governor_timelock_ids.read(proposal_id); if timelock_id.is_non_zero() { - let timelock_controller = this_component.timelock(); - let timelock_dispatcher = ITimelockDispatcher { - contract_address: timelock_controller - }; + let timelock_dispatcher = this_component.get_timelock_dispatcher(); timelock_dispatcher.cancel(timelock_id); this_component.Governor_timelock_ids.write(proposal_id, 0); @@ -281,6 +275,12 @@ pub mod GovernorTimelockExecutionComponent { (this.into() ^ description_hash).try_into().unwrap() } + /// Returns the timelock contract address wrapped in a ITimelockDispatcher. + fn get_timelock_dispatcher(self: @ComponentState) -> ITimelockDispatcher { + let timelock_controller = self.timelock(); + ITimelockDispatcher { contract_address: timelock_controller } + } + /// Updates the timelock contract address. /// /// Emits a `TimelockUpdated` event. diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index fade388b4..febeb523c 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -54,7 +54,7 @@ pub mod GovernorVotesComponent { self: @GovernorComponentState, account: ContractAddress, timepoint: u64, - params: @ByteArray + params: Span ) -> u256 { let contract = self.get_contract(); let this_component = GovernorVotes::get_component(contract); diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index cdb491ada..37c4bd683 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -98,7 +98,7 @@ pub mod GovernorVotesQuorumFractionComponent { self: @GovernorComponentState, account: ContractAddress, timepoint: u64, - params: @ByteArray + params: Span ) -> u256 { let contract = self.get_contract(); let this_component = GovernorVotesQuorumFraction::get_component(contract); @@ -133,7 +133,7 @@ pub mod GovernorVotesQuorumFractionComponent { // Optimistic search: check the latest checkpoint. // The initializer call ensures that there is at least one checkpoint in the history. // - // NOTE: This optimization is specially helpful when the supply is not updated often. + // NOTE: This optimization is especially helpful when the supply is not updated often. let (_, key, value) = self .Governor_quorum_numerator_history .deref() diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index 3a959843c..21770c048 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -34,7 +34,7 @@ pub trait ITimelocked { } #[starknet::interface] -pub trait ISetSettings { +pub trait IGovernorSettingsAdmin { /// Sets the voting delay. fn set_voting_delay(ref self: TState, new_voting_delay: u64); diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index c00a7645b..d3f32eb2e 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -14,8 +14,6 @@ pub mod GovernorComponent { use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; - use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; - use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; use openzeppelin_utils::structs::{DoubleEndedQueue, DoubleEndedQueueTrait}; @@ -100,7 +98,7 @@ pub mod GovernorComponent { pub support: u8, pub weight: u256, pub reason: ByteArray, - pub params: ByteArray + pub params: Span } pub mod Errors { @@ -121,7 +119,7 @@ pub mod GovernorComponent { /// methods that don't include them. pub trait ImmutableConfig { /// Temporary defined as a function since constant ByteArray is not supported yet. - fn DEFAULT_PARAMS() -> ByteArray; + fn DEFAULT_PARAMS() -> Span; } // @@ -159,7 +157,7 @@ pub mod GovernorComponent { account: ContractAddress, support: u8, total_weight: u256, - params: @ByteArray + params: Span ) -> u256; /// See `interface::IGovernor::has_voted`. @@ -186,7 +184,7 @@ pub mod GovernorComponent { self: @ComponentState, account: ContractAddress, timepoint: u64, - params: @ByteArray + params: Span ) -> u256; } @@ -381,7 +379,7 @@ pub mod GovernorComponent { fn get_votes( self: @ComponentState, account: ContractAddress, timepoint: u64 ) -> u256 { - self._get_votes(account, timepoint, @Immutable::DEFAULT_PARAMS()) + self._get_votes(account, timepoint, Immutable::DEFAULT_PARAMS()) } /// Voting power of an `account` at a specific `timepoint` given additional encoded @@ -390,9 +388,9 @@ pub mod GovernorComponent { self: @ComponentState, account: ContractAddress, timepoint: u64, - params: ByteArray + params: Span ) -> u256 { - self._get_votes(account, timepoint, @params) + self._get_votes(account, timepoint, params) } /// Returns whether `account` has cast a vote on `proposal_id`. @@ -408,7 +406,7 @@ pub mod GovernorComponent { /// This function has opt-in frontrunning protection, described in /// `is_valid_description_for_proposer`. /// - /// NOTE: The state of the Governor and `targets` may change between the proposal creation + /// NOTE: The state of the Governor and targets may change between the proposal creation /// and its execution. This may be the result of third party actions on the targeted /// contracts, or other governor proposals. For example, the balance of this contract could /// be updated or its access control permissions may be modified, possibly compromising the @@ -438,7 +436,7 @@ pub mod GovernorComponent { let vote_threshold = self._proposal_threshold(); if vote_threshold > 0 { let votes = self - ._get_votes(proposer, self.clock() - 1, @Immutable::DEFAULT_PARAMS()); + ._get_votes(proposer, self.clock() - 1, Immutable::DEFAULT_PARAMS()); assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES); } @@ -595,7 +593,7 @@ pub mod GovernorComponent { proposal_id: felt252, support: u8, reason: ByteArray, - params: ByteArray + params: Span ) -> u256 { let voter = starknet::get_caller_address(); self._cast_vote(proposal_id, voter, support, reason, params) @@ -619,7 +617,7 @@ pub mod GovernorComponent { support: u8, voter: ContractAddress, reason: ByteArray, - params: ByteArray, + params: Span, signature: Span ) -> u256 { 1 @@ -655,8 +653,6 @@ pub mod GovernorComponent { fn initializer(ref self: ComponentState) { let mut src5_component = get_dep_component_mut!(ref self, SRC5); src5_component.register_interface(IGOVERNOR_ID); - src5_component.register_interface(IERC721_RECEIVER_ID); - src5_component.register_interface(IERC1155_RECEIVER_ID); } /// Returns the proposal object given its id. @@ -791,7 +787,7 @@ pub mod GovernorComponent { self: @ComponentState, account: ContractAddress, timepoint: u64, - params: @ByteArray + params: Span ) -> u256 { GovernorVotes::get_votes(self, account, timepoint, params) } @@ -911,7 +907,7 @@ pub mod GovernorComponent { account: ContractAddress, support: u8, total_weight: u256, - params: @ByteArray + params: Span ) -> u256 { self.count_vote(proposal_id, account, support, total_weight, params) } @@ -928,13 +924,13 @@ pub mod GovernorComponent { voter: ContractAddress, support: u8, reason: ByteArray, - params: ByteArray + params: Span ) -> u256 { self.validate_state(proposal_id, array![ProposalState::Active].span()); let snapshot = self._proposal_snapshot(proposal_id); - let total_weight = self._get_votes(voter, snapshot, @params); - let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, @params); + let total_weight = self._get_votes(voter, snapshot, params); + let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, params); if params.len().is_zero() { self.emit(VoteCast { voter, proposal_id, support, weight: voted_weight, reason }); @@ -959,9 +955,9 @@ pub mod GovernorComponent { /// See /// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation /// -/// The `DEFAULT_PARAMS` is set to `""`. +/// The `DEFAULT_PARAMS` is set to an empty span of felts. pub impl DefaultConfig of GovernorComponent::ImmutableConfig { - fn DEFAULT_PARAMS() -> ByteArray { - "" + fn DEFAULT_PARAMS() -> Span { + array![].span() } } diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 7cbec8490..75d14c6e9 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -122,7 +122,7 @@ pub trait IGovernor { /// Voting power of an `account` at a specific `timepoint` given additional encoded parameters. fn get_votes_with_params( - self: @TState, account: ContractAddress, timepoint: u64, params: ByteArray + self: @TState, account: ContractAddress, timepoint: u64, params: Span ) -> u256; /// Returns whether `account` has cast a vote on `proposal_id`. @@ -131,7 +131,7 @@ pub trait IGovernor { /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and /// lasts for a duration specified by `voting_period`. /// - /// NOTE: The state of the Governor and `targets` may change between the proposal creation and + /// NOTE: The state of the Governor and targets may change between the proposal creation and /// its execution. This may be the result of third party actions on the targeted contracts, or /// other governor proposals. For example, the balance of this contract could be updated or its /// access control permissions may be modified, possibly compromising the proposal's ability to @@ -175,7 +175,11 @@ pub trait IGovernor { /// Cast a vote with a reason and additional serialized parameters. fn cast_vote_with_reason_and_params( - ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray, params: ByteArray + ref self: TState, + proposal_id: felt252, + support: u8, + reason: ByteArray, + params: Span ) -> u256; /// Cast a vote using the voter's signature. @@ -194,7 +198,7 @@ pub trait IGovernor { support: u8, voter: ContractAddress, reason: ByteArray, - params: ByteArray, + params: Span, signature: Span ) -> u256; diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 597fca377..7176c4482 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -10,8 +10,6 @@ use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::governor::GovernorMock; use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO}; use openzeppelin_testing::events::EventSpyExt; -use openzeppelin_token::erc1155::interface::IERC1155_RECEIVER_ID; -use openzeppelin_token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin_utils::bytearray::ByteArrayExtTrait; use snforge_std::EventSpy; use snforge_std::{spy_events, test_address}; @@ -319,11 +317,12 @@ fn test_get_votes_with_params() { let mut state = COMPONENT_STATE(); let timepoint = 0; let expected_weight = 100; + let params = array!['param'].span(); // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); - let votes = state.get_votes_with_params(OTHER(), timepoint, "params"); + let votes = state.get_votes_with_params(OTHER(), timepoint, params); assert_eq!(votes, expected_weight); } @@ -339,8 +338,6 @@ fn test_initializer() { state.initializer(); assert!(contract_state.supports_interface(IGOVERNOR_ID)); - assert!(contract_state.supports_interface(IERC721_RECEIVER_ID)); - assert!(contract_state.supports_interface(IERC1155_RECEIVER_ID)); } // @@ -573,11 +570,12 @@ fn test__get_votes() { let mut state = COMPONENT_STATE(); let timepoint = 0; let expected_weight = 100; + let params = array!['param'].span(); // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); - let votes = state._get_votes(OTHER(), timepoint, @"params"); + let votes = state._get_votes(OTHER(), timepoint, params); assert_eq!(votes, expected_weight); } @@ -800,8 +798,9 @@ fn test__cancel_executed() { fn test__cast_vote_pending() { let mut state = COMPONENT_STATE(); let (id, _) = setup_pending_proposal(ref state, false); + let params = array![].span(); - state._cast_vote(id, OTHER(), 0, "", ""); + state._cast_vote(id, OTHER(), 0, "", params); } #[test] @@ -812,12 +811,13 @@ fn test__cast_vote_active_no_params() { let contract_address = test_address(); let reason = "reason"; + let params = array![].span(); let expected_weight = 100; // Mock the get_past_votes call start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); - let weight = state._cast_vote(id, OTHER(), 0, reason, ""); + let weight = state._cast_vote(id, OTHER(), 0, reason, params); assert_eq!(weight, expected_weight); spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @"reason"); @@ -831,7 +831,7 @@ fn test__cast_vote_active_with_params() { let contract_address = test_address(); let reason = "reason"; - let params = "params"; + let params = array!['param'].span(); let expected_weight = 100; // Mock the get_past_votes call @@ -842,7 +842,7 @@ fn test__cast_vote_active_with_params() { spy .assert_event_vote_cast_with_params( - contract_address, OTHER(), id, 0, expected_weight, @"reason", @"params" + contract_address, OTHER(), id, 0, expected_weight, @"reason", params ); } @@ -851,8 +851,9 @@ fn test__cast_vote_active_with_params() { fn test__cast_vote_defeated() { let mut mock_state = CONTRACT_STATE(); let (id, _) = setup_defeated_proposal(ref mock_state, false); + let params = array![].span(); - mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); + mock_state.governor._cast_vote(id, OTHER(), 0, "", params); } #[test] @@ -860,8 +861,9 @@ fn test__cast_vote_defeated() { fn test__cast_vote_succeeded() { let mut mock_state = CONTRACT_STATE(); let (id, _) = setup_succeeded_proposal(ref mock_state, false); + let params = array![].span(); - mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); + mock_state.governor._cast_vote(id, OTHER(), 0, "", params); } #[test] @@ -869,8 +871,9 @@ fn test__cast_vote_succeeded() { fn test__cast_vote_queued() { let mut mock_state = CONTRACT_STATE(); let (id, _) = setup_queued_proposal(ref mock_state, false); + let params = array![].span(); - mock_state.governor._cast_vote(id, OTHER(), 0, "", ""); + mock_state.governor._cast_vote(id, OTHER(), 0, "", params); } #[test] @@ -878,8 +881,9 @@ fn test__cast_vote_queued() { fn test__cast_vote_canceled() { let mut state = COMPONENT_STATE(); let (id, _) = setup_canceled_proposal(ref state, false); + let params = array![].span(); - state._cast_vote(id, OTHER(), 0, "", ""); + state._cast_vote(id, OTHER(), 0, "", params); } #[test] @@ -887,8 +891,9 @@ fn test__cast_vote_canceled() { fn test__cast_vote_executed() { let mut state = COMPONENT_STATE(); let (id, _) = setup_executed_proposal(ref state, false); + let params = array![].span(); - state._cast_vote(id, OTHER(), 0, "", ""); + state._cast_vote(id, OTHER(), 0, "", params); } // @@ -1179,11 +1184,11 @@ pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers { support: u8, weight: u256, reason: @ByteArray, - params: @ByteArray + params: Span ) { let expected = GovernorComponent::Event::VoteCastWithParams( GovernorComponent::VoteCastWithParams { - voter, proposal_id, support, weight, reason: reason.clone(), params: params.clone() + voter, proposal_id, support, weight, reason: reason.clone(), params } ); self.assert_emitted_single(contract, expected); @@ -1197,7 +1202,7 @@ pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers { support: u8, weight: u256, reason: @ByteArray, - params: @ByteArray + params: Span ) { self .assert_event_vote_cast_with_params( diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo index fce9fde9c..eefdce4df 100644 --- a/packages/test_common/src/mocks/votes.cairo +++ b/packages/test_common/src/mocks/votes.cairo @@ -10,7 +10,7 @@ pub mod ERC20VotesMock { component!(path: ERC20Component, storage: erc20, event: ERC20Event); component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); - // Votes and ERC20Votes + // Votes #[abi(embed_v0)] impl VotesImpl = VotesComponent::VotesImpl; impl VotesInternalImpl = VotesComponent::InternalImpl; diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index f87c8d4f5..bcbc89da6 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -2,7 +2,8 @@ // OpenZeppelin Contracts for Cairo v0.19.0 (utils/bytearray.cairo) use core::byte_array::ByteArrayTrait; -use core::poseidon::poseidon_hash_span; +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::pedersen::PedersenTrait; use core::to_byte_array::FormatAsByteArray; /// Reads n bytes from a byte array starting from a given index. @@ -50,7 +51,12 @@ pub fn hash_byte_array(data: @ByteArray) -> felt252 { let mut serialized = array![]; data.serialize(ref serialized); - poseidon_hash_span(serialized.span()) + let mut state = PedersenTrait::new(0); + for elem in serialized { + state = state.update_with(elem); + }; + state = state.update_with(serialized.len()); + state.finalize() } /// ByteArray extension trait. diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index e213a2689..02dcf735f 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.19.0 (utils/lib.cairo) - pub mod bytearray; pub mod cryptography; pub mod deployments; From 70a278522608bbf3868ab5fd0bf1f355b9444be0 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 17:08:37 +0100 Subject: [PATCH 65/76] fix: linter --- packages/governance/src/governor/governor.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 600000bfb..23272545a 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -365,9 +365,9 @@ pub mod GovernorComponent { /// Minimum number of casted votes required for a proposal to be successful. /// - /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting votes. This - /// allows the quorum to scale depending on values such as the total supply of a token at - /// this timepoint. + /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting votes. + /// This allows the quorum to scale depending on values such as the total supply of a token + /// at this timepoint. fn quorum(self: @ComponentState, timepoint: u64) -> u256 { GovernorQuorum::quorum(self, timepoint) } From 3c67aac961953b640b03447e4abcb429588bd4aa Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 12 Nov 2024 17:13:31 +0100 Subject: [PATCH 66/76] fix: bytearray --- packages/utils/src/bytearray.cairo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index bcbc89da6..83b0a8f29 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -49,13 +49,15 @@ pub fn to_byte_array, +Copy>( /// then hashing the span using the Poseidon hash algorithm. pub fn hash_byte_array(data: @ByteArray) -> felt252 { let mut serialized = array![]; + data.serialize(ref serialized); + let len = serialized.len(); let mut state = PedersenTrait::new(0); for elem in serialized { state = state.update_with(elem); }; - state = state.update_with(serialized.len()); + state = state.update_with(len); state.finalize() } From 81aced26ccbcb0847d4b65566ee4b7b77f310eb0 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Wed, 13 Nov 2024 14:00:36 +0100 Subject: [PATCH 67/76] feat: add more tests --- .../extensions/governor_counting_simple.cairo | 2 +- .../extensions/governor_settings.cairo | 2 +- .../governor_timelock_execution.cairo | 2 +- .../governor/extensions/governor_votes.cairo | 2 +- .../governor_votes_quorum_fraction.cairo | 2 +- .../governance/src/governor/governor.cairo | 36 +- .../src/tests/governor/test_governor.cairo | 573 ++++++++++++++++-- 7 files changed, 540 insertions(+), 79 deletions(-) diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo index ba8c0f7a8..5983a5e37 100644 --- a/packages/governance/src/governor/extensions/governor_counting_simple.cairo +++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo @@ -49,7 +49,7 @@ pub mod GovernorCountingSimpleComponent { pub has_voted: Map } - mod Errors { + pub mod Errors { pub const ALREADY_CAST_VOTE: felt252 = 'Already cast vote'; pub const INVALID_VOTE_TYPE: felt252 = 'Invalid vote type'; } diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 9e8b8f3a4..893019096 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -51,7 +51,7 @@ pub mod GovernorSettingsComponent { pub new_proposal_threshold: u256 } - mod Errors { + pub mod Errors { pub const INVALID_VOTING_PERIOD: felt252 = 'Invalid voting period'; } diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index 185963990..6cb98675f 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -58,7 +58,7 @@ pub mod GovernorTimelockExecutionComponent { pub new_timelock: ContractAddress } - mod Errors { + pub mod Errors { pub const INVALID_TIMELOCK_CONTROLLER: felt252 = 'Invalid timelock controller'; } diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo index febeb523c..d997bda35 100644 --- a/packages/governance/src/governor/extensions/governor_votes.cairo +++ b/packages/governance/src/governor/extensions/governor_votes.cairo @@ -22,7 +22,7 @@ pub mod GovernorVotesComponent { pub Governor_token: ContractAddress } - mod Errors { + pub mod Errors { pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; } diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index 37c4bd683..ececf0200 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -37,7 +37,7 @@ pub mod GovernorVotesQuorumFractionComponent { pub new_quorum_numerator: u256 } - mod Errors { + pub mod Errors { pub const INVALID_QUORUM_FRACTION: felt252 = 'Invalid quorum fraction'; pub const INVALID_TOKEN: felt252 = 'Invalid votes token'; } diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 23272545a..05dbad161 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -16,7 +16,6 @@ pub mod GovernorComponent { use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; - use openzeppelin_utils::structs::{DoubleEndedQueue, DoubleEndedQueueTrait}; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, SyscallResultTrait}; @@ -24,7 +23,6 @@ pub mod GovernorComponent { #[storage] pub struct Storage { pub Governor_proposals: Map, - pub Governor_governance_call: DoubleEndedQueue } #[event] @@ -195,6 +193,15 @@ pub mod GovernorComponent { /// Address through which the governor executes action. /// Should be used to specify whether the module execute actions through another contract /// such as a timelock. + /// + /// NOTE: MUST be the governor itself, or an instance of TimelockController with the + /// governor as the only proposer, canceller, and executor. + /// + /// WARNING: When the executor is not the governor itself (i.e. a timelock), it can call + /// functions that are restricted with the `assert_only_governance` modifier, and also + /// potentially execute transactions on behalf of the governor. Because of this, this module + /// is designed to work with the TimelockController as the unique potential external + /// executor. fn executor(self: @ComponentState) -> ContractAddress; /// Execution mechanism. Can be used to modify the way execution is @@ -502,22 +509,8 @@ pub mod GovernorComponent { proposal.executed = true; self.Governor_proposals.write(proposal_id, proposal); - let self_executor = self.executor() == starknet::get_contract_address(); - // Register governance call in queue before execution - if !self_executor { // TODO: save the calldatas in the governance_call queue - } - self.execute_operations(proposal_id, calls, description_hash); - // Clean up the governance call queue - if !self_executor - && (@self) - .Governor_governance_call - .deref() - .len() - .into() > 0_u256 { // TODO: clean up the queue - } - self.emit(ProposalExecuted { proposal_id }); proposal_id @@ -756,13 +749,16 @@ pub mod GovernorComponent { impl GovernorVotes: GovernorVotesTrait, +Drop > of InternalExtendedTrait { + /// Asserts that the caller is the governance executor. + /// + /// WARNING: When the executor is not the governor itself (i.e. a timelock), it can call + /// functions that are restricted with this modifier, and also potentially execute + /// transactions on behalf of the governor. Because of this, this module is designed to work + /// with the TimelockController as the unique potential external executor. The timelock + /// MUST have the governor as the only proposer, canceller, and executor. fn assert_only_governance(self: @ComponentState) { let executor = self.executor(); assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY); - - // TODO: either check that the calldata matches the whitelist or assume the Executor - // can't execute proposals not created from the Governor itself. - () } /// Validates that the proposal is in one of the expected states. diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 7176c4482..5443b450a 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -3,20 +3,25 @@ use core::num::traits::Zero; use core::pedersen::PedersenTrait; use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl}; use crate::governor::interface::{IGovernor, IGOVERNOR_ID, ProposalState}; +use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait}; use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore}; use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::governor::GovernorMock; +use openzeppelin_test_common::mocks::timelock::{ + IMockContractDispatcher, IMockContractDispatcherTrait +}; +use openzeppelin_testing as utils; use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_utils::bytearray::ByteArrayExtTrait; use snforge_std::EventSpy; use snforge_std::{spy_events, test_address}; use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp_global, start_mock_call}; -use starknet::ContractAddress; use starknet::account::Call; use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess}; +use starknet::{ContractAddress, contract_address_const}; type ComponentState = GovernorComponent::ComponentState; @@ -28,6 +33,39 @@ fn COMPONENT_STATE() -> ComponentState { GovernorComponent::component_state_for_testing() } +// +// Constants +// + +fn VOTES_TOKEN() -> ContractAddress { + contract_address_const::<'VOTES_TOKEN'>() +} + +// +// Dispatchers +// + +fn deploy_governor() -> IGovernorDispatcher { + let mut calldata = array![VOTES_TOKEN().into()]; + + let address = utils::declare_and_deploy("GovernorMock", calldata); + IGovernorDispatcher { contract_address: address } +} + +fn deploy_mock_target() -> IMockContractDispatcher { + let mut calldata = array![]; + + let address = utils::declare_and_deploy("MockContract", calldata); + IMockContractDispatcher { contract_address: address } +} + +fn setup_dispatchers() -> (IGovernorDispatcher, IMockContractDispatcher) { + let governor = deploy_governor(); + let target = deploy_mock_target(); + + (governor, target) +} + // // External // @@ -56,7 +94,7 @@ fn test_counting_mode() { #[test] fn test_hash_proposal() { let state = COMPONENT_STATE(); - let calls = get_calls(ZERO()); + let calls = get_calls(ZERO(), false); let description = @"proposal description"; let description_hash = description.hash(); @@ -202,7 +240,7 @@ fn test_state_succeeded() { #[test] fn test_proposal_threshold() { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let threshold = state.proposal_threshold(); let expected = GovernorMock::PROPOSAL_THRESHOLD; @@ -270,7 +308,7 @@ fn test_proposal_needs_queuing() { #[test] fn test_voting_delay() { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let threshold = state.voting_delay(); let expected = GovernorMock::VOTING_DELAY; @@ -279,7 +317,7 @@ fn test_voting_delay() { #[test] fn test_voting_period() { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let threshold = state.voting_period(); let expected = GovernorMock::VOTING_PERIOD; @@ -288,7 +326,7 @@ fn test_voting_period() { #[test] fn test_quorum(timepoint: u64) { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let threshold = state.quorum(timepoint); let expected = GovernorMock::QUORUM; @@ -301,7 +339,7 @@ fn test_quorum(timepoint: u64) { #[test] fn test_get_votes() { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let timepoint = 0; let expected_weight = 100; @@ -314,7 +352,7 @@ fn test_get_votes() { #[test] fn test_get_votes_with_params() { - let mut state = COMPONENT_STATE(); + let state = COMPONENT_STATE(); let timepoint = 0; let expected_weight = 100; let params = array!['param'].span(); @@ -326,6 +364,434 @@ fn test_get_votes_with_params() { assert_eq!(votes, expected_weight); } +// +// has_voted +// + +#[test] +fn test_has_voted() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_active_proposal(ref state, false); + + let reason = "reason"; + let params = array![].span(); + + // 1. Assert has not voted + let has_not_voted = !state.has_voted(id, OTHER()); + assert!(has_not_voted); + + // 2. Cast vote + start_mock_call(Zero::zero(), selector!("get_past_votes"), 100_u256); + state._cast_vote(id, OTHER(), 0, reason, params); + + // 3. Assert has voted + let has_voted = state.has_voted(id, OTHER()); + assert!(has_voted); +} + +// +// propose +// + +fn test_propose_external_version(external_state_version: bool) { + let mut state = COMPONENT_STATE(); + let mut spy = spy_events(); + let contract_address = test_address(); + + let calls = get_calls(OTHER(), false); + let description = "proposal description"; + let description_snap = @description; + let proposer = ADMIN(); + let vote_start = starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY; + let vote_end = vote_start + GovernorMock::VOTING_PERIOD; + + // 1. Check id + let id = if external_state_version { + state.propose(calls, description) + } else { + state._propose(calls, description_snap, proposer) + }; + let expected_id = hash_proposal(calls, description_snap.hash()); + assert_eq!(id, expected_id); + + // 2. Check event + spy + .assert_only_event_proposal_created( + contract_address, + expected_id, + proposer, + calls, + array![].span(), + vote_start, + vote_end, + description_snap + ); + + // 3. Check proposal + let proposal = state.get_proposal(id); + let expected = ProposalCore { + proposer: ADMIN(), + vote_start: starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY, + vote_duration: GovernorMock::VOTING_PERIOD, + executed: false, + canceled: false, + eta_seconds: 0 + }; + + assert_eq!(proposal, expected); +} + +#[test] +fn test_propose() { + let votes = GovernorMock::PROPOSAL_THRESHOLD + 1; + + start_cheat_block_timestamp_global(10); + start_mock_call(Zero::zero(), selector!("get_past_votes"), votes); + start_cheat_caller_address(test_address(), ADMIN()); + + test_propose_external_version(true); +} + +#[test] +#[should_panic(expected: 'Existent proposal')] +fn test_propose_existent_proposal() { + let mut state = COMPONENT_STATE(); + let calls = get_calls(OTHER(), false); + let description = "proposal description"; + let votes = GovernorMock::PROPOSAL_THRESHOLD + 1; + + start_cheat_block_timestamp_global(10); + start_mock_call(Zero::zero(), selector!("get_past_votes"), votes); + start_cheat_caller_address(test_address(), ADMIN()); + + state.propose(calls, description.clone()); + + // Propose again + state.propose(calls, description); +} + +#[test] +#[should_panic(expected: 'Insufficient votes')] +fn test_propose_insufficient_proposer_votes() { + let mut state = COMPONENT_STATE(); + let votes = GovernorMock::PROPOSAL_THRESHOLD - 1; + + start_cheat_block_timestamp_global(10); + start_mock_call(Zero::zero(), selector!("get_past_votes"), votes); + + let calls = get_calls(ZERO(), false); + let description = "proposal description"; + + state.propose(calls, description); +} + +#[test] +#[should_panic(expected: 'Restricted proposer')] +fn test_propose_restricted_proposer() { + let mut state = COMPONENT_STATE(); + + let calls = get_calls(ZERO(), false); + let description = + "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + + state.propose(calls, description); +} + +// +// queue +// + +// +// execute +// + +#[test] +fn test_execute() { + let (mut governor, target) = setup_dispatchers(); + let new_number = 125; + + let call = Call { + to: target.contract_address, + selector: selector!("set_number"), + calldata: array![new_number].span() + }; + let calls = array![call].span(); + let description = "proposal description"; + + let number = target.get_number(); + assert_eq!(number, 0); + + // Mock the get_past_votes call + let quorum = GovernorMock::QUORUM; + start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum); + + // 1. Propose + let mut current_time = 10; + start_cheat_block_timestamp_global(current_time); + let id = governor.propose(calls, description.clone()); + + // 2. Cast vote + + // Fast forward the vote delay + current_time += GovernorMock::VOTING_DELAY; + start_cheat_block_timestamp_global(current_time); + + // Cast vote + governor.cast_vote(id, 1); + + // 3. Execute + + // Fast forward the vote duration + current_time += (GovernorMock::VOTING_PERIOD + 1); + start_cheat_block_timestamp_global(current_time); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Succeeded); + + let mut spy = spy_events(); + governor.execute(calls, (@description).hash()); + + // 4. Assertions + let number = target.get_number(); + assert_eq!(number, new_number); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Executed); + + spy.assert_only_event_proposal_executed(governor.contract_address, id); +} + +#[test] +fn test_execute_correct_id() { + let mut mock_state = CONTRACT_STATE(); + setup_succeeded_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), true); + let description = @"proposal description"; + + let id = mock_state.governor.execute(calls, description.hash()); + let expected_id = hash_proposal(calls, description.hash()); + assert_eq!(id, expected_id); +} + +#[test] +fn test_execute_succeeded_passes() { + let mut mock_state = CONTRACT_STATE(); + setup_succeeded_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), true); + let description = @"proposal description"; + + mock_state.governor.execute(calls, description.hash()); +} + +#[test] +fn test_execute_queued_passes() { + let mut mock_state = CONTRACT_STATE(); + setup_queued_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), true); + let description = @"proposal description"; + + mock_state.governor.execute(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_execute_pending() { + let mut state = COMPONENT_STATE(); + setup_pending_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.execute(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_execute_active() { + let mut state = COMPONENT_STATE(); + setup_active_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.execute(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_execute_defeated() { + let mut mock_state = CONTRACT_STATE(); + setup_defeated_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + mock_state.governor.execute(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_execute_canceled() { + let mut state = COMPONENT_STATE(); + setup_canceled_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.execute(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_execute_executed() { + let mut state = COMPONENT_STATE(); + setup_executed_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.execute(calls, description.hash()); +} + +// +// cancel +// + +#[test] +fn test_cancel() { + let mut state = COMPONENT_STATE(); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + let proposer = ADMIN(); + + // 1. Propose + let id = state._propose(calls, description, proposer); + + let proposal_state = state.state(id); + assert_eq!(proposal_state, ProposalState::Pending); + + // 2. Cancel + let mut spy = spy_events(); + + // Proposer must be the caller + start_cheat_caller_address(test_address(), ADMIN()); + + state.cancel(calls, description.hash()); + + // 3. Assertions + let proposal_state = state.state(id); + assert_eq!(proposal_state, ProposalState::Canceled); + + spy.assert_only_event_proposal_canceled(test_address(), id); +} + +#[test] +fn test_cancel_correct_id() { + let mut state = COMPONENT_STATE(); + setup_pending_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + // Proposer must be the caller + start_cheat_caller_address(test_address(), ADMIN()); + + let id = state.cancel(calls, description.hash()); + let expected_id = hash_proposal(calls, description.hash()); + assert_eq!(id, expected_id); +} + +#[test] +#[should_panic(expected: 'Proposer only')] +fn test_cancel_invalid_caller() { + let mut state = COMPONENT_STATE(); + setup_pending_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + // Proposer must be the caller + start_cheat_caller_address(test_address(), OTHER()); + + state.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_succeeded() { + let mut mock_state = CONTRACT_STATE(); + setup_succeeded_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), true); + let description = @"proposal description"; + + mock_state.governor.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_queued() { + let mut mock_state = CONTRACT_STATE(); + setup_queued_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), true); + let description = @"proposal description"; + + mock_state.governor.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_active() { + let mut state = COMPONENT_STATE(); + setup_active_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_defeated() { + let mut mock_state = CONTRACT_STATE(); + setup_defeated_proposal(ref mock_state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + mock_state.governor.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_canceled() { + let mut state = COMPONENT_STATE(); + setup_canceled_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.cancel(calls, description.hash()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_executed() { + let mut state = COMPONENT_STATE(); + setup_executed_proposal(ref state, false); + + let calls = get_calls(OTHER(), false); + let description = @"proposal description"; + + state.cancel(calls, description.hash()); +} + // // Internal // @@ -387,7 +853,8 @@ fn test_is_valid_description_too_short() { #[test] fn test_is_valid_description_wrong_suffix() { let state = COMPONENT_STATE(); - let description = "?proposer=0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + let description = + "?proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description); assert!(is_valid); @@ -422,7 +889,7 @@ fn test_is_valid_description_valid_proposer() { #[test] fn test__hash_proposal() { let state = COMPONENT_STATE(); - let calls = get_calls(ZERO()); + let calls = get_calls(ZERO(), false); let description = @"proposal description"; let description_hash = description.hash(); @@ -652,54 +1119,14 @@ fn test__state_succeeded() { #[test] fn test__propose() { - let mut state = COMPONENT_STATE(); - let mut spy = spy_events(); - let contract_address = test_address(); - - let calls = get_calls(OTHER()); - let description = @"proposal description"; - let proposer = ADMIN(); - let vote_start = starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY; - let vote_end = vote_start + GovernorMock::VOTING_PERIOD; - - let id = state._propose(calls, description, proposer); - - // Check id - let expected_id = hash_proposal(calls, description.hash()); - assert_eq!(id, expected_id); - - // Check event - spy - .assert_only_event_proposal_created( - contract_address, - expected_id, - proposer, - calls, - array![].span(), - vote_start, - vote_end, - description - ); - - // Check proposal - let proposal = state.get_proposal(id); - let expected = ProposalCore { - proposer: ADMIN(), - vote_start: starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY, - vote_duration: GovernorMock::VOTING_PERIOD, - executed: false, - canceled: false, - eta_seconds: 0 - }; - - assert_eq!(proposal, expected); + test_propose_external_version(false); } #[test] #[should_panic(expected: 'Existent proposal')] fn test__propose_existent_proposal() { let mut state = COMPONENT_STATE(); - let calls = get_calls(OTHER()); + let calls = get_calls(OTHER(), false); let description = @"proposal description"; let proposer = ADMIN(); @@ -901,7 +1328,8 @@ fn test__cast_vote_executed() { // fn get_proposal_info() -> (felt252, ProposalCore) { - get_proposal_with_id(array![].span(), @"") + let calls = get_calls(OTHER(), false); + get_proposal_with_id(calls, @"proposal description") } fn get_proposal_with_id(calls: Span, description: @ByteArray) -> (felt252, ProposalCore) { @@ -926,10 +1354,15 @@ fn hash_proposal(calls: Span, description_hash: felt252) -> felt252 { PedersenTrait::new(0).update_with(calls).update_with(description_hash).finalize() } -fn get_calls(to: ContractAddress) -> Span { +fn get_calls(to: ContractAddress, mock_syscalls: bool) -> Span { let call1 = Call { to, selector: selector!("test1"), calldata: array![].span() }; let call2 = Call { to, selector: selector!("test2"), calldata: array![].span() }; + if mock_syscalls { + start_mock_call(to, selector!("test1"), 'test1'); + start_mock_call(to, selector!("test2"), 'test2'); + } + array![call1, call2].span() } @@ -1210,4 +1643,36 @@ pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers { ); self.assert_no_events_left_from(contract); } + + fn assert_event_proposal_executed( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252 + ) { + let expected = GovernorComponent::Event::ProposalExecuted( + GovernorComponent::ProposalExecuted { proposal_id } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_proposal_executed( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252 + ) { + self.assert_event_proposal_executed(contract, proposal_id); + self.assert_no_events_left_from(contract); + } + + fn assert_event_proposal_canceled( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252 + ) { + let expected = GovernorComponent::Event::ProposalCanceled( + GovernorComponent::ProposalCanceled { proposal_id } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_proposal_canceled( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252 + ) { + self.assert_event_proposal_canceled(contract, proposal_id); + self.assert_no_events_left_from(contract); + } } From 85b032c45a7644cc043e2c220d475dcc1a3a549a Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 16:28:23 +0100 Subject: [PATCH 68/76] feat: add more tests --- .../governance/src/governor/governor.cairo | 6 +- .../src/tests/governor/test_governor.cairo | 249 ++++++++++++++++++ 2 files changed, 253 insertions(+), 2 deletions(-) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 05dbad161..53d36c69c 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -580,7 +580,8 @@ pub mod GovernorComponent { /// /// - The proposal must be active. /// - /// Emits a `VoteCast` event. + /// Emits a `VoteCast` event if no params are provided. + /// Emits a `VoteCastWithParams` event otherwise. fn cast_vote_with_reason_and_params( ref self: ComponentState, proposal_id: felt252, @@ -913,7 +914,8 @@ pub mod GovernorComponent { /// Checks that the vote is pending, that it has not been cast yet, retrieve /// voting weight using `get_votes` and call the `_count_vote` internal function. /// - /// Emits a `VoteCast` event. + /// Emits a `VoteCast` event if no params are provided. + /// Emits a `VoteCastWithParams` event otherwise. fn _cast_vote( ref self: ComponentState, proposal_id: felt252, diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 5443b450a..262c5339b 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -792,6 +792,255 @@ fn test_cancel_executed() { state.cancel(calls, description.hash()); } +// +// cast_vote +// + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_pending() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_pending_proposal(ref state, false); + + state.cast_vote(id, 0); +} + +#[test] +fn test_cast_vote_active() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_active_proposal(ref state, false); + let mut spy = spy_events(); + let contract_address = test_address(); + + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + start_cheat_caller_address(contract_address, OTHER()); + let weight = state.cast_vote(id, 0); + assert_eq!(weight, expected_weight); + + spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_defeated() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_defeated_proposal(ref mock_state, false); + + mock_state.governor.cast_vote(id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_succeeded_proposal(ref mock_state, false); + + mock_state.governor.cast_vote(id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_queued() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_queued_proposal(ref mock_state, false); + + mock_state.governor.cast_vote(id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_canceled() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_canceled_proposal(ref state, false); + + state.cast_vote(id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_executed() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_executed_proposal(ref state, false); + + state.cast_vote(id, 0); +} + +// +// cast_vote_with_reason +// + +#[test] +fn test_cast_vote_with_reason_active() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_active_proposal(ref state, false); + let mut spy = spy_events(); + let contract_address = test_address(); + + let reason = "proposal reason"; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + start_cheat_caller_address(contract_address, OTHER()); + let weight = state.cast_vote_with_reason(id, 0, reason.clone()); + assert_eq!(weight, expected_weight); + + spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @reason); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_pending() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_pending_proposal(ref state, false); + + state.cast_vote_with_reason(id, 0, ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_defeated() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_defeated_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason(id, 0, ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_succeeded_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason(id, 0, ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_queued() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_queued_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason(id, 0, ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_canceled() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_canceled_proposal(ref state, false); + + state.cast_vote_with_reason(id, 0, ""); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_executed() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_executed_proposal(ref state, false); + + state.cast_vote_with_reason(id, 0, ""); +} + +// +// cast_vote_with_reason_and_params +// + +#[test] +fn test_cast_vote_with_reason_and_params_active() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_active_proposal(ref state, false); + let mut spy = spy_events(); + let contract_address = test_address(); + + let params = array!['param1', 'param2'].span(); + let reason = "proposal reason"; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + start_cheat_caller_address(contract_address, OTHER()); + let weight = state.cast_vote_with_reason_and_params(id, 0, reason.clone(), params); + assert_eq!(weight, expected_weight); + + spy + .assert_only_event_vote_cast_with_params( + contract_address, OTHER(), id, 0, expected_weight, @reason, params + ); +} + +#[test] +fn test_cast_vote_with_reason_and_params_active_no_params() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_active_proposal(ref state, false); + let mut spy = spy_events(); + let contract_address = test_address(); + + let params = array![].span(); + let reason = "proposal reason"; + let expected_weight = 100; + + // Mock the get_past_votes call + start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight); + + start_cheat_caller_address(contract_address, OTHER()); + let weight = state.cast_vote_with_reason_and_params(id, 0, reason.clone(), params); + assert_eq!(weight, expected_weight); + + spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @reason); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_and_params_defeated() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_defeated_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_and_params_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_succeeded_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_and_params_queued() { + let mut mock_state = CONTRACT_STATE(); + let (id, _) = setup_queued_proposal(ref mock_state, false); + + mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_and_params_canceled() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_canceled_proposal(ref state, false); + + state.cast_vote_with_reason_and_params(id, 0, "", array![].span()); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cast_vote_with_reason_and_params_executed() { + let mut state = COMPONENT_STATE(); + let (id, _) = setup_executed_proposal(ref state, false); + + state.cast_vote_with_reason_and_params(id, 0, "", array![].span()); +} + // // Internal // From 8d96ff21ed88f1fc0a300408a3f92603e0a73e96 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 17:08:44 +0100 Subject: [PATCH 69/76] Update packages/utils/src/bytearray.cairo Co-authored-by: immrsd <103599616+immrsd@users.noreply.github.com> --- packages/utils/src/bytearray.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index 83b0a8f29..a64b4da2e 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -31,7 +31,7 @@ pub fn to_byte_array, +Copy>( ) -> ByteArray { let value: felt252 = (*value).into(); let base: felt252 = base.into(); - let mut byte_array = value.format_as_byte_array(base.try_into().unwrap()); + let mut byte_array = value.format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0')); if padding.into() > byte_array.len() { let mut padding = padding.into() - byte_array.len(); From b43bc0d9f3db9bb685a00d6daf6a61c862f7079e Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 18:03:36 +0100 Subject: [PATCH 70/76] feat: apply review updates --- .../extensions/governor_settings.cairo | 40 ++++++++++++----- .../governor_votes_quorum_fraction.cairo | 18 +++++--- .../governance/src/governor/governor.cairo | 8 ++-- .../governance/src/governor/interface.cairo | 9 ++-- .../src/governor/proposal_core.cairo | 45 ++++++++++--------- packages/utils/src/bytearray.cairo | 11 ++--- 6 files changed, 81 insertions(+), 50 deletions(-) diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index 893019096..783b794da 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -95,8 +95,8 @@ pub mod GovernorSettingsComponent { // External // - #[embeddable_as(SetSettingsImpl)] - impl SetSettings< + #[embeddable_as(GovernorSettingsAdminImpl)] + impl GovernorSettingsAdmin< TContractState, +HasComponent, +GovernorComponent::HasComponent, @@ -108,42 +108,60 @@ pub mod GovernorSettingsComponent { > of IGovernorSettingsAdmin> { /// Sets the voting delay. /// - /// Emits a `VotingDelayUpdated` event. + /// NOTE: This function does not emit an event if the new voting delay is the same as the + /// old one. + /// + /// May emit a `VotingDelayUpdated` event. fn set_voting_delay(ref self: ComponentState, new_voting_delay: u64) { self.assert_only_governance(); let old_voting_delay = self.Governor_voting_delay.read(); - self.emit(VotingDelayUpdated { old_voting_delay, new_voting_delay }); - self.Governor_voting_delay.write(new_voting_delay); + if old_voting_delay != new_voting_delay { + self.emit(VotingDelayUpdated { old_voting_delay, new_voting_delay }); + self.Governor_voting_delay.write(new_voting_delay); + } } /// Sets the voting period. /// + /// NOTE: This function does not emit an event if the new voting period is the same as the + /// old one. + /// /// Requirements: /// /// - `new_voting_period` must be greater than 0. /// - /// Emits a `VotingPeriodUpdated` event. + /// May emit a `VotingPeriodUpdated` event. fn set_voting_period(ref self: ComponentState, new_voting_period: u64) { self.assert_only_governance(); assert(new_voting_period > 0, Errors::INVALID_VOTING_PERIOD); let old_voting_period = self.Governor_voting_period.read(); - self.emit(VotingPeriodUpdated { old_voting_period, new_voting_period }); - self.Governor_voting_period.write(new_voting_period); + if old_voting_period != new_voting_period { + self.emit(VotingPeriodUpdated { old_voting_period, new_voting_period }); + self.Governor_voting_period.write(new_voting_period); + } } /// Sets the proposal threshold. /// - /// Emits a `ProposalThresholdUpdated` event. + /// NOTE: This function does not emit an event if the new proposal threshold is the same as + /// the old one. + /// + /// May emit a `ProposalThresholdUpdated` event. fn set_proposal_threshold( ref self: ComponentState, new_proposal_threshold: u256 ) { self.assert_only_governance(); let old_proposal_threshold = self.Governor_proposal_threshold.read(); - self.emit(ProposalThresholdUpdated { old_proposal_threshold, new_proposal_threshold }); - self.Governor_proposal_threshold.write(new_proposal_threshold); + if old_proposal_threshold != new_proposal_threshold { + let event = ProposalThresholdUpdated { + old_proposal_threshold, new_proposal_threshold + }; + self.emit(event); + self.Governor_proposal_threshold.write(new_proposal_threshold); + } } } diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo index ececf0200..a4df1779d 100644 --- a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo +++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo @@ -149,7 +149,7 @@ pub mod GovernorVotesQuorumFractionComponent { /// Returns the quorum denominator. fn quorum_denominator(self: @ComponentState) -> u256 { - 100 + 1000 } } @@ -187,11 +187,14 @@ pub mod GovernorVotesQuorumFractionComponent { /// Updates the quorum numerator. /// + /// NOTE: This function does not emit an event if the new quorum numerator is the same as + /// the old one. + /// /// Requirements: /// /// - `new_quorum_numerator` must be less than `quorum_denominator`. /// - /// Emits a `QuorumNumeratorUpdated` event. + /// May emit a `QuorumNumeratorUpdated` event. fn update_quorum_numerator( ref self: ComponentState, new_quorum_numerator: u256 ) { @@ -200,13 +203,16 @@ pub mod GovernorVotesQuorumFractionComponent { assert(new_quorum_numerator <= denominator, Errors::INVALID_QUORUM_FRACTION); let old_quorum_numerator = self.current_quorum_numerator(); - let governor_component = get_dep_component!(@self, Governor); - let clock = governor_component.clock(); + if old_quorum_numerator != new_quorum_numerator { + let governor_component = get_dep_component!(@self, Governor); - self.Governor_quorum_numerator_history.deref().push(clock, new_quorum_numerator); + let clock = governor_component.clock(); - self.emit(QuorumNumeratorUpdated { old_quorum_numerator, new_quorum_numerator }); + self.Governor_quorum_numerator_history.deref().push(clock, new_quorum_numerator); + + self.emit(QuorumNumeratorUpdated { old_quorum_numerator, new_quorum_numerator }); + } } } } diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 53d36c69c..bfaad1dd8 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -20,9 +20,11 @@ pub mod GovernorComponent { use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, SyscallResultTrait}; + type ProposalId = felt252; + #[storage] pub struct Storage { - pub Governor_proposals: Map, + pub Governor_proposals: Map, } #[event] @@ -557,7 +559,7 @@ pub mod GovernorComponent { self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS()) } - /// Cast a vote with a reason. + /// Cast a vote with a `reason`. /// /// Requirements: /// @@ -574,7 +576,7 @@ pub mod GovernorComponent { self._cast_vote(proposal_id, voter, support, reason, Immutable::DEFAULT_PARAMS()) } - /// Cast a vote with a reason and additional serialized parameters. + /// Cast a vote with a `reason` and additional serialized `params`. /// /// Requirements: /// diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index f0f392c43..3f89caef3 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -168,12 +168,12 @@ pub trait IGovernor { /// Cast a vote. fn cast_vote(ref self: TState, proposal_id: felt252, support: u8) -> u256; - /// Cast a vote with a reason. + /// Cast a vote with a `reason`. fn cast_vote_with_reason( ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray ) -> u256; - /// Cast a vote with a reason and additional serialized parameters. + /// Cast a vote with a `reason` and additional serialized `params`. fn cast_vote_with_reason_and_params( ref self: TState, proposal_id: felt252, @@ -182,7 +182,7 @@ pub trait IGovernor { params: Span ) -> u256; - /// Cast a vote using the voter's signature. + /// Cast a vote using the `voter`'s signature. fn cast_vote_by_sig( ref self: TState, proposal_id: felt252, @@ -191,7 +191,8 @@ pub trait IGovernor { signature: Span ) -> u256; - /// Cast a vote with a reason and additional serialized parameters using the voter's signature + /// Cast a vote with a `reason` and additional serialized `params` using the `voter`'s + /// signature. fn cast_vote_with_reason_and_params_by_sig( ref self: TState, proposal_id: felt252, diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index ffc385533..f0ea57634 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/proposal_core.cairo) +use core::traits::DivRem; use starknet::ContractAddress; use starknet::storage_access::StorePacking; +/// Proposal state. #[derive(Copy, Drop, Serde, PartialEq, Debug)] pub struct ProposalCore { pub proposer: ContractAddress, @@ -16,26 +18,24 @@ pub struct ProposalCore { const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000; const _2_POW_120: felt252 = 0x1000000000000000000000000000000; -const _2_POW_119: felt252 = 0x800000000000000000000000000000; -const _2_POW_118: felt252 = 0x400000000000000000000000000000; +const _2_POW_56: felt252 = 0x100000000000000; +const _2_POW_55: felt252 = 0x80000000000000; const _2_POW_54: felt252 = 0x40000000000000; - -const _64_BITS_MASK: u256 = 0xffffffffffffffff; -const _1_BIT_MASK: u256 = 0x1; +const _2_POW_64: NonZero = 0xffffffffffffffff; /// Packs a ProposalCore into a (felt252, felt252). /// /// The packing is done as follows: /// /// 1. The first felt of the tuple contains `proposer` serialized. -/// 2. The second felt of the tuple contains `vote_start`, `vote_duration`, `executed`, `canceled` -/// and `eta_seconds` organized as: +/// 2. The second felt of the tuple contains `vote_start`, `vote_duration`, `eta_seconds`, +/// `executed`, and `canceled` organized as: /// - `vote_start` is stored at range [4,67] bits (0-indexed), taking the most significant usable /// bits. /// - `vote_duration` is stored at range [68, 131], following `vote_start`. -/// - `executed` is stored at range [132, 132], following `vote_duration`. -/// - `canceled` is stored at range [133, 133], following `executed`. -/// - `eta_seconds` is stored at range [134, 197], following `canceled`. +/// - `eta_seconds` is stored at range [132, 195], following `vote_duration`. +/// - `executed` is stored at range [133, 133], following `eta_seconds`. +/// - `canceled` is stored at range [134, 134], following `executed`. /// /// NOTE: In the second felt252, the first four bits are skipped to avoid representation errors due /// to `felt252` max value being a bit less than a 252 bits number max value @@ -47,25 +47,28 @@ impl ProposalCoreStorePacking of StorePacking // shift-left to reach the corresponding positions let vote_start = proposal.vote_start.into() * _2_POW_184; let vote_duration = proposal.vote_duration.into() * _2_POW_120; - let executed = proposal.executed.into() * _2_POW_119; - let canceled = proposal.canceled.into() * _2_POW_118; - let eta_seconds = proposal.eta_seconds.into() * _2_POW_54; + let eta_seconds = proposal.eta_seconds.into() * _2_POW_56; + let executed = proposal.executed.into() * _2_POW_55; + let canceled = proposal.canceled.into() * _2_POW_54; - let second_felt = vote_start + vote_duration + executed + canceled + eta_seconds; + let second_felt = vote_start + vote_duration + eta_seconds + executed + canceled; (proposal.proposer.into(), second_felt) } fn unpack(value: (felt252, felt252)) -> ProposalCore { let (proposer, second_felt) = value; - let second_felt: u256 = second_felt.into(); - // shift-right and mask to extract the corresponding values - let vote_start: u256 = (second_felt / _2_POW_184.into()) & _64_BITS_MASK; - let vote_duration: u256 = (second_felt / _2_POW_120.into()) & _64_BITS_MASK; - let executed: u256 = (second_felt / _2_POW_119.into()) & _1_BIT_MASK; - let canceled: u256 = (second_felt / _2_POW_118.into()) & _1_BIT_MASK; - let eta_seconds: u256 = (second_felt / _2_POW_54.into()) & _64_BITS_MASK; + const _2_POW_64: NonZero = 0xffffffffffffffff; + + // shift-right to extract the corresponding values + let val: u256 = second_felt.into() / _2_POW_54.into(); + + let (val, canceled) = DivRem::div_rem(val, 2); + let (val, executed) = DivRem::div_rem(val, 2); + let (val, eta_seconds) = DivRem::div_rem(val, _2_POW_64); + let (val, vote_duration) = DivRem::div_rem(val, _2_POW_64); + let (_, vote_start) = DivRem::div_rem(val, _2_POW_64); ProposalCore { proposer: proposer.try_into().unwrap(), diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index 83b0a8f29..2c17af46c 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -12,11 +12,12 @@ use core::to_byte_array::FormatAsByteArray; /// /// - `start + n` must be less than or equal to the length of the byte array. pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray { - assert(start + n <= data.len(), 'ByteArray: out of bounds'); + let end_index = start + n; + assert(end_index <= data.len(), 'ByteArray: out of bounds'); let mut result: ByteArray = Default::default(); - for i in 0..n { - result.append_byte(data[start + i]); + for i in start..end_index { + result.append_byte(data[i]); }; result } @@ -25,7 +26,7 @@ pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray { /// /// Requirements: /// -/// - `base` can not be zero. +/// - `base` cannot be zero. pub fn to_byte_array, +Copy>( value: @T, base: u8, padding: u16 ) -> ByteArray { @@ -77,7 +78,7 @@ pub impl ByteArrayExtImpl of ByteArrayExtTrait { /// /// Requirements: /// - /// - `base` can not be zero. + /// - `base` cannot be zero. fn to_byte_array, +Copy>( self: @T, base: u8, padding: u16 ) -> ByteArray { From bdcdec439561b46f1b462700d65a72d485874dbf Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 18:52:48 +0100 Subject: [PATCH 71/76] feat: add cast vote by sig --- packages/governance/src/governor.cairo | 1 + .../governance/src/governor/governor.cairo | 85 ++++++++++++++++++- .../governance/src/governor/interface.cairo | 3 + packages/governance/src/governor/vote.cairo | 67 +++++++++++++++ packages/utils/src/bytearray.cairo | 3 +- 5 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 packages/governance/src/governor/vote.cairo diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo index aea36a537..4d2442162 100644 --- a/packages/governance/src/governor.cairo +++ b/packages/governance/src/governor.cairo @@ -2,6 +2,7 @@ pub mod extensions; pub mod governor; pub mod interface; pub mod proposal_core; +pub mod vote; pub use governor::{GovernorComponent, DefaultConfig}; pub use proposal_core::ProposalCore; diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index bfaad1dd8..28fc74a85 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -11,6 +11,7 @@ pub mod GovernorComponent { use core::pedersen::PedersenTrait; use crate::governor::ProposalCore; use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; + use crate::governor::vote::{Vote, VoteWithReasonAndParams}; use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; @@ -25,6 +26,7 @@ pub mod GovernorComponent { #[storage] pub struct Storage { pub Governor_proposals: Map, + pub Governor_nonces: Map } #[event] @@ -110,6 +112,7 @@ pub mod GovernorComponent { pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Insufficient votes'; pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Unexpected proposal state'; pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Queue not implemented'; + pub const INVALID_SIGNATURE: felt252 = 'Invalid signature'; } /// Constants expected to be defined at the contract level used to configure the component @@ -595,7 +598,16 @@ pub mod GovernorComponent { self._cast_vote(proposal_id, voter, support, reason, params) } - /// TODO: implement vote by signature + /// Cast a vote using the `voter`'s signature. + /// + /// Requirements: + /// + /// - The proposal must be active. + /// - The nonce in the signed message must match the account's current nonce. + /// - `voter` must implement `SRC6::is_valid_signature`. + /// - `signature` should be valid for the message hash. + /// + /// Emits a `VoteCast` event. fn cast_vote_by_sig( ref self: ComponentState, proposal_id: felt252, @@ -603,10 +615,39 @@ pub mod GovernorComponent { voter: ContractAddress, signature: Span ) -> u256 { - 1 + // 1. Get and increase current nonce + let nonce = self.use_nonce(voter); + + // 2. Build hash for calling `is_valid_signature` + let verifying_contract = starknet::get_contract_address(); + let vote = Vote { verifying_contract, nonce, proposal_id, support, voter }; + let hash = vote.get_message_hash(voter); + + let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter } + .is_valid_signature(hash, signature.into()); + + // 3. 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); + + // 4. Cast vote + self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS()) } - /// TODO: implement vote by signature + /// Cast a vote with a `reason` and additional serialized `params` using the `voter`'s + /// signature. + /// + /// Requirements: + /// + /// - The proposal must be active. + /// - The nonce in the signed message must match the account's current nonce. + /// - `voter` must implement `SRC6::is_valid_signature`. + /// - `signature` should be valid for the message hash. + /// + /// Emits a `VoteCast` event if no params are provided. + /// Emits a `VoteCastWithParams` event otherwise. fn cast_vote_with_reason_and_params_by_sig( ref self: ComponentState, proposal_id: felt252, @@ -616,7 +657,33 @@ pub mod GovernorComponent { params: Span, signature: Span ) -> u256 { - 1 + // 1. Get and increase current nonce + let nonce = self.use_nonce(voter); + + // 2. Build hash for calling `is_valid_signature` + let verifying_contract = starknet::get_contract_address(); + let reason_hash = reason.hash(); + let vote = VoteWithReasonAndParams { + verifying_contract, nonce, proposal_id, support, voter, reason_hash, params + }; + let hash = vote.get_message_hash(voter); + + let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter } + .is_valid_signature(hash, signature.into()); + + // 3. 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); + + // 4. Cast vote + self._cast_vote(proposal_id, voter, support, reason, params) + } + + /// Returns the next unused nonce for an address. + fn nonces(self: @ComponentState, voter: ContractAddress) -> felt252 { + self.Governor_nonces.read(voter) } /// Relays a transaction or function call to an arbitrary target. @@ -781,6 +848,16 @@ pub mod GovernorComponent { assert(found, Errors::UNEXPECTED_PROPOSAL_STATE); } + /// Consumes a nonce, returns the current value, and increments nonce. + fn use_nonce(ref self: ComponentState, voter: ContractAddress) -> felt252 { + // For each account, the nonce has an initial value of 0, can only be incremented by + // one, and cannot be decremented or reset. This guarantees that the nonce never + // overflows. + let nonce = self.Governor_nonces.read(voter); + self.Governor_nonces.write(voter, nonce + 1); + nonce + } + /// Internal wrapper for `GovernorVotesTrait::get_votes`. fn _get_votes( self: @ComponentState, diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo index 3f89caef3..317309876 100644 --- a/packages/governance/src/governor/interface.cairo +++ b/packages/governance/src/governor/interface.cairo @@ -203,6 +203,9 @@ pub trait IGovernor { signature: Span ) -> u256; + /// Returns the next unused nonce for an address. + fn nonces(self: @TState, voter: ContractAddress) -> felt252; + /// Relays a transaction or function call to an arbitrary target. /// /// In cases where the governance executor is some contract other than the governor itself, like diff --git a/packages/governance/src/governor/vote.cairo b/packages/governance/src/governor/vote.cairo new file mode 100644 index 000000000..9ccf320d1 --- /dev/null +++ b/packages/governance/src/governor/vote.cairo @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/vote.cairo) + +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; +use openzeppelin_utils::cryptography::snip12::StructHash; +use starknet::ContractAddress; + +// sn_keccak( +// "\"Vote\"(\"verifying_contract\":\"ContractAddress\", +// \"nonce\":\"felt\", +// \"proposal_id\":\"felt\", +// \"support\":\"u8\", +// \"voter\":\"ContractAddress\")" +// ) +// +// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation. +pub const VOTE_TYPE_HASH: felt252 = + 0x21d38a715b9e9f6da132e4d01c8e4bd956340b0407942182043d516d8e27f3f; + +#[derive(Copy, Drop, Hash)] +pub struct Vote { + pub verifying_contract: ContractAddress, + pub nonce: felt252, + pub proposal_id: felt252, + pub support: u8, + pub voter: ContractAddress, +} + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Vote) -> felt252 { + let hash_state = PoseidonTrait::new(); + hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + } +} + +// sn_keccak( +// "\"Vote\"(\"verifying_contract\":\"ContractAddress\", +// \"nonce\":\"felt\", +// \"proposal_id\":\"felt\", +// \"support\":\"u8\", +// \"voter\":\"ContractAddress\", +// \"reason_hash\":\"felt\", +// \"params\":\"felt*\")" +// ) +// +// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation. +pub const VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH: felt252 = + 0x3866b6236bd1166c5b7eeda1b8e6d1d8f3cd5b82bccd3dac6f8d476d4848dd4; + +#[derive(Copy, Drop, Hash)] +pub struct VoteWithReasonAndParams { + pub verifying_contract: ContractAddress, + pub nonce: felt252, + pub proposal_id: felt252, + pub support: u8, + pub voter: ContractAddress, + pub reason_hash: felt252, + pub params: Span, +} + +impl StructHashImpl of StructHash { + fn hash_struct(self: @Vote) -> felt252 { + let hash_state = PoseidonTrait::new(); + hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + } +} diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo index 9f9278c56..e00dd41f3 100644 --- a/packages/utils/src/bytearray.cairo +++ b/packages/utils/src/bytearray.cairo @@ -32,7 +32,8 @@ pub fn to_byte_array, +Copy>( ) -> ByteArray { let value: felt252 = (*value).into(); let base: felt252 = base.into(); - let mut byte_array = value.format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0')); + let mut byte_array = value + .format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0')); if padding.into() > byte_array.len() { let mut padding = padding.into() - byte_array.len(); From ffaac53951e241bb178c73d706143c720ada9512 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 21:17:19 +0100 Subject: [PATCH 72/76] feat: add more tests --- CHANGELOG.md | 9 ++++ .../governance/src/governor/governor.cairo | 6 ++- .../src/governor/proposal_core.cairo | 41 +++++++++++++++++-- packages/governance/src/governor/vote.cairo | 11 ++--- .../governance/src/multisig/multisig.cairo | 3 +- .../src/tests/governor/test_governor.cairo | 3 +- .../src/timelock/timelock_controller.cairo | 3 +- packages/governance/src/utils.cairo | 15 +++++++ .../governance/src/utils/call_impls.cairo | 11 ----- 9 files changed, 78 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 931c71233..1bc1f4309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - SRC9 (Outside Execution) integration to account presets (#1201) +- `HashSpanImpl` to `openzeppelin_governance::utils` (#1180) +- GovernorComponent with the following extensions: (#1180) + - GovernorCoreExecutionComponent + - GovernorCountingSimpleComponent + - GovernorSettingsComponent + - GovernorTimelockExecutionComponent + - GovernorVotesQuorumFractionComponent + - GovernorVotesComponent ### Changed (Breaking) +- Remove `HashCallsImpl` from `openzeppelin_governance::utils::call_impls` (#1180) - VestingComponent `release` function won't emit an event or attempt to transfer when the amount is zero (#1209) - Bump snforge_std to v0.33.0 (#1203) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 28fc74a85..24e7f452e 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -12,11 +12,13 @@ pub mod GovernorComponent { use crate::governor::ProposalCore; use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; use crate::governor::vote::{Vote, VoteWithReasonAndParams}; - use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; + use crate::utils::HashSpanImpl; + use crate::utils::call_impls::HashCallImpl; + use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_utils::bytearray::ByteArrayExtTrait; - use openzeppelin_utils::cryptography::snip12::SNIP12Metadata; + use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata}; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, SyscallResultTrait}; diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo index f0ea57634..748ce0e91 100644 --- a/packages/governance/src/governor/proposal_core.cairo +++ b/packages/governance/src/governor/proposal_core.cairo @@ -21,7 +21,6 @@ const _2_POW_120: felt252 = 0x1000000000000000000000000000000; const _2_POW_56: felt252 = 0x100000000000000; const _2_POW_55: felt252 = 0x80000000000000; const _2_POW_54: felt252 = 0x40000000000000; -const _2_POW_64: NonZero = 0xffffffffffffffff; /// Packs a ProposalCore into a (felt252, felt252). /// @@ -58,8 +57,7 @@ impl ProposalCoreStorePacking of StorePacking fn unpack(value: (felt252, felt252)) -> ProposalCore { let (proposer, second_felt) = value; - - const _2_POW_64: NonZero = 0xffffffffffffffff; + let _2_POW_64: NonZero = 0x10000000000000000; // shift-right to extract the corresponding values let val: u256 = second_felt.into() / _2_POW_54.into(); @@ -80,3 +78,40 @@ impl ProposalCoreStorePacking of StorePacking } } } + +#[cfg(test)] +mod tests { + use core::num::traits::Bounded; + use openzeppelin_testing::constants::ALICE; + use super::{ProposalCore, ProposalCoreStorePacking}; + + #[test] + fn test_pack_and_unpack() { + let proposal = ProposalCore { + proposer: ALICE(), + vote_start: 100, + vote_duration: 200, + executed: false, + canceled: true, + eta_seconds: 300 + }; + let packed = ProposalCoreStorePacking::pack(proposal); + let unpacked = ProposalCoreStorePacking::unpack(packed); + assert_eq!(proposal, unpacked); + } + + #[test] + fn test_pack_and_unpack_big_values() { + let proposal = ProposalCore { + proposer: ALICE(), + vote_start: Bounded::MAX, + vote_duration: Bounded::MAX, + executed: true, + canceled: true, + eta_seconds: Bounded::MAX + }; + let packed = ProposalCoreStorePacking::pack(proposal); + let unpacked = ProposalCoreStorePacking::unpack(packed); + assert_eq!(proposal, unpacked); + } +} diff --git a/packages/governance/src/governor/vote.cairo b/packages/governance/src/governor/vote.cairo index 9ccf320d1..7b93296b3 100644 --- a/packages/governance/src/governor/vote.cairo +++ b/packages/governance/src/governor/vote.cairo @@ -3,6 +3,7 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; +use crate::utils::HashSpanImpl; use openzeppelin_utils::cryptography::snip12::StructHash; use starknet::ContractAddress; @@ -27,10 +28,10 @@ pub struct Vote { pub voter: ContractAddress, } -impl StructHashImpl of StructHash { +impl VoteStructHashImpl of StructHash { fn hash_struct(self: @Vote) -> felt252 { let hash_state = PoseidonTrait::new(); - hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + hash_state.update_with(VOTE_TYPE_HASH).update_with(*self).finalize() } } @@ -59,9 +60,9 @@ pub struct VoteWithReasonAndParams { pub params: Span, } -impl StructHashImpl of StructHash { - fn hash_struct(self: @Vote) -> felt252 { +impl VoteWithReasonAndParamsStructHashImpl of StructHash { + fn hash_struct(self: @VoteWithReasonAndParams) -> felt252 { let hash_state = PoseidonTrait::new(); - hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize() + hash_state.update_with(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH).update_with(*self).finalize() } } diff --git a/packages/governance/src/multisig/multisig.cairo b/packages/governance/src/multisig/multisig.cairo index 411824987..c088a11f9 100644 --- a/packages/governance/src/multisig/multisig.cairo +++ b/packages/governance/src/multisig/multisig.cairo @@ -22,7 +22,8 @@ pub mod MultisigComponent { use crate::multisig::interface::{IMultisig, TransactionID, TransactionState}; use crate::multisig::storage_utils::{SignersInfo, SignersInfoStorePacking}; use crate::multisig::storage_utils::{TxInfo, TxInfoStorePacking}; - use crate::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; + use crate::utils::HashSpanImpl; + use crate::utils::call_impls::{HashCallImpl, CallPartialEq}; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 262c5339b..2d96e7b86 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -5,7 +5,8 @@ use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl}; use crate::governor::interface::{IGovernor, IGOVERNOR_ID, ProposalState}; use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait}; use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore}; -use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; +use crate::utils::HashSpanImpl; +use crate::utils::call_impls::HashCallImpl; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::governor::GovernorMock; diff --git a/packages/governance/src/timelock/timelock_controller.cairo b/packages/governance/src/timelock/timelock_controller.cairo index 2d839f1ae..6641caa71 100644 --- a/packages/governance/src/timelock/timelock_controller.cairo +++ b/packages/governance/src/timelock/timelock_controller.cairo @@ -18,7 +18,8 @@ pub mod TimelockControllerComponent { use core::num::traits::Zero; use core::pedersen::PedersenTrait; use crate::timelock::interface::{ITimelock, TimelockABI}; - use crate::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; + use crate::utils::HashSpanImpl; + use crate::utils::call_impls::{HashCallImpl, CallPartialEq}; use openzeppelin_access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin_access::accesscontrol::AccessControlComponent::{ AccessControlImpl, AccessControlCamelImpl diff --git a/packages/governance/src/utils.cairo b/packages/governance/src/utils.cairo index 0b626634e..52dbc2dab 100644 --- a/packages/governance/src/utils.cairo +++ b/packages/governance/src/utils.cairo @@ -1 +1,16 @@ pub mod call_impls; +use core::hash::{HashStateTrait, HashStateExTrait, Hash}; + +/// Hash trait implementation for a span of elements. +pub impl HashSpanImpl< + S, T, +HashStateTrait, +Drop, +Copy, +Hash +> of Hash, S> { + fn update_state(mut state: S, value: Span) -> S { + state = state.update_with(value.len()); + for elem in value { + state = state.update_with(*elem); + }; + + state + } +} diff --git a/packages/governance/src/utils/call_impls.cairo b/packages/governance/src/utils/call_impls.cairo index 36ba4df14..1f48a2120 100644 --- a/packages/governance/src/utils/call_impls.cairo +++ b/packages/governance/src/utils/call_impls.cairo @@ -16,17 +16,6 @@ pub impl HashCallImpl, +Drop> of Hash { } } -pub impl HashCallsImpl, +Drop> of Hash, S> { - fn update_state(mut state: S, value: Span) -> S { - state = state.update_with(value.len()); - for call in value { - state = state.update_with(*call); - }; - - state - } -} - pub impl CallPartialEq of PartialEq { #[inline(always)] fn eq(lhs: @Call, rhs: @Call) -> bool { From cd52a53c6a88ea2396586ea86b0e3e56c0a40691 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 21:49:42 +0100 Subject: [PATCH 73/76] feat: add more tests --- packages/governance/src/governor/vote.cairo | 21 ++++ packages/governance/src/tests/governor.cairo | 1 + .../src/tests/governor/test_governor.cairo | 77 ++++++++++++++- .../test_governor_core_execution.cairo | 1 + packages/utils/src/structs.cairo | 1 - .../src/structs/double_ended_queue.cairo | 96 ------------------- 6 files changed, 96 insertions(+), 101 deletions(-) create mode 100644 packages/governance/src/tests/governor/test_governor_core_execution.cairo delete mode 100644 packages/utils/src/structs/double_ended_queue.cairo diff --git a/packages/governance/src/governor/vote.cairo b/packages/governance/src/governor/vote.cairo index 7b93296b3..806d1c09a 100644 --- a/packages/governance/src/governor/vote.cairo +++ b/packages/governance/src/governor/vote.cairo @@ -66,3 +66,24 @@ impl VoteWithReasonAndParamsStructHashImpl of StructHash (IGovernorDispatcher, IMockContractDispatcher) { (governor, target) } +fn setup_account(public_key: felt252) -> ContractAddress { + let mut calldata = array![public_key]; + utils::declare_and_deploy("SnakeAccountMock", calldata) +} + // // External // @@ -498,10 +506,6 @@ fn test_propose_restricted_proposer() { state.propose(calls, description); } -// -// queue -// - // // execute // @@ -1042,6 +1046,71 @@ fn test_cast_vote_with_reason_and_params_executed() { state.cast_vote_with_reason_and_params(id, 0, "", array![].span()); } +// +// cast_vote_by_sig +// + +fn prepare_governor_and_signature() -> ( + IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256 +) { + let mut governor = deploy_governor(); + let calls = get_calls(OTHER(), false); + let description = "proposal description"; + + // Mock the get_past_votes call + let quorum = GovernorMock::QUORUM; + start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum); + + // 1. Propose + let mut current_time = 10; + start_cheat_block_timestamp_global(current_time); + let proposal_id = governor.propose(calls, description.clone()); + + // 2. Fast forward the vote delay + current_time += GovernorMock::VOTING_DELAY; + start_cheat_block_timestamp_global(current_time); + + // 3. Generate a key pair and set up an account + let key_pair = StarkCurveKeyPairImpl::generate(); + let voter = setup_account(key_pair.public_key); + + // 4. Set up signature parameters + let nonce = 0; + let support = 1; + let verifying_contract = governor.contract_address; + + // 5. Create and sign the vote message + let vote = Vote { verifying_contract, nonce, proposal_id, support, voter }; + let msg_hash = vote.get_message_hash(voter); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + (governor, r, s, proposal_id, support, voter, quorum) +} + +#[test] +fn test_cast_vote_by_sig() { + let (governor, r, s, proposal_id, support, voter, quorum) = prepare_governor_and_signature(); + + // Set up event spy and cast vote + let mut spy = spy_events(); + governor.cast_vote_by_sig(proposal_id, support, voter, array![r, s].span()); + + spy + .assert_only_event_vote_cast( + governor.contract_address, voter, proposal_id, support, quorum, @"" + ); +} + + +#[test] +#[should_panic(expected: 'Invalid signature')] +fn test_cast_vote_by_sig_invalid_signature() { + let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(); + + // Cast vote with invalid signature + governor.cast_vote_by_sig(proposal_id, support, voter, array![r + 1, s].span()); +} + // // Internal // diff --git a/packages/governance/src/tests/governor/test_governor_core_execution.cairo b/packages/governance/src/tests/governor/test_governor_core_execution.cairo new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/governance/src/tests/governor/test_governor_core_execution.cairo @@ -0,0 +1 @@ + diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 13abe72cb..27164c620 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -1,5 +1,4 @@ pub mod checkpoint; -pub mod double_ended_queue; pub use checkpoint::{Trace, Checkpoint}; pub use double_ended_queue::{DoubleEndedQueue, DoubleEndedQueueTrait}; diff --git a/packages/utils/src/structs/double_ended_queue.cairo b/packages/utils/src/structs/double_ended_queue.cairo deleted file mode 100644 index 6b0961c12..000000000 --- a/packages/utils/src/structs/double_ended_queue.cairo +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.19.0 (utils/structs/double_ended_queue.cairo) - -use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; -use starknet::storage::{StoragePath, Mutable}; -use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - -/// A sequence with the ability to efficiently push and pop items (i.e. insert and remove) on both -/// ends of the sequence (called front and back). Among other access patterns, it can be used to -/// implement efficient LIFO and FIFO queues. All operations are O(1) constant time. This includes -/// ´clear´, given that the existing queue contents are left in storage. -/// -/// NOTE: This is a phantom type which can only be used in storage, and members are only accessible -/// through the ´DoubleEndedQueueTrait´ implementation. -#[starknet::storage_node] -pub struct DoubleEndedQueue { - pub(crate) _begin: felt252, - pub(crate) _end: felt252, - pub(crate) _data: Map -} - -pub mod DoubleEndedQueueErrors { - pub const FULL_QUEUE: felt252 = 'Queue: full queue'; - pub const EMPTY_QUEUE: felt252 = 'Queue: empty queue'; -} - -#[generate_trait] -pub impl DoubleEndedQueueImpl of DoubleEndedQueueTrait { - /// Inserts an item at the end of the queue. - fn push_back(self: StoragePath>, value: felt252) { - let back_index = self._end.read(); - assert(back_index + 1 != self._begin.read(), DoubleEndedQueueErrors::FULL_QUEUE); - self._data.write(back_index, value); - self._end.write(back_index + 1); - } - - /// Removes the item at the end of the queue and returns it. - fn pop_back(self: StoragePath>) -> felt252 { - let mut back_index = self._end.read(); - assert(back_index != self._begin.read(), DoubleEndedQueueErrors::EMPTY_QUEUE); - back_index -= 1; - let value = self._data.read(back_index); - self._data.write(back_index, 0); - self._end.write(back_index); - value - } - - /// Inserts an item at the beginning of the queue. - fn push_front(self: StoragePath>, value: felt252) { - let front_index = self._begin.read() - 1; - assert(front_index != self._end.read(), DoubleEndedQueueErrors::FULL_QUEUE); - self._data.write(front_index, value); - self._begin.write(front_index); - } - - /// Removes the item at the beginning of the queue and returns it. - fn pop_front(self: StoragePath>) -> felt252 { - let front_index = self._begin.read(); - assert(front_index != self._end.read(), DoubleEndedQueueErrors::EMPTY_QUEUE); - let value = self._data.read(front_index); - self._data.write(front_index, 0); - self._begin.write(front_index + 1); - value - } - - /// Resets the queue back to being empty. - /// - /// NOTE: The current items are left behind in storage. This does not affect the functioning - /// of the queue. - fn clear(self: StoragePath>) { - self._begin.write(0); - self._end.write(0); - } - - /// Returns the item at the beginning of the queue. - fn front(self: StoragePath) -> felt252 { - assert(!self.is_empty(), DoubleEndedQueueErrors::EMPTY_QUEUE); - self._data.read(self._begin.read()) - } - - /// Returns the item at the end of the queue. - fn back(self: StoragePath) -> felt252 { - assert(!self.is_empty(), DoubleEndedQueueErrors::EMPTY_QUEUE); - self._data.read(self._end.read() - 1) - } - - /// Returns the number of items in the queue. - fn len(self: StoragePath) -> felt252 { - self._end.read() - self._begin.read() - } - - /// Returns true if the queue is empty. - fn is_empty(self: StoragePath) -> bool { - self._begin.read() == self._end.read() - } -} From bbe27a95e77be412fc333a0ac70fd42a69f2a90f Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 14 Nov 2024 21:50:04 +0100 Subject: [PATCH 74/76] fix: remove import --- packages/utils/src/structs.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo index 27164c620..a42c00014 100644 --- a/packages/utils/src/structs.cairo +++ b/packages/utils/src/structs.cairo @@ -1,4 +1,3 @@ pub mod checkpoint; pub use checkpoint::{Trace, Checkpoint}; -pub use double_ended_queue::{DoubleEndedQueue, DoubleEndedQueueTrait}; From 4944d6db46e369739b8ebcc63e43ed26167a5f23 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 15 Nov 2024 00:42:04 +0100 Subject: [PATCH 75/76] feat: add yet more tests --- CHANGELOG.md | 1 + .../governance/src/governor/governor.cairo | 3 +- packages/governance/src/governor/vote.cairo | 30 +- .../governance/src/multisig/multisig.cairo | 3 +- .../src/tests/governor/test_governor.cairo | 273 +++++++++++++++++- .../src/timelock/timelock_controller.cairo | 3 +- packages/governance/src/utils.cairo | 15 - .../governance/src/utils/call_impls.cairo | 10 + packages/test_common/src/mocks/governor.cairo | 4 +- packages/utils/src/cryptography/snip12.cairo | 15 +- 10 files changed, 309 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc1f4309..352ce32dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) - Remove `HashCallsImpl` from `openzeppelin_governance::utils::call_impls` (#1180) +- `HashCallImpl` now hashes the len after the span elements (#1180) - VestingComponent `release` function won't emit an event or attempt to transfer when the amount is zero (#1209) - Bump snforge_std to v0.33.0 (#1203) diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo index 24e7f452e..bbe90739c 100644 --- a/packages/governance/src/governor/governor.cairo +++ b/packages/governance/src/governor/governor.cairo @@ -12,8 +12,7 @@ pub mod GovernorComponent { use crate::governor::ProposalCore; use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID}; use crate::governor::vote::{Vote, VoteWithReasonAndParams}; - use crate::utils::HashSpanImpl; - use crate::utils::call_impls::HashCallImpl; + use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait}; use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl; use openzeppelin_introspection::src5::SRC5Component; diff --git a/packages/governance/src/governor/vote.cairo b/packages/governance/src/governor/vote.cairo index 806d1c09a..75e98b4b4 100644 --- a/packages/governance/src/governor/vote.cairo +++ b/packages/governance/src/governor/vote.cairo @@ -3,21 +3,20 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::poseidon::PoseidonTrait; -use crate::utils::HashSpanImpl; -use openzeppelin_utils::cryptography::snip12::StructHash; +use openzeppelin_utils::cryptography::snip12::{StructHash, SNIP12HashSpanImpl}; use starknet::ContractAddress; // sn_keccak( // "\"Vote\"(\"verifying_contract\":\"ContractAddress\", // \"nonce\":\"felt\", // \"proposal_id\":\"felt\", -// \"support\":\"u8\", +// \"support\":\"u128\", // \"voter\":\"ContractAddress\")" // ) // // Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation. pub const VOTE_TYPE_HASH: felt252 = - 0x21d38a715b9e9f6da132e4d01c8e4bd956340b0407942182043d516d8e27f3f; + 0x19a625949c5c367200d9ca91e845fe9c5f3c7f04735d97d91f3a6cb4cb30b81; #[derive(Copy, Drop, Hash)] pub struct Vote { @@ -36,10 +35,10 @@ impl VoteStructHashImpl of StructHash { } // sn_keccak( -// "\"Vote\"(\"verifying_contract\":\"ContractAddress\", +// "\"VoteWithReasonAndParams\"(\"verifying_contract\":\"ContractAddress\", // \"nonce\":\"felt\", // \"proposal_id\":\"felt\", -// \"support\":\"u8\", +// \"support\":\"u128\", // \"voter\":\"ContractAddress\", // \"reason_hash\":\"felt\", // \"params\":\"felt*\")" @@ -47,9 +46,9 @@ impl VoteStructHashImpl of StructHash { // // Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation. pub const VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH: felt252 = - 0x3866b6236bd1166c5b7eeda1b8e6d1d8f3cd5b82bccd3dac6f8d476d4848dd4; + 0x1f4ccab7220d6a3c0c1cbc1008bfcb3f6fbdb361dd14fd017fabd229e0cf94b; -#[derive(Copy, Drop, Hash)] +#[derive(Copy, Drop)] pub struct VoteWithReasonAndParams { pub verifying_contract: ContractAddress, pub nonce: felt252, @@ -63,7 +62,16 @@ pub struct VoteWithReasonAndParams { impl VoteWithReasonAndParamsStructHashImpl of StructHash { fn hash_struct(self: @VoteWithReasonAndParams) -> felt252 { let hash_state = PoseidonTrait::new(); - hash_state.update_with(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH).update_with(*self).finalize() + hash_state + .update_with(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH) + .update_with(*self.verifying_contract) + .update_with(*self.nonce) + .update_with(*self.proposal_id) + .update_with(*self.support) + .update_with(*self.voter) + .update_with(*self.reason_hash) + .update_with(*self.params) + .finalize() } } @@ -74,7 +82,7 @@ mod tests { #[test] fn test_vote_type_hash() { let expected = selector!( - "\"Vote\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u8\",\"voter\":\"ContractAddress\")" + "\"Vote\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u128\",\"voter\":\"ContractAddress\")" ); assert_eq!(VOTE_TYPE_HASH, expected); } @@ -82,7 +90,7 @@ mod tests { #[test] fn test_vote_with_reason_and_params_type_hash() { let expected = selector!( - "\"Vote\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u8\",\"voter\":\"ContractAddress\",\"reason_hash\":\"felt\",\"params\":\"felt*\")" + "\"VoteWithReasonAndParams\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u128\",\"voter\":\"ContractAddress\",\"reason_hash\":\"felt\",\"params\":\"felt*\")" ); assert_eq!(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH, expected); } diff --git a/packages/governance/src/multisig/multisig.cairo b/packages/governance/src/multisig/multisig.cairo index c088a11f9..411824987 100644 --- a/packages/governance/src/multisig/multisig.cairo +++ b/packages/governance/src/multisig/multisig.cairo @@ -22,8 +22,7 @@ pub mod MultisigComponent { use crate::multisig::interface::{IMultisig, TransactionID, TransactionState}; use crate::multisig::storage_utils::{SignersInfo, SignersInfoStorePacking}; use crate::multisig::storage_utils::{TxInfo, TxInfoStorePacking}; - use crate::utils::HashSpanImpl; - use crate::utils::call_impls::{HashCallImpl, CallPartialEq}; + use crate::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; use starknet::account::Call; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 106168654..bbd2bb633 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -6,8 +6,7 @@ use crate::governor::interface::{IGovernor, IGOVERNOR_ID, ProposalState}; use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait}; use crate::governor::vote::{Vote, VoteWithReasonAndParams}; use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore}; -use crate::utils::HashSpanImpl; -use crate::utils::call_impls::HashCallImpl; +use crate::utils::call_impls::{HashCallImpl, HashCallsImpl}; use openzeppelin_introspection::src5::SRC5Component::SRC5Impl; use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; use openzeppelin_test_common::mocks::governor::GovernorMock; @@ -21,8 +20,11 @@ use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use snforge_std::EventSpy; use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl}; +use snforge_std::{ + start_cheat_caller_address, start_cheat_block_timestamp_global, start_cheat_chain_id_global, + start_mock_call +}; use snforge_std::{spy_events, test_address}; -use snforge_std::{start_cheat_caller_address, start_cheat_block_timestamp_global, start_mock_call}; use starknet::account::Call; use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, contract_address_const}; @@ -83,14 +85,14 @@ fn setup_account(public_key: felt252) -> ContractAddress { fn test_name() { let state = @COMPONENT_STATE(); let name = state.name(); - assert_eq!(name, 'APP_NAME'); + assert_eq!(name, 'DAPP_NAME'); } #[test] fn test_version() { let state = COMPONENT_STATE(); let version = state.version(); - assert_eq!(version, 'APP_VERSION'); + assert_eq!(version, 'DAPP_VERSION'); } #[test] @@ -1050,9 +1052,9 @@ fn test_cast_vote_with_reason_and_params_executed() { // cast_vote_by_sig // -fn prepare_governor_and_signature() -> ( - IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256 -) { +fn prepare_governor_and_signature( + nonce: felt252 +) -> (IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256) { let mut governor = deploy_governor(); let calls = get_calls(OTHER(), false); let description = "proposal description"; @@ -1075,7 +1077,6 @@ fn prepare_governor_and_signature() -> ( let voter = setup_account(key_pair.public_key); // 4. Set up signature parameters - let nonce = 0; let support = 1; let verifying_contract = governor.contract_address; @@ -1089,7 +1090,7 @@ fn prepare_governor_and_signature() -> ( #[test] fn test_cast_vote_by_sig() { - let (governor, r, s, proposal_id, support, voter, quorum) = prepare_governor_and_signature(); + let (governor, r, s, proposal_id, support, voter, quorum) = prepare_governor_and_signature(0); // Set up event spy and cast vote let mut spy = spy_events(); @@ -1101,16 +1102,264 @@ fn test_cast_vote_by_sig() { ); } - #[test] #[should_panic(expected: 'Invalid signature')] fn test_cast_vote_by_sig_invalid_signature() { - let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(); + let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(0); // Cast vote with invalid signature governor.cast_vote_by_sig(proposal_id, support, voter, array![r + 1, s].span()); } +#[test] +#[should_panic(expected: 'Invalid signature')] +fn test_cast_vote_by_sig_invalid_msg_hash() { + // Use invalid nonce (not the account's current nonce) + let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(1); + + // Cast vote with invalid msg hash + governor.cast_vote_by_sig(proposal_id, support, voter, array![r, s].span()); +} + +#[test] +fn test_cast_vote_by_sig_hash_generation() { + start_cheat_chain_id_global('SN_TEST'); + + let verifying_contract = contract_address_const::<'VERIFIER'>(); + let nonce = 0; + let proposal_id = 1; + let support = 1; + let voter = contract_address_const::<'VOTER'>(); + + let vote = Vote { verifying_contract, nonce, proposal_id, support, voter }; + let hash = vote.get_message_hash(voter); + + // This hash was computed using starknet js sdk from the following values: + // - name: 'DAPP_NAME' + // - version: 'DAPP_VERSION' + // - chainId: 'SN_TEST' + // - account: 'VOTER' + // - nonce: 0 + // - verifying_contract: 'VERIFIER' + // - proposal_id: 1 + // - support: 1 + // - voter: 'VOTER' + // - revision: '1' + let expected_hash = 0x6541a00fa95d4796bded177fca3cee29d9697174edadebfa4b0b9784379f636; + assert_eq!(hash, expected_hash); +} + +// +// cast_vote_with_reason_and_params_by_sig +// + +fn prepare_governor_and_signature_with_reason_and_params( + reason: @ByteArray, params: Span, nonce: felt252 +) -> (IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256) { + let mut governor = deploy_governor(); + let calls = get_calls(OTHER(), false); + let description = "proposal description"; + + // Mock the get_past_votes call + let quorum = GovernorMock::QUORUM; + start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum); + + // 1. Propose + let mut current_time = 10; + start_cheat_block_timestamp_global(current_time); + let proposal_id = governor.propose(calls, description.clone()); + + // 2. Fast forward the vote delay + current_time += GovernorMock::VOTING_DELAY; + start_cheat_block_timestamp_global(current_time); + + // 3. Generate a key pair and set up an account + let key_pair = StarkCurveKeyPairImpl::generate(); + let voter = setup_account(key_pair.public_key); + + // 4. Set up signature parameters + let support = 1; + let verifying_contract = governor.contract_address; + let reason_hash = reason.hash(); + + // 5. Create and sign the vote message + let vote = VoteWithReasonAndParams { + verifying_contract, nonce, proposal_id, support, voter, reason_hash, params + }; + let msg_hash = vote.get_message_hash(voter); + let (r, s) = key_pair.sign(msg_hash).unwrap(); + + (governor, r, s, proposal_id, support, voter, quorum) +} + +#[test] +fn test_cast_vote_with_reason_and_params_by_sig() { + let reason = "proposal reason"; + let params = array!['param'].span(); + + let (governor, r, s, proposal_id, support, voter, quorum) = + prepare_governor_and_signature_with_reason_and_params( + @reason, params, 0 + ); + + // Set up event spy and cast vote + let mut spy = spy_events(); + governor + .cast_vote_with_reason_and_params_by_sig( + proposal_id, support, voter, reason.clone(), params, array![r, s].span() + ); + + spy + .assert_only_event_vote_cast_with_params( + governor.contract_address, voter, proposal_id, support, quorum, @reason, params + ); +} + +#[test] +fn test_cast_vote_with_reason_and_params_by_sig_empty_params() { + let reason = "proposal reason"; + let params = array![].span(); + + let (governor, r, s, proposal_id, support, voter, quorum) = + prepare_governor_and_signature_with_reason_and_params( + @reason, params, 0 + ); + + // Set up event spy and cast vote + let mut spy = spy_events(); + governor + .cast_vote_with_reason_and_params_by_sig( + proposal_id, support, voter, reason.clone(), params, array![r, s].span() + ); + + spy + .assert_only_event_vote_cast( + governor.contract_address, voter, proposal_id, support, quorum, @reason + ); +} + +#[test] +#[should_panic(expected: 'Invalid signature')] +fn test_cast_vote_with_reason_and_params_by_sig_invalid_signature() { + let reason = "proposal reason"; + let params = array!['param'].span(); + + // Use invalid nonce (not the account's current nonce) + let (governor, r, s, proposal_id, support, voter, _) = + prepare_governor_and_signature_with_reason_and_params( + @reason, params, 0 + ); + + // Cast vote with invalid signature + governor + .cast_vote_with_reason_and_params_by_sig( + proposal_id, support, voter, reason.clone(), params, array![r + 1, s].span() + ); +} + +#[test] +#[should_panic(expected: 'Invalid signature')] +fn test_cast_vote_with_reason_and_params_by_sig_invalid_msg_hash() { + let reason = "proposal reason"; + let params = array!['param'].span(); + + // Use invalid nonce (not the account's current nonce) + let (governor, r, s, proposal_id, support, voter, _) = + prepare_governor_and_signature_with_reason_and_params( + @reason, params, 1 + ); + + // Cast vote with invalid msg hash + governor + .cast_vote_with_reason_and_params_by_sig( + proposal_id, support, voter, reason.clone(), params, array![r, s].span() + ); +} + +#[test] +fn test_cast_vote_with_reason_and_params_by_sig_hash_generation() { + start_cheat_chain_id_global('SN_TEST'); + + let verifying_contract = contract_address_const::<'VERIFIER'>(); + let nonce = 0; + let proposal_id = 1; + let support = 1; + let voter = contract_address_const::<'VOTER'>(); + let reason_hash = 'hash'; + let params = array!['param'].span(); + let vote = VoteWithReasonAndParams { + verifying_contract, nonce, proposal_id, support, voter, reason_hash, params + }; + let hash = vote.get_message_hash(voter); + + // This hash was computed using starknet js sdk from the following values: + // - name: 'DAPP_NAME' + // - version: 'DAPP_VERSION' + // - chainId: 'SN_TEST' + // - account: 'VOTER' + // - nonce: 0 + // - verifying_contract: 'VERIFIER' + // - proposal_id: 1 + // - support: 1 + // - voter: 'VOTER' + // - reason_hash: 'hash' + // - params: ['param'] + // - revision: '1' + let expected_hash = 0x729b7bd36fcddae615f7e2d7c78270e7f820f0dec9faf7842e0187670d3e84a; + assert_eq!(hash, expected_hash); +} + +// +// nonces +// + +#[test] +fn test_nonces() { + let mut state = COMPONENT_STATE(); + let nonce = state.nonces(OTHER()); + assert_eq!(nonce, 0); + + state.Governor_nonces.write(OTHER(), 1); + let nonce = state.nonces(OTHER()); + assert_eq!(nonce, 1); +} + +// +// relay +// + +#[test] +fn test_relay() { + let (mut governor, target) = setup_dispatchers(); + let new_number = 1; + let contract_address = governor.contract_address; + + let call = Call { + to: target.contract_address, + selector: selector!("set_number"), + calldata: array![new_number].span() + }; + + let number = target.get_number(); + assert_eq!(number, 0); + + start_cheat_caller_address(contract_address, contract_address); + governor.relay(call); + + let number = target.get_number(); + assert_eq!(number, new_number); +} + +#[test] +#[should_panic(expected: 'Executor only')] +fn test_relay_invalid_caller() { + let mut state = COMPONENT_STATE(); + let call = Call { to: ADMIN(), selector: selector!("foo"), calldata: array![].span() }; + + start_cheat_caller_address(test_address(), OTHER()); + state.relay(call); +} + // // Internal // diff --git a/packages/governance/src/timelock/timelock_controller.cairo b/packages/governance/src/timelock/timelock_controller.cairo index 6641caa71..2d839f1ae 100644 --- a/packages/governance/src/timelock/timelock_controller.cairo +++ b/packages/governance/src/timelock/timelock_controller.cairo @@ -18,8 +18,7 @@ pub mod TimelockControllerComponent { use core::num::traits::Zero; use core::pedersen::PedersenTrait; use crate::timelock::interface::{ITimelock, TimelockABI}; - use crate::utils::HashSpanImpl; - use crate::utils::call_impls::{HashCallImpl, CallPartialEq}; + use crate::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; use openzeppelin_access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin_access::accesscontrol::AccessControlComponent::{ AccessControlImpl, AccessControlCamelImpl diff --git a/packages/governance/src/utils.cairo b/packages/governance/src/utils.cairo index 52dbc2dab..0b626634e 100644 --- a/packages/governance/src/utils.cairo +++ b/packages/governance/src/utils.cairo @@ -1,16 +1 @@ pub mod call_impls; -use core::hash::{HashStateTrait, HashStateExTrait, Hash}; - -/// Hash trait implementation for a span of elements. -pub impl HashSpanImpl< - S, T, +HashStateTrait, +Drop, +Copy, +Hash -> of Hash, S> { - fn update_state(mut state: S, value: Span) -> S { - state = state.update_with(value.len()); - for elem in value { - state = state.update_with(*elem); - }; - - state - } -} diff --git a/packages/governance/src/utils/call_impls.cairo b/packages/governance/src/utils/call_impls.cairo index 1f48a2120..4079ffe06 100644 --- a/packages/governance/src/utils/call_impls.cairo +++ b/packages/governance/src/utils/call_impls.cairo @@ -16,6 +16,16 @@ pub impl HashCallImpl, +Drop> of Hash { } } +pub impl HashCallsImpl, +Drop> of Hash, S> { + fn update_state(mut state: S, value: Span) -> S { + state = state.update_with(value.len()); + for elem in value { + state = state.update_with(*elem); + }; + state + } +} + pub impl CallPartialEq of PartialEq { #[inline(always)] fn eq(lhs: @Call, rhs: @Call) -> bool { diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo index 127fa9c3e..6c02eea38 100644 --- a/packages/test_common/src/mocks/governor.cairo +++ b/packages/test_common/src/mocks/governor.cairo @@ -87,11 +87,11 @@ pub mod GovernorMock { pub impl SNIP12MetadataImpl of SNIP12Metadata { fn name() -> felt252 { - 'APP_NAME' + 'DAPP_NAME' } fn version() -> felt252 { - 'APP_VERSION' + 'DAPP_VERSION' } } diff --git a/packages/utils/src/cryptography/snip12.cairo b/packages/utils/src/cryptography/snip12.cairo index 33a267250..52d4633d3 100644 --- a/packages/utils/src/cryptography/snip12.cairo +++ b/packages/utils/src/cryptography/snip12.cairo @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.19.0 (utils/cryptography/snip12.cairo) -use core::hash::{HashStateTrait, HashStateExTrait}; -use core::poseidon::PoseidonTrait; +use core::hash::{HashStateTrait, HashStateExTrait, Hash}; +use core::poseidon::{PoseidonTrait, HashState}; use starknet::{ContractAddress, get_tx_info}; // selector!( @@ -65,3 +65,14 @@ pub(crate) impl OffchainMessageHashImpl< state.finalize() } } + +/// Hash trait implementation for a span of elements according to SNIP-12. +pub impl SNIP12HashSpanImpl, +Hash> of Hash, HashState> { + fn update_state(mut state: HashState, value: Span) -> HashState { + let mut inner_state = PoseidonTrait::new(); + for elem in value { + inner_state = inner_state.update_with(*elem); + }; + state.update_with(inner_state.finalize()) + } +} From 92cd728d538e88158f275423baa14a2264e20b8c Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 15 Nov 2024 01:03:10 +0100 Subject: [PATCH 76/76] feat: update CHANGELOG --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 352ce32dc..99f583c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - SRC9 (Outside Execution) integration to account presets (#1201) -- `HashSpanImpl` to `openzeppelin_governance::utils` (#1180) +- `SNIP12HashSpanImpl` to `openzeppelin_utils::cryptography::snip12` (#1180) - GovernorComponent with the following extensions: (#1180) - GovernorCoreExecutionComponent - GovernorCountingSimpleComponent @@ -23,8 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (Breaking) -- Remove `HashCallsImpl` from `openzeppelin_governance::utils::call_impls` (#1180) -- `HashCallImpl` now hashes the len after the span elements (#1180) - VestingComponent `release` function won't emit an event or attempt to transfer when the amount is zero (#1209) - Bump snforge_std to v0.33.0 (#1203)