diff --git a/src/constants.cairo b/src/constants.cairo index 17480f85..9d295f00 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -1,13 +1,3 @@ const NEW_PROPOSAL_QUORUM: u128 = 200; // 1/200 of totalSupply required to propose an upgrade. Quorums don't take into account investors. at all, they don't count into total eligible voters, but do vote -const QUORUM: u128 = 10; // 1/10 of totalSupply required to participate to pass const MINUS_ONE: felt252 = 0x800000000000011000000000000000000000000000000000000000000000000; -const TEAM_TOKEN_BALANCE: u128 = 1000000000000000000; -const PROPOSAL_VOTING_SECONDS: u64 = consteval_int!(60 * 60 * 24 * 7); - - -// ADDRESSES - -const USDC_ADDRESS: felt252 = 0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; -const ETH_ADDRESS: felt252 = 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; -const BTC_ADDRESS: felt252 = 0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac; diff --git a/src/contract.cairo b/src/contract.cairo index f1d9419b..32acf8f7 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -134,6 +134,7 @@ mod Governance { ref self: ContractState, voting_token_class: ClassHash, floating_token_class: ClassHash, + treasury_classhash: ClassHash, recipient: ContractAddress ) { // This is not used in production on mainnet, because the governance token is already deployed (and distributed). @@ -170,25 +171,42 @@ mod Governance { staking.set_curve_point(SIX_MONTHS, 160); staking.set_curve_point(ONE_YEAR, 250); - let treasury_classhash = 0x06a38a382eddf3d7ebf9673516ac7cf1ff185a1ecbf490cb07f1687e531eb9ec - .try_into() - .unwrap(); - let mut treasury_calldata: Array = ArrayTrait::new(); - treasury_calldata.append(governance_address.into()); - treasury_calldata.append(0x1); // carmine amm addr - treasury_calldata.append(0x1); // zklend addr - let (treasury_address, _) = deploy_syscall( - treasury_classhash, 42, treasury_calldata.span(), true - ) - .unwrap(); let proposals = IProposalsDispatcher { contract_address: governance_address }; - let send_tokens_custom_proposal_config: CustomProposalConfig = CustomProposalConfig { - target: treasury_address.into(), - selector: selector!("send_tokens_to_address"), - library_call: false + + if (treasury_classhash.into() != 0) { + let mut treasury_calldata: Array = ArrayTrait::new(); + treasury_calldata.append(governance_address.into()); + treasury_calldata.append(0x1); // carmine amm addr + treasury_calldata.append(0x1); // zklend addr + let (treasury_address, _) = deploy_syscall( + treasury_classhash, 42, treasury_calldata.span(), true + ) + .unwrap(); + + let send_tokens_custom_proposal_config: CustomProposalConfig = CustomProposalConfig { + target: treasury_address.into(), + selector: selector!("send_tokens_to_address"), + library_call: false, + proposal_voting_time: 0 // use global default + }; + + proposals.add_custom_proposal_config(send_tokens_custom_proposal_config); + } + + let set_default_proposal_params_custom_proposal_config: CustomProposalConfig = + CustomProposalConfig { + target: governance_address.into(), + selector: selector!("set_default_proposal_params"), + library_call: false, + proposal_voting_time: 0 // use global default }; - proposals.add_custom_proposal_config(send_tokens_custom_proposal_config); + proposals.add_custom_proposal_config(set_default_proposal_params_custom_proposal_config); + + proposals + .set_default_proposal_params( + quorum: 10, proposal_voting_seconds: consteval_int!(60 * 60 * 24 * 7) + ); // can be omitted to keep the default values } #[abi(embed_v0)] diff --git a/src/proposals.cairo b/src/proposals.cairo index f79aec66..4c5084b4 100644 --- a/src/proposals.cairo +++ b/src/proposals.cairo @@ -35,6 +35,9 @@ trait IProposals { ); fn get_total_delegated_to(self: @TContractState, to_addr: ContractAddress) -> u128; fn add_custom_proposal_config(ref self: TContractState, config: CustomProposalConfig) -> u32; + fn set_default_proposal_params( + ref self: TContractState, quorum: u32, proposal_voting_seconds: u32 + ); } #[starknet::component] @@ -94,7 +97,9 @@ mod proposals { custom_proposal_type: LegacyMap::, // custom proposal type custom_proposal_payload: LegacyMap::< (u32, u32), felt252 - > // mapping from prop_id and index to calldata + >, // mapping from prop_id and index to calldata + quorum: u32, + proposal_voting_seconds: u32 } #[derive(starknet::Event, Drop)] @@ -270,6 +275,24 @@ mod proposals { }; i } + + fn get_quorum(self: @ComponentState) -> u32 { + let saved = self.quorum.read(); + if (saved == 0) { + 10 + } else { + saved + } + } + + fn get_proposal_voting_seconds(self: @ComponentState) -> u64 { + let saved = self.proposal_voting_seconds.read(); + if (saved == 0) { + consteval_int!(60 * 60 * 24 * 7) + } else { + saved.into() + } + } } #[embeddable_as(ProposalsImpl)] @@ -337,7 +360,7 @@ mod proposals { self.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 + self.get_proposal_voting_seconds(); self.proposal_vote_end_timestamp.write(prop_id, end_timestamp); self.emit(Proposed { prop_id, payload, to_upgrade }); @@ -367,7 +390,11 @@ mod proposals { self.proposal_details.write(prop_id_felt, prop_details); let current_timestamp: u64 = get_block_timestamp(); - let end_timestamp: u64 = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp: u64 = if (config.proposal_voting_time == 0) { + current_timestamp + self.get_proposal_voting_seconds() + } else { + current_timestamp + config.proposal_voting_time.into() + }; self.proposal_vote_end_timestamp.write(prop_id_felt, end_timestamp); self.emit(Proposed { prop_id: prop_id_felt, payload, to_upgrade: 5 }); @@ -522,7 +549,7 @@ mod proposals { assert(total_eligible_votes_u256.high == 0, 'unable to check quorum'); let total_eligible_votes: u128 = total_eligible_votes_u256.low; - let quorum_threshold: u128 = total_eligible_votes * constants::QUORUM; + let quorum_threshold: u128 = total_eligible_votes * self.get_quorum().into(); if total_tally_multiplied < quorum_threshold { return constants::MINUS_ONE; // didn't meet quorum } @@ -554,5 +581,17 @@ mod proposals { self.custom_proposal_type.write(idx, config); idx } + + fn set_default_proposal_params( + ref self: ComponentState, quorum: u32, proposal_voting_seconds: u32 + ) { + assert(get_caller_address() == get_contract_address(), 'can only be called by self'); + assert(quorum < 30, 'quorum must be <30'); + assert(quorum >= 1, 'quorum < 1?'); + assert(proposal_voting_seconds > 3600, 'propvoting secs too short'); + assert(proposal_voting_seconds < 3000000, 'propvoting secs > 1mo?'); + self.quorum.write(quorum); + self.proposal_voting_seconds.write(proposal_voting_seconds); + } } } diff --git a/src/types.cairo b/src/types.cairo index 09612fb4..ac26a540 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -24,7 +24,8 @@ type ContractType = struct CustomProposalConfig { target: felt252, //class hash if library call, contract address if regular call selector: felt252, - library_call: bool + library_call: bool, + proposal_voting_time: u32 } #[derive(Drop, Serde, starknet::Store)] diff --git a/tests/README.md b/tests/README.md index f70e5cf1..900e4ec0 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,6 +40,8 @@ successfully [] Re-applying a proposal that has already passed : make sure the contract rejects the submission of a proposal that has expired in the past +[] Change the quorum and proposal voting time + ### Delegate_vote and withdraw_delegation functions diff --git a/tests/basic.cairo b/tests/basic.cairo index 6f881507..02d31815 100644 --- a/tests/basic.cairo +++ b/tests/basic.cairo @@ -66,7 +66,7 @@ fn test_upgrade_mainnet_to_master() { //simulate passage of time let current_timestamp = get_block_timestamp(); - let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp = current_timestamp + consteval_int!(60 * 60 * 24 * 7); start_warp(CheatTarget::One(gov_contract_addr), end_timestamp + 1); assert(dispatcher.get_proposal_status(new_prop_id) == 1, 'proposal not passed!'); diff --git a/tests/deploy.cairo b/tests/deploy.cairo index f892c399..18657f1d 100644 --- a/tests/deploy.cairo +++ b/tests/deploy.cairo @@ -15,6 +15,7 @@ fn test_deploy() { let mut args: Array = ArrayTrait::new(); args.append(voting_token_class); args.append(floating_token_class); + args.append(0); args.append(0x03f37e36c20E85e6F39b2C6F6e7ECEB2e3aAb40b94064f20983588cfe9f6fc60); gov_contract.deploy(@args).expect('unable to deploy governance'); } diff --git a/tests/proposals_tests.cairo b/tests/proposals_tests.cairo index 60415994..3f227db0 100644 --- a/tests/proposals_tests.cairo +++ b/tests/proposals_tests.cairo @@ -31,7 +31,8 @@ use super::staking_tests::{set_staking_curve, stake_all, stake_half}; const GOV_TOKEN_INITIAL_SUPPLY: felt252 = 1000000000000000000; - +const PROPOSAL_VOTING_SECONDS: u64 = consteval_int!(60 * 60 * 24 * 7); +const QUORUM: u128 = 10; #[test] fn test_express_proposal() { @@ -62,7 +63,7 @@ fn test_proposal_expiry() { //simulate passage of time let current_timestamp = get_block_timestamp(); - let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp = current_timestamp + PROPOSAL_VOTING_SECONDS; start_warp(CheatTarget::One(gov.contract_address), end_timestamp + 1); let status = dispatcher.get_proposal_status(prop_id); @@ -87,7 +88,7 @@ fn test_vote_on_expired_proposal() { //simulate passage of time let current_timestamp = get_block_timestamp(); - let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp = current_timestamp + PROPOSAL_VOTING_SECONDS; start_warp(CheatTarget::One(gov.contract_address), end_timestamp + 1); prank( @@ -137,11 +138,11 @@ fn test_vote_on_quorum_not_met() { } .totalSupply() .low; - let quorum_threshold = total_eligible_votes * constants::QUORUM / 100; + let quorum_threshold = total_eligible_votes * QUORUM / 100; assert(total_votes < quorum_threshold, 'Total votes >= quorum threshold'); let current_timestamp = get_block_timestamp(); - let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp = current_timestamp + PROPOSAL_VOTING_SECONDS; start_warp(CheatTarget::One(gov_contract_addr), end_timestamp + 1); assert( dispatcher.get_proposal_status(prop_id) == constants::MINUS_ONE, @@ -462,7 +463,7 @@ fn test_add_comment_on_non_live_proposal() { //simulate passage of time let current_timestamp = get_block_timestamp(); - let end_timestamp = current_timestamp + constants::PROPOSAL_VOTING_SECONDS; + let end_timestamp = current_timestamp + PROPOSAL_VOTING_SECONDS; start_warp(CheatTarget::One(gov_contract_addr), end_timestamp + 1); IDiscussionDispatcher { contract_address: gov_contract_addr } diff --git a/tests/setup.cairo b/tests/setup.cairo index 88584a11..10ade327 100644 --- a/tests/setup.cairo +++ b/tests/setup.cairo @@ -50,6 +50,7 @@ fn deploy_governance_and_both_tokens() -> ( let mut args: Array = ArrayTrait::new(); args.append(voting_token_class.class_hash.into()); args.append(floating_token_class.class_hash.into()); + args.append(0); // treasury – no treasury args.append(admin_addr); gov_contract .deploy_at(@args, governance_address.try_into().unwrap()) diff --git a/tests/test_treasury.cairo b/tests/test_treasury.cairo index caa57b33..b5019b92 100644 --- a/tests/test_treasury.cairo +++ b/tests/test_treasury.cairo @@ -23,7 +23,7 @@ mod testStorage { use starknet::ContractAddress; const zero_address: felt252 = 0; const GOV_CONTRACT_ADDRESS: felt252 = - 0x0304256e5fade73a6fc8f49ed7c1c43ac34e6867426601b01204e1f7ba05b53d; + 0x57dfabb5a506bfd1937062562a1adf45c7c4c62d0377ccfc59a0b42d7ab3212; const CARMINE_AMM_CONTRACT_ADDRESS: felt252 = 0x047472e6755afc57ada9550b6a3ac93129cc4b5f98f51c73e0644d129fd208d9; const ZKLEND_MARKET_C0NTRACT_ADDRESS: felt252 = @@ -43,23 +43,29 @@ fn get_important_addresses() -> ( testStorage::ZKLEND_MARKET_C0NTRACT_ADDRESS .try_into() .unwrap(); - let contract = declare("Treasury").expect('unable to declare'); let mut calldata = ArrayTrait::new(); gov_contract_address.serialize(ref calldata); AMM_contract_address.serialize(ref calldata); zklend_market_contract_address.serialize(ref calldata); - // Precalculate the address to obtain the contract address before the constructor call (deploy) itself - let contract_address = contract.precalculate_address(@calldata); - - prank(CheatTarget::One(contract_address), gov_contract_address, CheatSpan::TargetCalls(1)); - let (deployed_contract, _) = contract.deploy(@calldata).unwrap(); + //let contract = declare("Treasury").expect('unable to declare'); + let treasury_address: ContractAddress = match declare("Treasury") { + Result::Ok(r) => { + let contract_address = r.precalculate_address(@calldata); + prank( + CheatTarget::One(contract_address), gov_contract_address, CheatSpan::TargetCalls(1) + ); + let (deployed_contract, _) = r.deploy(@calldata).unwrap(); + deployed_contract + }, + // FIXME – this is suboptimal, but afaik no way to get this in current snforge version? + Result::Err(_) => 0x04c990da03da72bdfb10db5c04e8aaa9d5404a07fe454037facb7744c132d42c + .try_into() + .unwrap() + }; return ( - gov_contract_address, - AMM_contract_address, - deployed_contract, - zklend_market_contract_address + gov_contract_address, AMM_contract_address, treasury_address, zklend_market_contract_address ); }