From 36dda6719d1f9545a0cf718d699cda5543685f86 Mon Sep 17 00:00:00 2001 From: pscott <30843220+pscott@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:59:31 +0200 Subject: [PATCH] add vote tests (#515) Co-authored-by: Orland0x <37511817+Orland0x@users.noreply.github.com> --- starknet/src/space/space.cairo | 46 ++- starknet/src/tests.cairo | 2 + starknet/src/tests/mocks.cairo | 1 + starknet/src/tests/mocks/executor.cairo | 28 +- .../src/tests/mocks/no_voting_power.cairo | 22 + starknet/src/tests/vote.cairo | 378 ++++++++++++++++++ 6 files changed, 456 insertions(+), 21 deletions(-) create mode 100644 starknet/src/tests/mocks/no_voting_power.cairo create mode 100644 starknet/src/tests/vote.cairo diff --git a/starknet/src/space/space.cairo b/starknet/src/space/space.cairo index baaf6da6..26da1c84 100644 --- a/starknet/src/space/space.cairo +++ b/starknet/src/space/space.cairo @@ -17,7 +17,7 @@ trait ISpace { fn next_voting_strategy_index(self: @TContractState) -> u8; fn proposal_validation_strategy(self: @TContractState) -> Strategy; // #[view] - // fn vote_power(proposal_id: u256, choice: u8) -> u256; + fn vote_power(self: @TContractState, proposal_id: u256, choice: Choice) -> u256; // #[view] // fn vote_registry(proposal_id: u256, voter: ContractAddress) -> bool; fn proposals(self: @TContractState, proposal_id: u256) -> Proposal; @@ -75,32 +75,33 @@ trait ISpace { #[starknet::contract] mod Space { use super::ISpace; - use starknet::storage_access::{StorePacking, StoreUsingPacking}; - use starknet::{ClassHash, ContractAddress, info, Store, syscalls}; + use starknet::{ + storage_access::{StorePacking, StoreUsingPacking}, ClassHash, ContractAddress, info, Store, + syscalls + }; use zeroable::Zeroable; use array::{ArrayTrait, SpanTrait}; use clone::Clone; use option::OptionTrait; use hash::LegacyHash; use traits::{Into, TryInto}; - use serde::Serde; - - use sx::interfaces::{ - IProposalValidationStrategyDispatcher, IProposalValidationStrategyDispatcherTrait, - IVotingStrategyDispatcher, IVotingStrategyDispatcherTrait, IExecutionStrategyDispatcher, - IExecutionStrategyDispatcherTrait - }; - use sx::types::{ - UserAddress, Choice, FinalizationStatus, Strategy, IndexedStrategy, Proposal, - PackedProposal, IndexedStrategyTrait, IndexedStrategyImpl, UpdateSettingsCalldata, - NoUpdateU32, NoUpdateStrategy, NoUpdateArray + use sx::{ + interfaces::{ + IProposalValidationStrategyDispatcher, IProposalValidationStrategyDispatcherTrait, + IVotingStrategyDispatcher, IVotingStrategyDispatcherTrait, IExecutionStrategyDispatcher, + IExecutionStrategyDispatcherTrait + }, + types::{ + UserAddress, Choice, FinalizationStatus, Strategy, IndexedStrategy, Proposal, + PackedProposal, IndexedStrategyTrait, IndexedStrategyImpl, UpdateSettingsCalldata, + NoUpdateU32, NoUpdateStrategy, NoUpdateArray + }, + utils::{ + reinitializable::{Reinitializable}, ReinitializableImpl, bits::BitSetter, + legacy_hash::LegacyHashChoice, constants::INITIALIZE_SELECTOR + }, + external::ownable::Ownable }; - use sx::utils::reinitializable::Reinitializable; - use sx::utils::ReinitializableImpl; - use sx::utils::bits::BitSetter; - use sx::utils::legacy_hash::LegacyHashChoice; - use sx::external::ownable::Ownable; - use sx::utils::constants::INITIALIZE_SELECTOR; #[storage] struct Storage { @@ -304,6 +305,7 @@ mod Space { _add_authenticators(ref self, authenticators); self._next_proposal_id.write(1_u256); } + fn propose( ref self: ContractState, author: UserAddress, @@ -705,6 +707,10 @@ mod Space { } } + fn vote_power(self: @ContractState, proposal_id: u256, choice: Choice) -> u256 { + self._vote_power.read((proposal_id, choice)) + } + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { //TODO: temporary component syntax let mut state = Ownable::unsafe_new_contract_state(); diff --git a/starknet/src/tests.cairo b/starknet/src/tests.cairo index 96cd6099..ae1d679f 100644 --- a/starknet/src/tests.cairo +++ b/starknet/src/tests.cairo @@ -11,3 +11,5 @@ mod mocks; mod setup; mod utils; + +mod vote; diff --git a/starknet/src/tests/mocks.cairo b/starknet/src/tests/mocks.cairo index 70586b4d..95eba6d4 100644 --- a/starknet/src/tests/mocks.cairo +++ b/starknet/src/tests/mocks.cairo @@ -1,4 +1,5 @@ mod erc20_votes_preset; mod executor; +mod no_voting_power; mod proposal_validation_always_fail; mod space_v2; diff --git a/starknet/src/tests/mocks/executor.cairo b/starknet/src/tests/mocks/executor.cairo index b0b59ea2..ae7595f2 100644 --- a/starknet/src/tests/mocks/executor.cairo +++ b/starknet/src/tests/mocks/executor.cairo @@ -2,7 +2,6 @@ mod ExecutorExecutionStrategy { use sx::interfaces::IExecutionStrategy; use sx::types::{Proposal, ProposalStatus}; - use sx::execution_strategies::simple_quorum::SimpleQuorumExecutionStrategy; use starknet::ContractAddress; use core::serde::Serde; use core::array::ArrayTrait; @@ -39,3 +38,30 @@ mod ExecutorExecutionStrategy { #[constructor] fn constructor(ref self: ContractState) {} } + + +#[starknet::contract] +mod ExecutorWithoutTxExecutionStrategy { + use sx::interfaces::IExecutionStrategy; + use sx::types::{Proposal, ProposalStatus}; + use core::array::ArrayTrait; + + #[storage] + struct Storage {} + + #[external(v0)] + impl ExecutorWithoutTxExecutionStrategy of IExecutionStrategy { + // Dummy function that will do nothing + fn execute( + ref self: ContractState, + proposal: Proposal, + votes_for: u256, + votes_against: u256, + votes_abstain: u256, + payload: Array + ) {} + } + + #[constructor] + fn constructor(ref self: ContractState) {} +} diff --git a/starknet/src/tests/mocks/no_voting_power.cairo b/starknet/src/tests/mocks/no_voting_power.cairo new file mode 100644 index 00000000..fc4d60ca --- /dev/null +++ b/starknet/src/tests/mocks/no_voting_power.cairo @@ -0,0 +1,22 @@ +#[starknet::contract] +mod NoVotingPowerVotingStrategy { + use sx::interfaces::IVotingStrategy; + use sx::types::UserAddress; + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[external(v0)] + impl NoVotingPowerVotingStrategy of IVotingStrategy { + fn get_voting_power( + self: @ContractState, + timestamp: u32, + voter: UserAddress, + params: Array, + user_params: Array, + ) -> u256 { + 0 + } + } +} diff --git a/starknet/src/tests/vote.cairo b/starknet/src/tests/vote.cairo new file mode 100644 index 00000000..c7c250f1 --- /dev/null +++ b/starknet/src/tests/vote.cairo @@ -0,0 +1,378 @@ +#[cfg(test)] +mod tests { + use array::ArrayTrait; + use starknet::{ + ContractAddress, syscalls::deploy_syscall, testing, contract_address_const, info + }; + use traits::{Into, TryInto}; + use result::ResultTrait; + use option::OptionTrait; + use integer::u256_from_felt252; + use clone::Clone; + use serde::{Serde}; + + use sx::space::space::{Space, ISpaceDispatcher, ISpaceDispatcherTrait}; + use sx::authenticators::vanilla::{ + VanillaAuthenticator, IVanillaAuthenticatorDispatcher, IVanillaAuthenticatorDispatcherTrait + }; + use sx::tests::mocks::executor::ExecutorWithoutTxExecutionStrategy; + use sx::voting_strategies::vanilla::VanillaVotingStrategy; + use sx::proposal_validation_strategies::vanilla::VanillaProposalValidationStrategy; + use sx::tests::mocks::proposal_validation_always_fail::AlwaysFailProposalValidationStrategy; + use sx::tests::mocks::no_voting_power::NoVotingPowerVotingStrategy; + use sx::tests::setup::setup::setup::{setup, deploy}; + use sx::types::{ + UserAddress, Strategy, IndexedStrategy, Choice, FinalizationStatus, Proposal, + UpdateSettingsCalldataImpl + }; + use sx::tests::utils::strategy_trait::{StrategyImpl}; + use sx::utils::constants::{PROPOSE_SELECTOR, VOTE_SELECTOR, UPDATE_PROPOSAL_SELECTOR}; + + use Space::Space as SpaceImpl; + + fn get_execution_strategy() -> Strategy { + let mut constructor_calldata = array![]; + + let (execution_strategy_address, _) = deploy_syscall( + ExecutorWithoutTxExecutionStrategy::TEST_CLASS_HASH.try_into().unwrap(), + 0, + constructor_calldata.span(), + false + ) + .unwrap(); + let strategy = StrategyImpl::from_address(execution_strategy_address); + strategy + } + + fn create_proposal( + authenticator: IVanillaAuthenticatorDispatcher, + space: ISpaceDispatcher, + execution_strategy: Strategy + ) { + let author = UserAddress::Starknet(contract_address_const::<0x5678>()); + let mut propose_calldata = array![]; + author.serialize(ref propose_calldata); + execution_strategy.serialize(ref propose_calldata); + ArrayTrait::::new().serialize(ref propose_calldata); + ArrayTrait::::new().serialize(ref propose_calldata); + + // Create Proposal + authenticator.authenticate(space.contract_address, PROPOSE_SELECTOR, propose_calldata); + } + + #[test] + #[available_gas(10000000000)] + fn vote_for() { + let config = setup(); + let (factory, space) = deploy(@config); + + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Increasing block timestamp pass voting delay + testing::set_block_timestamp(config.voting_delay); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + assert(space.vote_power(proposal_id, Choice::For(())) == 1, 'Vote power should be 1'); + assert(space.vote_power(proposal_id, Choice::Against(())) == 0, 'Vote power should be 0'); + assert(space.vote_power(proposal_id, Choice::Abstain(())) == 0, 'Vote power should be 0'); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + fn vote_against() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Increasing block timestamp pass voting delay + testing::set_block_timestamp(config.voting_delay); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::Against(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + assert(space.vote_power(proposal_id, Choice::For(())) == 0, 'Vote power should be 0'); + assert(space.vote_power(proposal_id, Choice::Against(())) == 1, 'Vote power should be 1'); + assert(space.vote_power(proposal_id, Choice::Abstain(())) == 0, 'Vote power should be 0'); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + fn vote_abstain() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Increasing block timestamp by voting delay + testing::set_block_timestamp(config.voting_delay); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::Abstain(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + assert(space.vote_power(proposal_id, Choice::For(())) == 0, 'Vote power should be 0'); + assert(space.vote_power(proposal_id, Choice::Against(())) == 0, 'Vote power should be 0'); + assert(space.vote_power(proposal_id, Choice::Abstain(())) == 1, 'Vote power should be 1'); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + #[should_panic( + expected: ('Voting period has not started', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED') + )] + fn vote_too_early() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Do NOT increase block timestamp + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + #[should_panic(expected: ('Voting period has ended', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] + fn vote_too_late() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Fast forward to end of voting period + testing::set_block_timestamp(config.voting_delay + config.max_voting_duration); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + #[should_panic( + expected: ('Proposal has been finalized', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED') + )] + fn vote_finalized_proposal() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + testing::set_block_timestamp(config.voting_delay); + + space + .execute( + 1, array![] + ); // Execute the proposal (will work because execution strategy doesn't check for finalization status or quorum) + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + } + + #[test] + #[available_gas(10000000000)] + #[should_panic(expected: ('Caller is not an authenticator', 'ENTRYPOINT_FAILED'))] + fn vote_without_authenticator() { + let config = setup(); + let (factory, space) = deploy(@config); + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Fast forward to end of voting period + testing::set_block_timestamp(config.voting_delay + config.max_voting_duration); + + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + let proposal_id = 1_u256; + let choice = Choice::For(()); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + let metadata_uri = array![]; + + space.vote(voter, proposal_id, choice, user_voting_strategies, metadata_uri); + // TODO : check event + } + + #[test] + #[available_gas(10000000000)] + #[should_panic(expected: ('Voter has already voted', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] + fn vote_twice() { + let config = setup(); + let (factory, space) = deploy(@config); + + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + create_proposal(authenticator, space, execution_strategy); + + // Increasing block timestamp pass voting delay + testing::set_block_timestamp(config.voting_delay); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![IndexedStrategy { index: 0_u8, params: array![] }]; + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata.clone()); + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + } + + #[test] + #[available_gas(10000000000)] + #[should_panic( + expected: ('User has no voting power', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED') + )] + fn vote_no_voting_power() { + let config = setup(); + let (factory, space) = deploy(@config); + + let execution_strategy = get_execution_strategy(); + + let authenticator = IVanillaAuthenticatorDispatcher { + contract_address: *config.authenticators.at(0), + }; + + let (no_voting_power_contract, _) = deploy_syscall( + NoVotingPowerVotingStrategy::TEST_CLASS_HASH.try_into().unwrap(), + 0, + array![].span(), + false + ) + .unwrap(); + let no_voting_power_strategy = StrategyImpl::from_address(no_voting_power_contract); + + let mut input = UpdateSettingsCalldataImpl::default(); + input.voting_strategies_to_add = array![no_voting_power_strategy]; + + testing::set_contract_address(config.owner); + space.update_settings(input); + + create_proposal(authenticator, space, execution_strategy); + + // Increasing block timestamp pass voting delay + testing::set_block_timestamp(config.voting_delay); + + let mut vote_calldata = array![]; + let voter = UserAddress::Starknet(contract_address_const::<0x8765>()); + voter.serialize(ref vote_calldata); + let proposal_id = 1_u256; + proposal_id.serialize(ref vote_calldata); + let choice = Choice::For(()); + choice.serialize(ref vote_calldata); + let mut user_voting_strategies = array![ + IndexedStrategy { index: 1_u8, params: array![] } + ]; // index 1 + user_voting_strategies.serialize(ref vote_calldata); + ArrayTrait::::new().serialize(ref vote_calldata); + + authenticator.authenticate(space.contract_address, VOTE_SELECTOR, vote_calldata); + } +}