diff --git a/src/governor.cairo b/src/governor.cairo index 8c68f22..915887c 100644 --- a/src/governor.cairo +++ b/src/governor.cairo @@ -43,10 +43,15 @@ pub trait IGovernor { // Vote on the given proposal. fn vote(ref self: TContractState, id: felt252, yea: bool); - // Cancel the given proposal. The proposer may cancel the proposal at any time during before or during the voting period. - // Cancellation can happen by any address if the average voting weight is below the proposal_creation_threshold. + // Cancel the proposal with the given ID. Same as #cancel_at_timestamp, but uses the current timestamp for computing the voting weight. fn cancel(ref self: TContractState, id: felt252); + // Cancel the proposal with the given ID. The proposal may be canceled at any time before it is executed. + // There are two ways the proposal cancellation can be authorized: + // - The proposer can cancel the proposal + // - Anyone can cancel if the average voting weight of the proposer was below the proposal_creation_threshold during the voting period (at the given breach_timestamp) + fn cancel_at_timestamp(ref self: TContractState, id: felt252, breach_timestamp: u64); + // Execute the given proposal. fn execute(ref self: TContractState, call: Call) -> Span; @@ -100,6 +105,7 @@ pub mod Governor { #[derive(starknet::Event, Drop)] pub struct Canceled { pub id: felt252, + pub breach_timestamp: u64, } #[derive(starknet::Event, Drop)] @@ -242,46 +248,55 @@ pub mod Governor { fn cancel(ref self: ContractState, id: felt252) { + self.cancel_at_timestamp(id, get_block_timestamp()) + } + + fn cancel_at_timestamp(ref self: ContractState, id: felt252, breach_timestamp: u64) { let config = self.config.read(); let staker = self.staker.read(); let mut proposal = self.proposals.read(id); assert(proposal.proposer.is_non_zero(), 'DOES_NOT_EXIST'); - if (proposal.proposer != get_caller_address()) { - // if at any point the average voting weight is below the proposal_creation_threshold for the proposer, it can be canceled + assert(proposal.execution_state.canceled.is_zero(), 'ALREADY_CANCELED'); + assert(proposal.execution_state.executed.is_zero(), 'ALREADY_EXECUTED'); + assert(breach_timestamp >= proposal.execution_state.created, 'PROPOSAL_NOT_CREATED'); + + assert( + breach_timestamp < (proposal.execution_state.created + + config.voting_start_delay + + config.voting_period), + 'VOTING_ENDED' + ); + + // iff the proposer is not calling this we need to check the voting weight + if proposal.proposer != get_caller_address() { + // if at the given timestamp (during the voting period), + // the average voting weight is below the proposal_creation_threshold for the proposer, it can be canceled assert( staker - .get_average_delegated_over_last( + .get_average_delegated( delegate: proposal.proposer, - period: config.voting_weight_smoothing_duration + start: breach_timestamp - config.voting_weight_smoothing_duration, + end: breach_timestamp ) < config .proposal_creation_threshold, 'THRESHOLD_NOT_BREACHED' ); } - let timestamp_current = get_block_timestamp(); - - assert( - timestamp_current < (proposal.execution_state.created - + config.voting_start_delay - + config.voting_period), - 'VOTING_ENDED' - ); - - // we know it's not executed since we check voting has not ended proposal .execution_state = ExecutionState { created: proposal.execution_state.created, + // we asserted that it is not already executed executed: 0, - canceled: timestamp_current + canceled: get_block_timestamp() }; self.proposals.write(id, proposal); - self.emit(Canceled { id }); + self.emit(Canceled { id, breach_timestamp }); } fn execute(ref self: ContractState, call: Call) -> Span { diff --git a/src/governor_test.cairo b/src/governor_test.cairo index 8157f6c..2d1d916 100644 --- a/src/governor_test.cairo +++ b/src/governor_test.cairo @@ -424,6 +424,14 @@ fn test_vote_after_voting_period_should_fail() { governor.vote(id, true); // vote should fail } +#[test] +#[should_panic(expected: ('DOES_NOT_EXIST', 'ENTRYPOINT_FAILED'))] +fn test_cancel_fails_if_proposal_not_exists() { + let (_staker, _token, governor, _config) = setup(); + let id = 1234; + governor.cancel(id); +} + #[test] fn test_cancel_by_proposer() { let (staker, token, governor, config) = setup(); @@ -453,6 +461,22 @@ fn test_cancel_by_proposer() { ); } +#[test] +#[should_panic(expected: ('ALREADY_CANCELED', 'ENTRYPOINT_FAILED'))] +fn test_double_cancel_by_proposer() { + let (staker, token, governor, _config) = setup(); + + let proposer = proposer(); + + let id = create_proposal(governor, token, staker); + + advance_time(30); + + set_contract_address(proposer); + governor.cancel(id); + governor.cancel(id); +} + #[test] fn test_cancel_by_non_proposer() { let (staker, token, governor, config) = setup();