diff --git a/starknet/src/interfaces/i_proposal_validation_strategy.cairo b/starknet/src/interfaces/i_proposal_validation_strategy.cairo index 543da850..d22eb037 100644 --- a/starknet/src/interfaces/i_proposal_validation_strategy.cairo +++ b/starknet/src/interfaces/i_proposal_validation_strategy.cairo @@ -7,6 +7,6 @@ trait IProposalValidationStrategy { self: @TContractState, author: UserAddress, params: Array, - userParams: Array + user_params: Array ) -> bool; } diff --git a/starknet/src/proposal_validation_strategies.cairo b/starknet/src/proposal_validation_strategies.cairo index d753bcfe..1e6675a7 100644 --- a/starknet/src/proposal_validation_strategies.cairo +++ b/starknet/src/proposal_validation_strategies.cairo @@ -1 +1,2 @@ +mod proposing_power; mod vanilla; diff --git a/starknet/src/proposal_validation_strategies/proposing_power.cairo b/starknet/src/proposal_validation_strategies/proposing_power.cairo new file mode 100644 index 00000000..cdbff952 --- /dev/null +++ b/starknet/src/proposal_validation_strategies/proposing_power.cairo @@ -0,0 +1,33 @@ +#[starknet::contract] +mod ProposingPowerProposalValidationStrategy { + use sx::interfaces::IProposalValidationStrategy; + use sx::types::{UserAddress, IndexedStrategy, IndexedStrategyTrait, Strategy}; + use sx::interfaces::{IVotingStrategyDispatcher, IVotingStrategyDispatcherTrait}; + use starknet::ContractAddress; + use starknet::info; + use traits::{Into, TryInto}; + use option::OptionTrait; + use result::ResultTrait; + use array::{ArrayTrait, SpanTrait}; + use serde::Serde; + use sx::utils::bits::BitSetter; + use box::BoxTrait; + use clone::Clone; + use sx::utils::proposition_power::_validate; + + #[storage] + struct Storage {} + + #[external(v0)] + impl ProposingPowerProposalValidationStrategy of IProposalValidationStrategy { + fn validate( + self: @ContractState, + author: UserAddress, + params: Array, // [proposal_threshold: u256, allowed_strategies: Array] + user_params: Array // [user_strategies: Array] + ) -> bool { + _validate(author, params, user_params) + } + } +} + diff --git a/starknet/src/proposal_validation_strategies/vanilla.cairo b/starknet/src/proposal_validation_strategies/vanilla.cairo index 935f0426..54a2fa17 100644 --- a/starknet/src/proposal_validation_strategies/vanilla.cairo +++ b/starknet/src/proposal_validation_strategies/vanilla.cairo @@ -13,7 +13,7 @@ mod VanillaProposalValidationStrategy { self: @ContractState, author: UserAddress, params: Array, - userParams: Array + user_params: Array ) -> bool { true } diff --git a/starknet/src/tests.cairo b/starknet/src/tests.cairo index f8471302..b6aa3b5e 100644 --- a/starknet/src/tests.cairo +++ b/starknet/src/tests.cairo @@ -4,6 +4,8 @@ mod test_space; mod test_upgrade; mod test_stark_tx_auth; +mod proposal_validation_strategies; + mod mocks; mod setup; diff --git a/starknet/src/tests/mocks/proposal_validation_always_fail.cairo b/starknet/src/tests/mocks/proposal_validation_always_fail.cairo index fd4a9f6b..2d418b35 100644 --- a/starknet/src/tests/mocks/proposal_validation_always_fail.cairo +++ b/starknet/src/tests/mocks/proposal_validation_always_fail.cairo @@ -13,7 +13,7 @@ mod AlwaysFailProposalValidationStrategy { self: @ContractState, author: UserAddress, params: Array, - userParams: Array + user_params: Array ) -> bool { false } diff --git a/starknet/src/tests/proposal_validation_strategies.cairo b/starknet/src/tests/proposal_validation_strategies.cairo new file mode 100644 index 00000000..0c278e9a --- /dev/null +++ b/starknet/src/tests/proposal_validation_strategies.cairo @@ -0,0 +1 @@ +mod proposing_power; diff --git a/starknet/src/tests/proposal_validation_strategies/proposing_power.cairo b/starknet/src/tests/proposal_validation_strategies/proposing_power.cairo new file mode 100644 index 00000000..d73600f5 --- /dev/null +++ b/starknet/src/tests/proposal_validation_strategies/proposing_power.cairo @@ -0,0 +1,236 @@ +#[cfg(test)] +mod tests { + use starknet::syscalls::deploy_syscall; + use traits::{TryInto}; + use starknet::SyscallResult; + use array::{ArrayTrait, SpanTrait}; + use result::ResultTrait; + use option::OptionTrait; + use sx::voting_strategies::vanilla::{VanillaVotingStrategy}; + use sx::voting_strategies::merkle_whitelist::{MerkleWhitelistVotingStrategy}; + use sx::utils::merkle::Leaf; + use sx::proposal_validation_strategies::proposing_power::{ + ProposingPowerProposalValidationStrategy + }; + use sx::interfaces::{ + IProposalValidationStrategy, IProposalValidationStrategyDispatcher, + IProposalValidationStrategyDispatcherTrait + }; + use sx::types::{IndexedStrategy, Strategy, UserAddress}; + use serde::Serde; + use starknet::contract_address_const; + use clone::Clone; + use sx::tests::test_merkle_whitelist::merkle_utils::{ + generate_merkle_data, generate_merkle_root, generate_proof + }; + + // #[test] + // #[available_gas(10000000000)] + fn test_vanilla_works() { + // deploy vanilla voting strategy + let (vanilla_contract, _) = deploy_syscall( + VanillaVotingStrategy::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap(); + + let vanilla_strategy = Strategy { address: vanilla_contract, params: array![], }; + + // create a proposal validation strategy + let (proposal_validation_contract, _) = deploy_syscall( + ProposingPowerProposalValidationStrategy::TEST_CLASS_HASH.try_into().unwrap(), + 0, + array![].span(), + false + ) + .unwrap(); + + let allowed_strategies = array![vanilla_strategy.clone()]; + let proposal_threshold = 1_u256; + let mut params = array![]; + proposal_threshold.serialize(ref params); + allowed_strategies.serialize(ref params); + + // used strategies + let used_strategy = IndexedStrategy { index: 0, params: array![], }; + let used_strategies = array![used_strategy.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let contract = IProposalValidationStrategyDispatcher { + contract_address: proposal_validation_contract, + }; + + let author = UserAddress::Starknet(contract_address_const::<0x123456789>()); + + // Vanilla should return 1 so it should be fine + let is_validated = contract.validate(author, params, user_params.clone()); + assert(is_validated, 'not enough VP'); + + // Now increase threshold + let proposal_threshold = 2_u256; + let mut params = array![]; + proposal_threshold.serialize(ref params); + allowed_strategies.serialize(ref params); + + // Threshold is 2 but VP should be 1 + let is_validated = contract.validate(author, params.clone(), user_params); + assert(!is_validated, 'Threshold should not be reached'); + + // But now if we add the vanilla voting strategy twice then it should be fine + let allowed_strategies = array![ + vanilla_strategy.clone(), vanilla_strategy.clone() + ]; // Add it twice + let proposal_threshold = 2_u256; // Threshold is still 2 + let mut params = array![]; + proposal_threshold.serialize(ref params); + allowed_strategies.serialize(ref params); + + let used_strategy1 = used_strategy; + let used_strategy2 = IndexedStrategy { index: 1, params: array![], }; + let used_strategies = array![used_strategy1, used_strategy2]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params, user_params); + assert(is_validated, 'should have 2 VP'); + } + + #[test] + #[available_gas(10000000000)] + fn test_merkle_whitelist_works() { + // deploy merkle whitelist contract + let (merkle_contract, _) = deploy_syscall( + MerkleWhitelistVotingStrategy::TEST_CLASS_HASH.try_into().unwrap(), + 0, + array![].span(), + false + ) + .unwrap(); + + // create proposal validation strategy based on the deployed merkle whitelist contract + let (proposal_validation_contract, _) = deploy_syscall( + ProposingPowerProposalValidationStrategy::TEST_CLASS_HASH.try_into().unwrap(), + 0, + array![].span(), + false + ) + .unwrap(); + + let contract = IProposalValidationStrategyDispatcher { + contract_address: proposal_validation_contract, + }; + + // Generate leaves + let voter1 = UserAddress::Starknet(contract_address_const::<0x111111>()); + let voter2 = UserAddress::Starknet(contract_address_const::<0x111112>()); + let voter3 = UserAddress::Starknet(contract_address_const::<0x111113>()); + let leaf1 = Leaf { address: voter1, voting_power: 1 }; + let leaf2 = Leaf { address: voter2, voting_power: 2 }; + let leaf3 = Leaf { address: voter3, voting_power: 3 }; + + let members = array![leaf1, leaf2, leaf3]; + + let merkle_data = generate_merkle_data(members.span()); + + let proof1 = generate_proof(merkle_data.span(), 0); + let proof2 = generate_proof(merkle_data.span(), 1); + let proof3 = generate_proof(merkle_data.span(), 2); + + let mut user_params = ArrayTrait::::new(); + leaf1.serialize(ref user_params); + proof1.serialize(ref user_params); + + let root = generate_merkle_root(merkle_data.span()); + let merkle_whitelist_strategy = Strategy { + address: merkle_contract, params: array![root], + }; + let allowed_strategies = array![merkle_whitelist_strategy.clone()]; + let proposal_threshold = + 2_u256; // voter1 should not hit threshold but voter2 and voter3 should + + let mut params = array![]; + proposal_threshold.serialize(ref params); + allowed_strategies.serialize(ref params); + + // setup for voter1 + let author = leaf1.address; + let mut indexed_params = array![]; + leaf1.serialize(ref indexed_params); + proof1.serialize(ref indexed_params); + let used_strategy = IndexedStrategy { index: 0, params: indexed_params, }; + let used_strategies = array![used_strategy.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params.clone(), user_params.clone()); + assert(!is_validated, 'should not have enough VP'); + + // setup for voter2 + let author = leaf2.address; + let mut indexed_params = array![]; + leaf2.serialize(ref indexed_params); + proof2.serialize(ref indexed_params); + let used_strategy = IndexedStrategy { index: 0, params: indexed_params, }; + let used_strategies = array![used_strategy.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params.clone(), user_params.clone()); + assert(is_validated, 'should have enough VP'); + + // setup for voter3 + let author = leaf3.address; + let mut indexed_params = array![]; + leaf3.serialize(ref indexed_params); + proof3.serialize(ref indexed_params); + let used_strategy = IndexedStrategy { index: 0, params: indexed_params, }; + let used_strategies = array![used_strategy.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params.clone(), user_params.clone()); + assert(is_validated, 'should have enough VP'); + + // -- Now let's mix merkle and vanilla voting strategies -- + + // deploy vanilla voting strategy + let (vanilla_contract, _) = deploy_syscall( + VanillaVotingStrategy::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap(); + + let vanilla_strategy = Strategy { address: vanilla_contract, params: array![], }; + + let allowed_strategies = array![ + merkle_whitelist_strategy.clone(), vanilla_strategy.clone() + ]; // update allowed strategies + let proposal_threshold = proposal_threshold; // threshold is left unchanged + let mut params = array![]; // update params + proposal_threshold.serialize(ref params); + allowed_strategies.serialize(ref params); + + // voter 1 should now have enough voting power! + let author = leaf1.address; + let vanilla = IndexedStrategy { index: 1, params: array![], }; + let mut indexed_params = array![]; + leaf1.serialize(ref indexed_params); + proof1.serialize(ref indexed_params); + let merkle = IndexedStrategy { index: 0, params: indexed_params, }; + + let used_strategies = array![vanilla.clone(), merkle.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params.clone(), user_params.clone()); + assert(is_validated, 'should have enough VP'); + + // and a random voter that doesn't use the whitelist should not have enough VP + let author = UserAddress::Starknet(contract_address_const::<0x123456789>()); + let used_strategies = array![vanilla.clone()]; + let mut user_params = array![]; + used_strategies.serialize(ref user_params); + + let is_validated = contract.validate(author, params.clone(), user_params.clone()); + assert(!is_validated, 'should not have enough VP'); + } +} diff --git a/starknet/src/tests/test_upgrade.cairo b/starknet/src/tests/test_upgrade.cairo index fe321210..34945a64 100644 --- a/starknet/src/tests/test_upgrade.cairo +++ b/starknet/src/tests/test_upgrade.cairo @@ -11,7 +11,6 @@ mod tests { use option::OptionTrait; use integer::u256_from_felt252; use clone::Clone; - use debug::PrintTrait; use serde::{Serde}; use sx::space::space::{Space, ISpaceDispatcher, ISpaceDispatcherTrait}; diff --git a/starknet/src/types/indexed_strategy.cairo b/starknet/src/types/indexed_strategy.cairo index d1ca8787..e7dbfb59 100644 --- a/starknet/src/types/indexed_strategy.cairo +++ b/starknet/src/types/indexed_strategy.cairo @@ -29,7 +29,7 @@ impl IndexedStrategyImpl of IndexedStrategyTrait { // Check that bit at index `strats[i].index` is not set. let s = math::pow(2_u256, *self.at(i).index); - assert((bit_map & s) == 1_u256, 'Duplicate Found'); + assert((bit_map & s) != u256 { low: 1_u128, high: 0_u128 }, 'Duplicate Found'); // Update aforementioned bit. bit_map = bit_map | s; i += 1; diff --git a/starknet/src/utils.cairo b/starknet/src/utils.cairo index 9cd3b565..057bf08a 100644 --- a/starknet/src/utils.cairo +++ b/starknet/src/utils.cairo @@ -10,6 +10,8 @@ mod legacy_hash; mod math; mod merkle; +mod proposition_power; + mod struct_hash; mod single_slot_proof; diff --git a/starknet/src/utils/merkle.cairo b/starknet/src/utils/merkle.cairo index eac03d73..1f044389 100644 --- a/starknet/src/utils/merkle.cairo +++ b/starknet/src/utils/merkle.cairo @@ -5,7 +5,6 @@ use serde::Serde; use sx::types::UserAddress; use clone::Clone; use hash::{LegacyHash}; -use debug::PrintTrait; use sx::utils::legacy_hash::LegacyHashSpanFelt252; /// Leaf struct for the merkle tree diff --git a/starknet/src/utils/proposition_power.cairo b/starknet/src/utils/proposition_power.cairo new file mode 100644 index 00000000..e7361d4d --- /dev/null +++ b/starknet/src/utils/proposition_power.cairo @@ -0,0 +1,69 @@ +use sx::interfaces::IProposalValidationStrategy; +use sx::types::{UserAddress, IndexedStrategy, IndexedStrategyTrait, Strategy}; +use sx::interfaces::{IVotingStrategyDispatcher, IVotingStrategyDispatcherTrait}; +use starknet::ContractAddress; +use starknet::info; +use traits::{Into, TryInto}; +use option::OptionTrait; +use result::ResultTrait; +use array::{ArrayTrait, SpanTrait}; +use serde::Serde; +use sx::utils::bits::BitSetter; +use box::BoxTrait; +use clone::Clone; + +fn _get_cumulative_power( + voter: UserAddress, + block_number: u32, + mut user_strategies: Array, + allowed_strategies: Array, +) -> u256 { + user_strategies.assert_no_duplicate_indices(); + let mut total_voting_power = 0_u256; + loop { + match user_strategies.pop_front() { + Option::Some(indexed_strategy) => { + match allowed_strategies.get(indexed_strategy.index.into()) { + Option::Some(strategy) => { + let strategy: Strategy = strategy.unbox().clone(); + total_voting_power += IVotingStrategyDispatcher { + contract_address: strategy.address + } + .get_voting_power( + block_number, voter, strategy.params, indexed_strategy.params, + ); + }, + Option::None => { + panic_with_felt252('Invalid strategy index'); + }, + }; + }, + Option::None => { + break total_voting_power; + }, + }; + } +} + +fn _validate( + author: UserAddress, + params: Array, // [proposal_threshold: u256, allowed_strategies: Array] + user_params: Array // [user_strategies: Array] +) -> bool { + let mut params_span = params.span(); + let (proposal_threshold, allowed_strategies) = Serde::<( + u256, Array + )>::deserialize(ref params_span) + .unwrap(); + + let mut user_params_span = user_params.span(); + let user_strategies = Serde::>::deserialize(ref user_params_span) + .unwrap(); + + let timestamp: u32 = info::get_block_timestamp().try_into().unwrap(); + let voting_power = _get_cumulative_power( + author, timestamp, user_strategies, allowed_strategies + ); + voting_power >= proposal_threshold +} + diff --git a/starknet/src/voting_strategies/merkle_whitelist.cairo b/starknet/src/voting_strategies/merkle_whitelist.cairo index 6afeb9b5..182608e7 100644 --- a/starknet/src/voting_strategies/merkle_whitelist.cairo +++ b/starknet/src/voting_strategies/merkle_whitelist.cairo @@ -6,7 +6,6 @@ mod MerkleWhitelistVotingStrategy { use array::{ArrayTrait, Span, SpanTrait}; use option::OptionTrait; use sx::utils::merkle::{assert_valid_proof, Leaf}; - use debug::PrintTrait; const LEAF_SIZE: usize = 4; // Serde::::serialize().len() @@ -19,8 +18,8 @@ mod MerkleWhitelistVotingStrategy { self: @ContractState, timestamp: u32, voter: UserAddress, - params: Array, // [root] - user_params: Array, // [Serde(leaf), Serde(proofs)] + params: Array, // [root: felt252] + user_params: Array, // [leaf: Leaf, proof: Array)] ) -> u256 { let cache = user_params.span(); // cache