From 53edc248d58e0b97a4a3f7b30545cfbf34379db8 Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:02:43 +0200 Subject: [PATCH 1/3] feat: simple majority execution --- starknet/src/execution_strategies.cairo | 1 + .../no_execution_simple_majority.cairo | 47 +++++ starknet/src/utils.cairo | 2 + starknet/src/utils/simple_majority.cairo | 179 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 starknet/src/execution_strategies/no_execution_simple_majority.cairo create mode 100644 starknet/src/utils/simple_majority.cairo diff --git a/starknet/src/execution_strategies.cairo b/starknet/src/execution_strategies.cairo index 907558a8..5d7f4968 100644 --- a/starknet/src/execution_strategies.cairo +++ b/starknet/src/execution_strategies.cairo @@ -1,5 +1,6 @@ mod vanilla; +mod no_execution_simple_majority; mod simple_quorum; mod eth_relayer; diff --git a/starknet/src/execution_strategies/no_execution_simple_majority.cairo b/starknet/src/execution_strategies/no_execution_simple_majority.cairo new file mode 100644 index 00000000..449b9ce3 --- /dev/null +++ b/starknet/src/execution_strategies/no_execution_simple_majority.cairo @@ -0,0 +1,47 @@ +/// Execution strategy that will not execute anything but ensure that the +/// proposal is in the status `Accepted` or `VotingPeriodAccepted` by following +/// the `SimpleMajority` rule (`votes_for > votes_against`). +#[starknet::contract] +mod NoExecutionSimpleMajorityExecutionStrategy { + use sx::interfaces::{IExecutionStrategy, IQuorum}; + use sx::types::{Proposal, ProposalStatus}; + use sx::execution_strategies::simple_quorum::SimpleQuorumExecutionStrategy; + use sx::utils::simple_majority; + + #[storage] + struct Storage {} + + #[external(v0)] + impl NoExecutionSimpleMajorityExecutionStrategy of IExecutionStrategy { + fn execute( + ref self: ContractState, + proposal: Proposal, + votes_for: u256, + votes_against: u256, + votes_abstain: u256, + payload: Array + ) { + let proposal_status = self + .get_proposal_status(proposal, votes_for, votes_against, votes_abstain,); + assert( + (proposal_status == ProposalStatus::Accepted(())) + | (proposal_status == ProposalStatus::VotingPeriodAccepted(())), + 'Invalid Proposal Status' + ); + } + + fn get_proposal_status( + self: @ContractState, + proposal: Proposal, + votes_for: u256, + votes_against: u256, + votes_abstain: u256, + ) -> ProposalStatus { + simple_majority::get_proposal_status(@proposal, votes_for, votes_against, votes_abstain) + } + + fn get_strategy_type(self: @ContractState) -> felt252 { + 'NoExecutionSimpleMajority' + } + } +} diff --git a/starknet/src/utils.cairo b/starknet/src/utils.cairo index 057bf08a..96513028 100644 --- a/starknet/src/utils.cairo +++ b/starknet/src/utils.cairo @@ -16,6 +16,8 @@ mod struct_hash; mod single_slot_proof; +mod simple_majority; + mod signatures; mod stark_eip712; diff --git a/starknet/src/utils/simple_majority.cairo b/starknet/src/utils/simple_majority.cairo new file mode 100644 index 00000000..6811d175 --- /dev/null +++ b/starknet/src/utils/simple_majority.cairo @@ -0,0 +1,179 @@ +use starknet::info; +use sx::types::{Proposal, FinalizationStatus, ProposalStatus}; + +/// Returns the status of a proposal, according to a 'Simple Majority' rule. +/// 'Simple Majority' is defined like so: a proposal is accepted if there are more `votes_for` than `votes_against`. +/// So, a proposal will return Accepted if `max_end_timestamp` has been reached, and `votes_for > votes_agasint`. +/// A proposal will return `VotingPeriodAccepted` if `min_end_timestamp` has been reached and `votes_for > votes_against`. +fn get_proposal_status( + proposal: @Proposal, votes_for: u256, votes_against: u256, votes_abstain: u256, +) -> ProposalStatus { + let accepted = votes_for > votes_against; + + let timestamp = info::get_block_timestamp().try_into().unwrap(); + if *proposal.finalization_status == FinalizationStatus::Cancelled(()) { + ProposalStatus::Cancelled(()) + } else if *proposal.finalization_status == FinalizationStatus::Executed(()) { + ProposalStatus::Executed(()) + } else if timestamp < *proposal.start_timestamp { + ProposalStatus::VotingDelay(()) + } else if timestamp < *proposal.min_end_timestamp { + ProposalStatus::VotingPeriod(()) + } else if timestamp < *proposal.max_end_timestamp { + if accepted { + ProposalStatus::VotingPeriodAccepted(()) + } else { + ProposalStatus::VotingPeriod(()) + } + } else if accepted { + ProposalStatus::Accepted(()) + } else { + ProposalStatus::Rejected(()) + } +} + +#[cfg(test)] +mod tests { + use super::{get_proposal_status}; + use sx::types::{Proposal, proposal::ProposalDefault, FinalizationStatus, ProposalStatus}; + + #[test] + #[available_gas(10000000)] + fn cancelled() { + let mut proposal = ProposalDefault::default(); + proposal.finalization_status = FinalizationStatus::Cancelled(()); + let votes_for = 0; + let votes_against = 0; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Cancelled(()), 'failed cancelled'); + } + + #[test] + #[available_gas(10000000)] + fn executed() { + let mut proposal = ProposalDefault::default(); + proposal.finalization_status = FinalizationStatus::Executed(()); + let votes_for = 0; + let votes_against = 0; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Executed(()), 'failed executed'); + } + + #[test] + #[available_gas(10000000)] + fn voting_delay() { + let mut proposal = ProposalDefault::default(); + proposal.start_timestamp = 42424242; + let votes_for = 0; + let votes_against = 0; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingDelay(()), 'failed voting_delay'); + } + + #[test] + #[available_gas(10000000)] + fn voting_period() { + let mut proposal = ProposalDefault::default(); + proposal.min_end_timestamp = 42424242; + let votes_for = 0; + let votes_against = 0; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingPeriod(()), 'failed min_end_timestamp'); + } + + #[test] + #[available_gas(10000000)] + fn shortcut_accepted() { + let mut proposal = ProposalDefault::default(); + proposal.max_end_timestamp = 10; + let votes_for = 1; + let votes_against = 0; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingPeriodAccepted(()), 'failed shortcut_accepted'); + } + + #[test] + #[available_gas(10000000)] + fn shortcut_only_abstains() { + let mut proposal = ProposalDefault::default(); + proposal.max_end_timestamp = 10; + let votes_for = 0; + let votes_against = 0; + let votes_abstain = 1; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_only_abstains'); + } + + #[test] + #[available_gas(10000000)] + fn shortcut_only_againsts() { + let mut proposal = ProposalDefault::default(); + proposal.max_end_timestamp = 10; + let votes_for = 0; + let votes_against = 1; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_only_againsts'); + } + + #[test] + #[available_gas(10000000)] + fn shortcut_balanced() { + let mut proposal = ProposalDefault::default(); + proposal.max_end_timestamp = 10; + let votes_for = 1; + let votes_against = 1; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_balanced'); + } + + #[test] + #[available_gas(10000000)] + fn balanced() { + let mut proposal = ProposalDefault::default(); + let votes_for = 42; + let votes_against = 42; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Rejected(()), 'failed balanced'); + } + + #[test] + #[available_gas(10000000)] + fn accepted() { + let mut proposal = ProposalDefault::default(); + let votes_for = 10; + let votes_against = 9; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Accepted(()), 'failed accepted'); + } + + #[test] + #[available_gas(10000000)] + fn accepted_with_abstains() { + let mut proposal = ProposalDefault::default(); + let votes_for = 2; + let votes_against = 1; + let votes_abstain = 10; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Accepted(()), 'failed accepted abstains'); + } + + #[test] + #[available_gas(10000000)] + fn rejected_only_againsts() { + let mut proposal = ProposalDefault::default(); + let votes_for = 0; + let votes_against = 1; + let votes_abstain = 0; + let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); + assert(result == ProposalStatus::Rejected(()), 'failed rejected'); + } +} From fd62de3d445ab1af8b088a0aed0adf34b510739a Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:25:06 +0200 Subject: [PATCH 2/3] remove useless imports --- .../execution_strategies/no_execution_simple_majority.cairo | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/starknet/src/execution_strategies/no_execution_simple_majority.cairo b/starknet/src/execution_strategies/no_execution_simple_majority.cairo index 449b9ce3..481fb2e1 100644 --- a/starknet/src/execution_strategies/no_execution_simple_majority.cairo +++ b/starknet/src/execution_strategies/no_execution_simple_majority.cairo @@ -3,9 +3,8 @@ /// the `SimpleMajority` rule (`votes_for > votes_against`). #[starknet::contract] mod NoExecutionSimpleMajorityExecutionStrategy { - use sx::interfaces::{IExecutionStrategy, IQuorum}; + use sx::interfaces::{IExecutionStrategy}; use sx::types::{Proposal, ProposalStatus}; - use sx::execution_strategies::simple_quorum::SimpleQuorumExecutionStrategy; use sx::utils::simple_majority; #[storage] From f6ac7e04a8b18dc21688712bcd7acb7a1a1a2dcb Mon Sep 17 00:00:00 2001 From: Scott Piriou <30843220+pscott@users.noreply.github.com> Date: Fri, 8 Sep 2023 11:56:58 +0200 Subject: [PATCH 3/3] remove early end on simple majority --- starknet/src/utils/simple_majority.cairo | 50 ++---------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/starknet/src/utils/simple_majority.cairo b/starknet/src/utils/simple_majority.cairo index 6811d175..d7f6d1dc 100644 --- a/starknet/src/utils/simple_majority.cairo +++ b/starknet/src/utils/simple_majority.cairo @@ -4,7 +4,6 @@ use sx::types::{Proposal, FinalizationStatus, ProposalStatus}; /// Returns the status of a proposal, according to a 'Simple Majority' rule. /// 'Simple Majority' is defined like so: a proposal is accepted if there are more `votes_for` than `votes_against`. /// So, a proposal will return Accepted if `max_end_timestamp` has been reached, and `votes_for > votes_agasint`. -/// A proposal will return `VotingPeriodAccepted` if `min_end_timestamp` has been reached and `votes_for > votes_against`. fn get_proposal_status( proposal: @Proposal, votes_for: u256, votes_against: u256, votes_abstain: u256, ) -> ProposalStatus { @@ -17,14 +16,8 @@ fn get_proposal_status( ProposalStatus::Executed(()) } else if timestamp < *proposal.start_timestamp { ProposalStatus::VotingDelay(()) - } else if timestamp < *proposal.min_end_timestamp { - ProposalStatus::VotingPeriod(()) } else if timestamp < *proposal.max_end_timestamp { - if accepted { - ProposalStatus::VotingPeriodAccepted(()) - } else { - ProposalStatus::VotingPeriod(()) - } + ProposalStatus::VotingPeriod(()) } else if accepted { ProposalStatus::Accepted(()) } else { @@ -78,6 +71,7 @@ mod tests { fn voting_period() { let mut proposal = ProposalDefault::default(); proposal.min_end_timestamp = 42424242; + proposal.max_end_timestamp = proposal.min_end_timestamp + 1; let votes_for = 0; let votes_against = 0; let votes_abstain = 0; @@ -87,50 +81,14 @@ mod tests { #[test] #[available_gas(10000000)] - fn shortcut_accepted() { + fn early_end_does_not_work() { let mut proposal = ProposalDefault::default(); proposal.max_end_timestamp = 10; let votes_for = 1; let votes_against = 0; let votes_abstain = 0; let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); - assert(result == ProposalStatus::VotingPeriodAccepted(()), 'failed shortcut_accepted'); - } - - #[test] - #[available_gas(10000000)] - fn shortcut_only_abstains() { - let mut proposal = ProposalDefault::default(); - proposal.max_end_timestamp = 10; - let votes_for = 0; - let votes_against = 0; - let votes_abstain = 1; - let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); - assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_only_abstains'); - } - - #[test] - #[available_gas(10000000)] - fn shortcut_only_againsts() { - let mut proposal = ProposalDefault::default(); - proposal.max_end_timestamp = 10; - let votes_for = 0; - let votes_against = 1; - let votes_abstain = 0; - let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); - assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_only_againsts'); - } - - #[test] - #[available_gas(10000000)] - fn shortcut_balanced() { - let mut proposal = ProposalDefault::default(); - proposal.max_end_timestamp = 10; - let votes_for = 1; - let votes_against = 1; - let votes_abstain = 0; - let result = get_proposal_status(@proposal, votes_for, votes_against, votes_abstain); - assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_balanced'); + assert(result == ProposalStatus::VotingPeriod(()), 'failed shortcut_accepted'); } #[test]