From deaf7b68134f50c32d629bdfc58210bb0f923037 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Tue, 19 Nov 2024 21:02:26 +0100 Subject: [PATCH] feat: add more tests --- packages/governance/Scarb.toml | 1 + .../extensions/governor_settings.cairo | 6 +- .../governor_timelock_execution.cairo | 14 +- .../src/governor/extensions/interface.cairo | 3 + packages/governance/src/tests/governor.cairo | 1 + .../src/tests/governor/common.cairo | 6 + .../src/tests/governor/test_governor.cairo | 26 +- .../governor/test_governor_settings.cairo | 10 +- .../test_governor_timelock_execution.cairo | 638 ++++++++++++++++++ packages/testing/src/constants.cairo | 8 + 10 files changed, 688 insertions(+), 25 deletions(-) create mode 100644 packages/governance/src/tests/governor/test_governor_timelock_execution.cairo diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml index 73967928a..d9afb52cf 100644 --- a/packages/governance/Scarb.toml +++ b/packages/governance/Scarb.toml @@ -47,6 +47,7 @@ name = "openzeppelin_governance_unittest" build-external-contracts = [ "openzeppelin_test_common::mocks::account::SnakeAccountMock", "openzeppelin_test_common::mocks::governor::GovernorMock", + "openzeppelin_test_common::mocks::governor::GovernorTimelockedMock", "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_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo index d6041992d..73807c944 100644 --- a/packages/governance/src/governor/extensions/governor_settings.cairo +++ b/packages/governance/src/governor/extensions/governor_settings.cairo @@ -184,9 +184,9 @@ pub mod GovernorSettingsComponent { 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); + 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`. diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo index 59315f149..435733e0a 100644 --- a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo +++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo @@ -66,6 +66,9 @@ pub mod GovernorTimelockExecutionComponent { // Extensions // + /// NOTE: Some of these function can reenter through the external calls to the timelock, but we + /// assume the timelock is trusted and well behaved (according to TimelockController) and this + /// will not happen. pub impl GovernorExecution< TContractState, +GovernorComponent::HasComponent, @@ -174,10 +177,6 @@ pub mod GovernorTimelockExecutionComponent { /// 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, @@ -218,6 +217,13 @@ pub mod GovernorTimelockExecutionComponent { self.Governor_timelock_controller.read() } + /// Returns the timelock proposal id for a given proposal id. + fn get_timelock_id( + self: @ComponentState, proposal_id: felt252 + ) -> TimelockProposalId { + self.Governor_timelock_ids.read(proposal_id) + } + /// Updates the associated timelock. /// /// Requirements: diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo index 21770c048..a8343a81e 100644 --- a/packages/governance/src/governor/extensions/interface.cairo +++ b/packages/governance/src/governor/extensions/interface.cairo @@ -29,6 +29,9 @@ pub trait ITimelocked { /// Returns address of the associated timelock. fn timelock(self: @TState) -> ContractAddress; + /// Returns the timelock proposal id for a given proposal id. + fn get_timelock_id(self: @TState, proposal_id: felt252) -> felt252; + /// Updates the associated timelock. fn update_timelock(ref self: TState, new_timelock: ContractAddress); } diff --git a/packages/governance/src/tests/governor.cairo b/packages/governance/src/tests/governor.cairo index 9e561701a..a3e88a932 100644 --- a/packages/governance/src/tests/governor.cairo +++ b/packages/governance/src/tests/governor.cairo @@ -3,3 +3,4 @@ mod test_governor; mod test_governor_core_execution; mod test_governor_counting_simple; mod test_governor_settings; +mod test_governor_timelock_execution; diff --git a/packages/governance/src/tests/governor/common.cairo b/packages/governance/src/tests/governor/common.cairo index 6624ce2f4..4ebb68ef0 100644 --- a/packages/governance/src/tests/governor/common.cairo +++ b/packages/governance/src/tests/governor/common.cairo @@ -96,6 +96,12 @@ pub fn get_mock_state( } } +pub fn set_executor( + ref mock_state: GovernorTimelockedMock::ContractState, executor: ContractAddress +) { + mock_state.governor_timelock_execution.Governor_timelock_controller.write(executor); +} + // // Setup proposals // diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo index 7d9b51cc0..2cdbfa40a 100644 --- a/packages/governance/src/tests/governor/test_governor.cairo +++ b/packages/governance/src/tests/governor/test_governor.cairo @@ -19,7 +19,7 @@ use openzeppelin_test_common::mocks::timelock::{ IMockContractDispatcher, IMockContractDispatcherTrait }; use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO}; +use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO, VOTES_TOKEN}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_utils::bytearray::ByteArrayExtTrait; use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; @@ -33,14 +33,6 @@ use starknet::account::Call; use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess}; use starknet::{ContractAddress, contract_address_const}; -// -// Constants -// - -fn VOTES_TOKEN() -> ContractAddress { - contract_address_const::<'VOTES_TOKEN'>() -} - // // Dispatchers // @@ -2071,6 +2063,22 @@ pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers { self.assert_no_events_left_from(contract); } + fn assert_event_proposal_queued( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252, eta_seconds: u64 + ) { + let expected = GovernorComponent::Event::ProposalQueued( + GovernorComponent::ProposalQueued { proposal_id, eta_seconds } + ); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_proposal_queued( + ref self: EventSpy, contract: ContractAddress, proposal_id: felt252, eta_seconds: u64 + ) { + self.assert_event_proposal_queued(contract, proposal_id, eta_seconds); + self.assert_no_events_left_from(contract); + } + fn assert_event_proposal_executed( ref self: EventSpy, contract: ContractAddress, proposal_id: felt252 ) { diff --git a/packages/governance/src/tests/governor/test_governor_settings.cairo b/packages/governance/src/tests/governor/test_governor_settings.cairo index 7a03f7ea3..309f8ec13 100644 --- a/packages/governance/src/tests/governor/test_governor_settings.cairo +++ b/packages/governance/src/tests/governor/test_governor_settings.cairo @@ -5,11 +5,11 @@ use crate::governor::extensions::GovernorSettingsComponent::{ GovernorSettings, GovernorSettingsAdminImpl }; use crate::governor::extensions::GovernorSettingsComponent; +use crate::tests::governor::common::set_executor; use crate::tests::governor::common::{ COMPONENT_STATE_TIMELOCKED as COMPONENT_STATE, CONTRACT_STATE_TIMELOCKED as CONTRACT_STATE }; use openzeppelin_test_common::mocks::governor::GovernorTimelockedMock::SNIP12MetadataImpl; -use openzeppelin_test_common::mocks::governor::GovernorTimelockedMock; use openzeppelin_testing::constants::OTHER; use openzeppelin_testing::events::EventSpyExt; use snforge_std::start_cheat_caller_address; @@ -388,14 +388,6 @@ fn test__set_proposal_threshold_no_change() { spy.assert_no_events_left(); } -// -// Helpers -// - -fn set_executor(ref mock_state: GovernorTimelockedMock::ContractState, executor: ContractAddress) { - mock_state.governor_timelock_execution.Governor_timelock_controller.write(executor); -} - // // Event helpers // diff --git a/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo b/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo new file mode 100644 index 000000000..3d9847874 --- /dev/null +++ b/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo @@ -0,0 +1,638 @@ +use crate::governor::DefaultConfig; +use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl}; +use crate::governor::extensions::GovernorTimelockExecutionComponent::GovernorExecution; +use crate::governor::extensions::interface::{ITimelockedDispatcher, ITimelockedDispatcherTrait}; +use crate::governor::interface::ProposalState; +use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait}; +use crate::tests::governor::common::{ + COMPONENT_STATE_TIMELOCKED as COMPONENT_STATE, CONTRACT_STATE_TIMELOCKED as CONTRACT_STATE +}; +use crate::tests::governor::common::{set_executor, get_proposal_info, ComponentStateTimelocked}; +use crate::tests::governor::test_governor::GovernorSpyHelpersImpl; +use crate::tests::test_timelock::TimelockSpyHelpersImpl; +use crate::timelock::interface::{OperationState, ITimelockDispatcher}; +use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl; +use openzeppelin_test_common::mocks::governor::GovernorTimelockedMock; +use openzeppelin_test_common::mocks::timelock::{ + IMockContractDispatcher, IMockContractDispatcherTrait +}; +use openzeppelin_testing as utils; +use openzeppelin_testing::constants::{TIMELOCK, VOTES_TOKEN}; +use openzeppelin_utils::bytearray::ByteArrayExtTrait; +use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::{start_mock_call, start_cheat_block_timestamp_global, spy_events, store}; +use starknet::ContractAddress; +use starknet::account::Call; +use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess}; + +const MIN_DELAY: u64 = 100; + +// +// Dispatchers +// + +fn deploy_governor() -> IGovernorDispatcher { + let mut calldata = array![]; + calldata.append_serde(VOTES_TOKEN()); + calldata.append_serde(1); + + let address = utils::declare_and_deploy("GovernorTimelockedMock", calldata); + IGovernorDispatcher { contract_address: address } +} + +fn deploy_timelock(admin: ContractAddress) -> ITimelockDispatcher { + let proposers = array![admin].span(); + let executors = array![admin].span(); + + let mut calldata = array![]; + calldata.append_serde(MIN_DELAY); + calldata.append_serde(proposers); + calldata.append_serde(executors); + calldata.append_serde(admin); + + let address = utils::declare_and_deploy("TimelockControllerMock", calldata); + ITimelockDispatcher { 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, ITimelockDispatcher, IMockContractDispatcher) { + let governor = deploy_governor(); + let timelock = deploy_timelock(governor.contract_address); + let target = deploy_mock_target(); + + // Set the timelock controller + store( + governor.contract_address, + selector!("Governor_timelock_controller"), + array![timelock.contract_address.into()].span() + ); + + (governor, timelock, target) +} + +// +// state +// + +#[test] +fn test_state_executed() { + let mut component_state = COMPONENT_STATE(); + let id = setup_executed_proposal(ref component_state); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Executed); +} + +#[test] +fn test_state_canceled() { + let mut component_state = COMPONENT_STATE(); + let id = setup_canceled_proposal(ref component_state); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Canceled); +} + +#[test] +#[should_panic(expected: 'Nonexistent proposal')] +fn test_state_non_existent() { + let component_state = COMPONENT_STATE(); + + GovernorExecution::state(@component_state, 1); +} + +#[test] +fn test_state_pending() { + let mut component_state = COMPONENT_STATE(); + let id = setup_pending_proposal(ref component_state); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Pending); +} + +#[test] +fn test_state_active() { + let mut component_state = COMPONENT_STATE(); + let (id, proposal) = get_proposal_info(); + + component_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 state = GovernorExecution::state(@component_state, id); + assert_eq!(state, expected); + + // Is active in deadline + start_cheat_block_timestamp_global(deadline); + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, expected); +} + +#[test] +fn test_state_defeated_quorum_not_reached() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_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 = GovernorTimelockedMock::QUORUM; + let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id); + proposal_votes.for_votes.write(quorum - 1); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, expected); +} + +#[test] +fn test_state_defeated_vote_not_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_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 = GovernorTimelockedMock::QUORUM; + 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 state = GovernorExecution::state(@component_state, id); + assert_eq!(state, expected); +} + +#[test] +fn test_state_queued_timelock_waiting() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let id = setup_queued_proposal(ref mock_state); + + // 1. Mock the timelock to return pending + set_executor(ref mock_state, TIMELOCK()); + start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Waiting); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Queued); +} + +#[test] +fn test_state_queued_timelock_ready() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let id = setup_queued_proposal(ref mock_state); + + // 1. Mock the timelock to return pending + set_executor(ref mock_state, TIMELOCK()); + start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Ready); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Queued); +} + +#[test] +fn test_state_queued_timelock_done() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let id = setup_queued_proposal(ref mock_state); + + // 1. Mock the timelock to return pending + set_executor(ref mock_state, TIMELOCK()); + start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Done); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Executed); +} + +#[test] +fn test_state_queued_timelock_canceled() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let id = setup_queued_proposal(ref mock_state); + + // 1. Mock the timelock to return pending + set_executor(ref mock_state, TIMELOCK()); + start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Unset); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Canceled); +} + +#[test] +fn test_state_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let id = setup_succeeded_proposal(ref mock_state); + + let state = GovernorExecution::state(@component_state, id); + assert_eq!(state, ProposalState::Succeeded); +} + +// +// executor +// + +#[test] +fn test_executor() { + let mut mock_state = CONTRACT_STATE(); + let component_state = COMPONENT_STATE(); + let expected = TIMELOCK(); + + set_executor(ref mock_state, expected); + + assert_eq!(GovernorExecution::executor(@component_state), expected); +} + +// +// execute_operations +// + +#[test] +fn test_execute_operations() { + let (mut governor, timelock, target) = setup_dispatchers(); + let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address }; + + 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); + + // 1. Mock the get_past_votes call + let quorum = GovernorTimelockedMock::QUORUM; + start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum); + + // 2. Propose + let mut current_time = 10; + start_cheat_block_timestamp_global(current_time); + let id = governor.propose(calls, description.clone()); + + // 3. Cast vote + + // Fast forward the vote delay + current_time += GovernorTimelockedMock::VOTING_DELAY; + start_cheat_block_timestamp_global(current_time); + + // Cast vote + governor.cast_vote(id, 1); + + // 4. Queue + + // Fast forward the vote duration + current_time += (GovernorTimelockedMock::VOTING_PERIOD + 1); + start_cheat_block_timestamp_global(current_time); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Succeeded); + + governor.queue(calls, (@description).hash()); + + let target_id = timelocked.get_timelock_id(id); + let salt = timelock_salt(governor.contract_address, (@description).hash()); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Queued); + + // 5. Execute + // Fast forward the timelock delay + current_time += MIN_DELAY; + start_cheat_block_timestamp_global(current_time); + + let mut spy = spy_events(); + governor.execute(calls, (@description).hash()); + + // 6. Assertions + let number = target.get_number(); + assert_eq!(number, new_number); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Executed); + + let target_id = timelocked.get_timelock_id(id); + assert_eq!(target_id, 0); + + spy.assert_only_events_call_executed_batch(timelock.contract_address, target_id, calls); +} + +// +// queue_operations +// + +#[test] +fn test_queue_operations() { + let (mut governor, timelock, target) = setup_dispatchers(); + let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address }; + + 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); + + // 1. Mock the get_past_votes call + let quorum = GovernorTimelockedMock::QUORUM; + start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum); + + // 2. Propose + let mut current_time = 10; + start_cheat_block_timestamp_global(current_time); + let id = governor.propose(calls, description.clone()); + + // 3. Cast vote + + // Fast forward the vote delay + current_time += GovernorTimelockedMock::VOTING_DELAY; + start_cheat_block_timestamp_global(current_time); + + // Cast vote + governor.cast_vote(id, 1); + + // 4. Queue + + // Fast forward the vote duration + current_time += (GovernorTimelockedMock::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.queue(calls, (@description).hash()); + + let target_id = timelocked.get_timelock_id(id); + let salt = timelock_salt(governor.contract_address, (@description).hash()); + spy.assert_event_call_scheduled(timelock.contract_address, target_id, 0, call, 0, MIN_DELAY); + spy.assert_event_call_salt(timelock.contract_address, target_id, salt); + spy.assert_only_event_proposal_queued(governor.contract_address, id, current_time + MIN_DELAY); + + let state = governor.state(id); + assert_eq!(state, ProposalState::Queued); +} + +// +// proposal_needs_queuing +// + +#[test] +fn test_proposal_needs_queuing(id: felt252) { + let component_state = COMPONENT_STATE(); + + assert_eq!(GovernorExecution::proposal_needs_queuing(@component_state, id), true); +} + +// +// cancel_operations +// + +#[test] +fn test_cancel_operations_pending() { + let mut state = COMPONENT_STATE(); + let id = setup_pending_proposal(ref state); + + GovernorExecution::cancel_operations(ref state, id, 0); + + let canceled_proposal = state.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test_cancel_operations_active() { + let mut state = COMPONENT_STATE(); + let id = setup_active_proposal(ref state); + + GovernorExecution::cancel_operations(ref state, id, 0); + + let canceled_proposal = state.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test_cancel_operations_defeated() { + let mut mock_state = CONTRACT_STATE(); + let mut state = COMPONENT_STATE(); + let id = setup_defeated_proposal(ref mock_state); + + GovernorExecution::cancel_operations(ref state, id, 0); + + let canceled_proposal = mock_state.governor.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test_cancel_operations_succeeded() { + let mut mock_state = CONTRACT_STATE(); + let mut state = COMPONENT_STATE(); + let id = setup_succeeded_proposal(ref mock_state); + + GovernorExecution::cancel_operations(ref state, id, 0); + + let canceled_proposal = mock_state.governor.get_proposal(id); + assert_eq!(canceled_proposal.canceled, true); +} + +#[test] +fn test_cancel_operations_queued() { + let mut mock_state = CONTRACT_STATE(); + let mut state = COMPONENT_STATE(); + let id = setup_queued_proposal(ref mock_state); + + GovernorExecution::cancel_operations(ref state, 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_operations_canceled() { + let mut state = COMPONENT_STATE(); + let id = setup_canceled_proposal(ref state); + + // Cancel again + GovernorExecution::cancel_operations(ref state, id, 0); +} + +#[test] +#[should_panic(expected: 'Unexpected proposal state')] +fn test_cancel_operations_executed() { + let mut state = COMPONENT_STATE(); + let id = setup_executed_proposal(ref state); + + GovernorExecution::cancel_operations(ref state, id, 0); +} + +// +// Helpers +// + +fn timelock_salt(contract_address: ContractAddress, description_hash: felt252) -> felt252 { + let description_hash: u256 = description_hash.into(); + let contract_address: felt252 = contract_address.into(); + + (contract_address.into() ^ description_hash).try_into().unwrap() +} + +// +// Setup proposals +// + +pub fn setup_pending_proposal(ref state: ComponentStateTimelocked) -> felt252 { + let (id, proposal) = get_proposal_info(); + + state.Governor_proposals.write(id, proposal); + + let current_state = state._state(id); + let expected = ProposalState::Pending; + + assert_eq!(current_state, expected); + + id +} + +pub fn setup_active_proposal(ref state: ComponentStateTimelocked) -> felt252 { + 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 +} + +pub fn setup_queued_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 { + 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 = GovernorTimelockedMock::QUORUM; + 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 +} + +pub fn setup_canceled_proposal(ref state: ComponentStateTimelocked) -> felt252 { + 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 +} + +pub fn setup_defeated_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 { + 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 = GovernorTimelockedMock::QUORUM; + 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 +} + +pub fn setup_succeeded_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 { + 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 = GovernorTimelockedMock::QUORUM; + 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 +} + +pub fn setup_executed_proposal(ref state: ComponentStateTimelocked) -> felt252 { + let (id, mut proposal) = get_proposal_info(); + + proposal.executed = true; + state.Governor_proposals.write(id, proposal); + + let current_state = state._state(id); + let expected = ProposalState::Executed; + + assert_eq!(current_state, expected); + + id +} diff --git a/packages/testing/src/constants.cairo b/packages/testing/src/constants.cairo index d3cd64fdc..07752c82e 100644 --- a/packages/testing/src/constants.cairo +++ b/packages/testing/src/constants.cairo @@ -102,6 +102,14 @@ pub fn DELEGATEE() -> ContractAddress { contract_address_const::<'DELEGATEE'>() } +pub fn TIMELOCK() -> ContractAddress { + contract_address_const::<'TIMELOCK'>() +} + +pub fn VOTES_TOKEN() -> ContractAddress { + contract_address_const::<'VOTES_TOKEN'>() +} + pub fn ALICE() -> ContractAddress { contract_address_const::<'ALICE'>() }