diff --git a/Makefile b/Makefile index 18d6277d..a0024374 100644 --- a/Makefile +++ b/Makefile @@ -3,5 +3,13 @@ build: rm -rf target/ scarb build -deploy: build - starknet declare --contract target/release/governance_Governance.json --account version_11 \ No newline at end of file +# Declares class hash, the hash is then printed into terminal. +# Following ENV variables must be set: +# STARKNET_ACCOUNT - path to account file +# STARKNET_KEYSTORE - path to keystore file +# STARKNET_RPC - RPC node URL - network will be selected based on RPC network +_declare: + starkli declare target/dev/governance_Governance.contract_class.json + + +declare: build declare diff --git a/src/contract.cairo b/src/contract.cairo index f47fd004..faad5c6f 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -2,25 +2,23 @@ // When Components arrive in Cairo 2.?, it will be refactored to take advantage of them. Random change to rerun CI use starknet::ContractAddress; -use governance::types::{ContractType, PropDetails}; +use governance::types::{ContractType, PropDetails, PropStatus, VoteCount}; #[starknet::interface] trait IGovernance { // PROPOSALS - fn vote(ref self: TContractState, prop_id: felt252, opinion: felt252); - fn get_proposal_details(self: @TContractState, prop_id: felt252) -> PropDetails; - fn get_vote_counts(self: @TContractState, prop_id: felt252) -> (u128, u128); - fn submit_proposal( - ref self: TContractState, impl_hash: felt252, to_upgrade: ContractType - ) -> felt252; - fn get_proposal_status(self: @TContractState, prop_id: felt252) -> felt252; + fn vote(ref self: TContractState, prop_id: u32, opinion: bool); + fn get_proposal_details(self: @TContractState, prop_id: u32) -> PropDetails; + fn get_vote_counts(self: @TContractState, prop_id: u32) -> VoteCount; + fn submit_proposal(ref self: TContractState, impl_hash: felt252, to_upgrade: u8) -> u32; + fn get_proposal_status(self: @TContractState, prop_id: u32) -> PropStatus; // UPGRADES fn get_governance_token_address(self: @TContractState) -> ContractAddress; fn get_amm_address(self: @TContractState) -> ContractAddress; - fn apply_passed_proposal(ref self: TContractState, prop_id: felt252); + fn apply_passed_proposal(ref self: TContractState, prop_id: u32); // AIRDROPS @@ -34,11 +32,10 @@ trait IGovernance { #[starknet::contract] mod Governance { - use governance::types::BlockNumber; - use governance::types::VoteStatus; + use governance::types::{ + BlockNumber, ContractType, PropDetails, PropStatus, VoteStatus, VoteCount + }; use governance::proposals::Proposals; - use governance::types::ContractType; - use governance::types::PropDetails; use governance::upgrades::Upgrades; use governance::options::Options; use governance::airdrop::airdrop as airdrop_component; @@ -53,13 +50,12 @@ mod Governance { #[storage] struct Storage { - proposal_details: LegacyMap::, - proposal_vote_ends: LegacyMap::, - proposal_vote_end_timestamp: LegacyMap::, - proposal_voted_by: LegacyMap::<(felt252, ContractAddress), VoteStatus>, - proposal_total_yay: LegacyMap::, - proposal_total_nay: LegacyMap::, - proposal_applied: LegacyMap::, // should be Bool after migration + proposal_details: LegacyMap::, + proposal_vote_ends: LegacyMap::, + proposal_vote_end_timestamp: LegacyMap::, + proposal_voted_by: LegacyMap::<(u32, ContractAddress), Option>, + proposal_total: LegacyMap::, + proposal_applied: LegacyMap::, // should be Bool after migration proposal_initializer_run: LegacyMap::, investor_voting_power: LegacyMap::, total_investor_distributed_power: felt252, @@ -75,16 +71,16 @@ mod Governance { #[derive(starknet::Event, Drop)] struct Proposed { - prop_id: felt252, + prop_id: u32, payload: felt252, - to_upgrade: ContractType + to_upgrade: u8, } #[derive(starknet::Event, Drop)] struct Voted { - prop_id: felt252, + prop_id: u32, voter: ContractAddress, - opinion: VoteStatus + opinion: bool } #[derive(starknet::Event, Drop)] @@ -105,28 +101,26 @@ mod Governance { impl Governance of super::IGovernance { // PROPOSALS - fn get_proposal_details(self: @ContractState, prop_id: felt252) -> PropDetails { + fn get_proposal_details(self: @ContractState, prop_id: u32) -> PropDetails { Proposals::get_proposal_details(prop_id) } // This should ideally return VoteCounts, but it seems like structs can't be returned from // C1.0 external fns as they can't be serialized // Actually it can, TODO do the same as I did with PropDetails for this - fn get_vote_counts(self: @ContractState, prop_id: felt252) -> (u128, u128) { + fn get_vote_counts(self: @ContractState, prop_id: u32) -> VoteCount { Proposals::get_vote_counts(prop_id) } - fn submit_proposal( - ref self: ContractState, impl_hash: felt252, to_upgrade: ContractType - ) -> felt252 { + fn submit_proposal(ref self: ContractState, impl_hash: felt252, to_upgrade: u8) -> u32 { Proposals::submit_proposal(impl_hash, to_upgrade) } - fn vote(ref self: ContractState, prop_id: felt252, opinion: felt252) { + fn vote(ref self: ContractState, prop_id: u32, opinion: bool) { Proposals::vote(prop_id, opinion) } - fn get_proposal_status(self: @ContractState, prop_id: felt252) -> felt252 { + fn get_proposal_status(self: @ContractState, prop_id: u32) -> PropStatus { Proposals::get_proposal_status(prop_id) } @@ -140,7 +134,7 @@ mod Governance { self.amm_address.read() } - fn apply_passed_proposal(ref self: ContractState, prop_id: felt252) { + fn apply_passed_proposal(ref self: ContractState, prop_id: u32) { Upgrades::apply_passed_proposal(prop_id) } diff --git a/src/proposals.cairo b/src/proposals.cairo index ba0ac4e9..a4604d77 100644 --- a/src/proposals.cairo +++ b/src/proposals.cairo @@ -1,4 +1,5 @@ mod Proposals { + use core::traits::Destruct; use governance::contract::IGovernance; use traits::TryInto; use option::OptionTrait; @@ -24,8 +25,7 @@ mod Proposals { use starknet::class_hash::class_hash_try_from_felt252; use starknet::contract_address::contract_address_to_felt252; - use governance::contract::Governance::proposal_total_yayContractMemberStateTrait; - use governance::contract::Governance::proposal_total_nayContractMemberStateTrait; + use governance::contract::Governance::proposal_totalContractMemberStateTrait; use governance::contract::Governance::proposal_vote_endsContractMemberStateTrait; use governance::contract::Governance::proposal_vote_end_timestampContractMemberStateTrait; use governance::contract::Governance::delegate_hashContractMemberStateTrait; @@ -35,33 +35,28 @@ mod Proposals { use governance::contract::Governance::ContractState; use governance::contract::Governance::unsafe_new_contract_state; use governance::contract::Governance; - use governance::types::BlockNumber; - use governance::types::ContractType; - use governance::types::PropDetails; + use governance::types::{ + BlockNumber, ContractType, PropDetails, PropStatus, VoteCount, VoteStatus + }; use governance::traits::IERC20Dispatcher; use governance::traits::IERC20DispatcherTrait; use governance::constants; - fn get_vote_counts(prop_id: felt252) -> (u128, u128) { + fn get_vote_counts(prop_id: u32) -> VoteCount { let state: ContractState = Governance::unsafe_new_contract_state(); - - let yay = state.proposal_total_yay.read(prop_id); - let nay = state.proposal_total_nay.read(prop_id); - - (yay.try_into().unwrap(), nay.try_into().unwrap()) + state.proposal_total.read(prop_id) } - fn get_proposal_details(prop_id: felt252) -> PropDetails { + fn get_proposal_details(prop_id: u32) -> PropDetails { let state = Governance::unsafe_new_contract_state(); state.proposal_details.read(prop_id) } - fn assert_correct_contract_type(contract_type: ContractType) { - let contract_type_u: u64 = contract_type.try_into().unwrap(); - assert(contract_type_u <= 4, 'invalid contract type') + fn assert_correct_contract_type(contract_type: u8) { + assert(contract_type <= 4, 'invalid contract type') } - fn assert_voting_in_progress(prop_id: felt252) { + fn assert_voting_in_progress(prop_id: u32) { let state = Governance::unsafe_new_contract_state(); let end_timestamp: u64 = state.proposal_vote_end_timestamp.read(prop_id); assert(end_timestamp != 0, 'prop_id not found'); @@ -75,11 +70,11 @@ mod Proposals { u256 { low, high } } - fn get_free_prop_id() -> felt252 { + fn get_free_prop_id() -> u32 { _get_free_prop_id(0) } - fn _get_free_prop_id(currid: felt252) -> felt252 { + fn _get_free_prop_id(currid: u32) -> u32 { let state = Governance::unsafe_new_contract_state(); let res = state.proposal_vote_ends.read(currid); if res == 0 { @@ -89,11 +84,11 @@ mod Proposals { } } - fn get_free_prop_id_timestamp() -> felt252 { + fn get_free_prop_id_timestamp() -> u32 { _get_free_prop_id_timestamp(0) } - fn _get_free_prop_id_timestamp(currid: felt252) -> felt252 { + fn _get_free_prop_id_timestamp(currid: u32) -> u32 { let state = Governance::unsafe_new_contract_state(); let res = state.proposal_vote_end_timestamp.read(currid); if res == 0 { @@ -103,7 +98,7 @@ mod Proposals { } } - fn submit_proposal(payload: felt252, to_upgrade: ContractType) -> felt252 { + fn submit_proposal(payload: felt252, to_upgrade: u8) -> u32 { assert_correct_contract_type(to_upgrade); let mut state = Governance::unsafe_new_contract_state(); let govtoken_addr = state.get_governance_token_address(); @@ -122,7 +117,7 @@ mod Proposals { state.proposal_details.write(prop_id, prop_details); let current_timestamp: u64 = get_block_timestamp(); - let end_timestamp: u64 = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp: u64 = current_timestamp + 600; // ten minutes state.proposal_vote_end_timestamp.write(prop_id, end_timestamp); state.emit(Governance::Proposed { prop_id, payload, to_upgrade }); @@ -236,23 +231,13 @@ mod Proposals { } - fn vote(prop_id: felt252, opinion: felt252) { - // Checks - assert((opinion == 1) | (opinion == 2), 'opinion must be either 1 or 2'); - let mut actual_opinion = 0; - if opinion == 2 { - actual_opinion = constants::MINUS_ONE; - } else { - actual_opinion = 1; - } - + fn vote(prop_id: u32, opinion: bool) { let mut state = Governance::unsafe_new_contract_state(); - let gov_token_addr = state.get_governance_token_address(); let caller_addr = get_caller_address(); - let curr_vote_status: felt252 = state.proposal_voted_by.read((prop_id, caller_addr)); - // TODO allow override of previous vote - assert(curr_vote_status == 0, 'already voted'); + let vote_status: Option = state.proposal_voted_by.read((prop_id, caller_addr)); + + assert(vote_status.is_none(), 'already voted'); let caller_balance_u256: u256 = IERC20Dispatcher { contract_address: gov_token_addr } .balanceOf(caller_addr); @@ -266,28 +251,34 @@ mod Proposals { assert_voting_in_progress(prop_id); - // Cast vote - state.proposal_voted_by.write((prop_id, caller_addr), actual_opinion); - if actual_opinion == constants::MINUS_ONE { - let curr_votes: u128 = state.proposal_total_nay.read(prop_id).try_into().unwrap(); - let new_votes: u128 = curr_votes + caller_voting_power; - assert(new_votes >= 0, 'new_votes must be non-negative'); - state.proposal_total_nay.write(prop_id, new_votes.into()); + let current_vote_count = state.proposal_total.read(prop_id); + + if opinion { + // Vote YAY + state.proposal_voted_by.write((prop_id, caller_addr), Option::Some(VoteStatus::Yay)); + let new_vote_count = VoteCount { + yay: current_vote_count.yay + caller_voting_power, nay: current_vote_count.nay + }; + assert(new_vote_count.yay >= 0, 'new_votes must be non-negative'); + state.proposal_total.write(prop_id, new_vote_count); } else { - let curr_votes: u128 = state.proposal_total_yay.read(prop_id).try_into().unwrap(); - let new_votes: u128 = curr_votes + caller_voting_power; - assert(new_votes >= 0, 'new_votes must be non-negative'); - state.proposal_total_yay.write(prop_id, new_votes.into()); + // Vote NAY + state.proposal_voted_by.write((prop_id, caller_addr), Option::Some(VoteStatus::Nay)); + let new_vote_count = VoteCount { + yay: current_vote_count.yay, nay: current_vote_count.nay + caller_voting_power + }; + assert(current_vote_count.nay >= 0, 'new_votes must be non-negative'); + state.proposal_total.write(prop_id, new_vote_count); } + state.emit(Governance::Voted { prop_id: prop_id, voter: caller_addr, opinion: opinion }); } - fn check_proposal_passed_express(prop_id: felt252) -> u8 { + fn check_proposal_passed_express(prop_id: u32) -> PropStatus { let state = Governance::unsafe_new_contract_state(); let gov_token_addr = state.get_governance_token_address(); - let yay_tally_felt: felt252 = state.proposal_total_yay.read(prop_id); - let yay_tally: u128 = yay_tally_felt.try_into().unwrap(); + let total = state.proposal_total.read(prop_id); let total_eligible_votes_from_tokenholders_u256: u256 = IERC20Dispatcher { contract_address: gov_token_addr } @@ -306,14 +297,14 @@ mod Proposals { let minimum_for_express: u128 = total_eligible_votes_from_tokenholders / 2; // Check if yay_tally >= minimum_for_express - if yay_tally >= minimum_for_express { - 1 + if total.yay >= minimum_for_express { + PropStatus { code: 1, status: 'accepted expressly' } } else { - 0 + PropStatus { code: 2, status: 'voting still in progress' } } } - fn get_proposal_status(prop_id: felt252) -> felt252 { + fn get_proposal_status(prop_id: u32) -> PropStatus { let state = Governance::unsafe_new_contract_state(); let end_timestamp: u64 = state.proposal_vote_end_timestamp.read(prop_id); @@ -324,11 +315,9 @@ mod Proposals { } let gov_token_addr = state.get_governance_token_address(); - let nay_tally_felt: felt252 = state.proposal_total_nay.read(prop_id); - let yay_tally_felt: felt252 = state.proposal_total_yay.read(prop_id); - let nay_tally: u128 = nay_tally_felt.try_into().unwrap(); - let yay_tally: u128 = yay_tally_felt.try_into().unwrap(); - let total_tally: u128 = yay_tally + nay_tally; + let vote_count = state.proposal_total.read(prop_id); + let total_tally: u128 = vote_count.yay + vote_count.nay; + // Here we multiply by 100 as the constant QUORUM is in percent. // If QUORUM = 10, quorum was not met if (total_tally*100) < (total_eligible * 10). let total_tally_multiplied = total_tally * 100; @@ -339,18 +328,15 @@ mod Proposals { let total_eligible_votes: u128 = total_eligible_votes_u256.low; let quorum_threshold: u128 = total_eligible_votes * constants::QUORUM; - if total_tally_multiplied < quorum_threshold { - return constants::MINUS_ONE; // didn't meet quorum - } - if yay_tally == nay_tally { - return constants::MINUS_ONE; // yay_tally = nay_tally + if total_tally_multiplied < quorum_threshold { + return PropStatus { status: 'did not meet quorum', code: 0 }; // didn't meet quorum } - if yay_tally > nay_tally { - return 1; // yay_tally > nay_tally + if vote_count.yay > vote_count.nay { + return PropStatus { status: 'passed', code: 1 }; // didn't meet quorum } else { - return constants::MINUS_ONE; // yay_tally < nay_tally + return PropStatus { status: 'rejected', code: 0 }; // didn't meet quorum } } } diff --git a/src/types.cairo b/src/types.cairo index 91f02ab2..4766c91a 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -7,17 +7,31 @@ use core::serde::Serde; #[derive(Copy, Drop, Serde, starknet::Store)] struct PropDetails { payload: felt252, - to_upgrade: felt252, + to_upgrade: u8, } -struct VoteCounts { - yay: felt252, - nay: felt252 +#[derive(Copy, Drop, Serde, starknet::Store)] +struct VoteCount { + yay: u128, + nay: u128 +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct PropStatus { + // text description of what happened + status: felt252, + // 0 - rejected, 1 - accepted, 2 - in progress + code: u8, } type BlockNumber = felt252; -type VoteStatus = felt252; // 0 = not voted, 1 = yay, -1 = nay + +#[derive(Drop, Destruct, PanicDestruct, starknet::Store)] +enum VoteStatus { + Yay, + Nay, +} type ContractType = - felt252; // for Carmine 0 = amm, 1 = governance, 2 = CARM token, 3 = merkle tree root, 4 = no-op/signal vote + u8; // for Carmine 0 = amm, 1 = governance, 2 = CARM token, 3 = merkle tree root, 4 = no-op/signal vote type OptionSide = felt252; type OptionType = felt252; diff --git a/src/upgrades.cairo b/src/upgrades.cairo index cccaa211..c538dc30 100644 --- a/src/upgrades.cairo +++ b/src/upgrades.cairo @@ -33,10 +33,11 @@ mod Upgrades { use governance::traits::IGovernanceTokenDispatcher; use governance::traits::IGovernanceTokenDispatcherTrait; - fn apply_passed_proposal(prop_id: felt252) { + fn apply_passed_proposal(prop_id: u32) { let mut state = Governance::unsafe_new_contract_state(); - let status = Proposals::get_proposal_status(prop_id); - assert(status == 1, 'prop not passed'); + let prop_status = Proposals::get_proposal_status(prop_id); + let status_code = prop_status.code; + assert(status_code == 1, 'prop not passed'); let applied: felt252 = state.proposal_applied.read(prop_id); assert(applied == 0, 'Proposal already applied'); @@ -47,33 +48,22 @@ mod Upgrades { let impl_hash = prop_details.payload; - // Apply the upgrade - // TODO use full match/switch when supported - match contract_type { - 0 => { - let amm_addr: ContractAddress = state.get_amm_address(); - IAMMDispatcher { contract_address: amm_addr }.upgrade(impl_hash); - }, - _ => { - if (contract_type == 1) { - let impl_hash_classhash: ClassHash = impl_hash.try_into().unwrap(); - syscalls::replace_class_syscall(impl_hash_classhash); - } else if (contract_type == 2) { - let govtoken_addr = state.get_governance_token_address(); - IGovernanceTokenDispatcher { contract_address: govtoken_addr } - .upgrade(impl_hash); - } else if (contract_type == 3) { - let mut airdrop_component_state: ComponentState = - Governance::airdrop_component::unsafe_new_component_state(); - airdrop_component_state.merkle_root.write(impl_hash); - } else { - assert( - contract_type == 4, 'invalid contract_type' - ); // type 4 is no-op, signal vote - } - } + if (contract_type == 0) { + let amm_addr: ContractAddress = state.get_amm_address(); + IAMMDispatcher { contract_address: amm_addr }.upgrade(impl_hash); + } else if (contract_type == 1) { + let impl_hash_classhash: ClassHash = impl_hash.try_into().unwrap(); + syscalls::replace_class_syscall(impl_hash_classhash); + } else if (contract_type == 2) { + let govtoken_addr = state.get_governance_token_address(); + IGovernanceTokenDispatcher { contract_address: govtoken_addr }.upgrade(impl_hash); + } else if (contract_type == 3) { + let mut airdrop_component_state: ComponentState = + Governance::airdrop_component::unsafe_new_component_state(); + airdrop_component_state.merkle_root.write(impl_hash); + } else { + assert(contract_type == 4, 'invalid contract_type'); // type 4 is no-op, signal vote } state.proposal_applied.write(prop_id, 1); // Mark the proposal as applied - // TODO emit event } }