diff --git a/src/proposals.cairo b/src/proposals.cairo index a0a41e8f..a16e429c 100644 --- a/src/proposals.cairo +++ b/src/proposals.cairo @@ -16,6 +16,9 @@ trait IProposals { fn get_user_voted( self: @TContractState, user_address: ContractAddress, prop_id: felt252 ) -> VoteStatus; + fn submit_custom_proposal( + ref self: TContractState, custom_proposal_type: u32, calldata: Span + ) -> u32; } #[starknet::component] @@ -34,11 +37,13 @@ mod proposals { use hash::LegacyHash; use starknet::contract_address::ContractAddressZeroable; + use starknet::class_hash::ClassHashZeroable; use starknet::get_block_info; use starknet::get_block_timestamp; use starknet::get_caller_address; use starknet::BlockInfo; use starknet::ContractAddress; + use starknet::ClassHash; use starknet::contract_address_const; use starknet::event::EventEmitter; use starknet::get_contract_address; @@ -51,6 +56,7 @@ mod proposals { use governance::types::ContractType; use governance::types::PropDetails; use governance::types::VoteStatus; + use governance::types::CustomProposalConfig; use governance::traits::IERC20Dispatcher; use governance::traits::IERC20DispatcherTrait; use governance::traits::get_governance_token_address_self; @@ -67,6 +73,10 @@ mod proposals { proposal_applied: LegacyMap::, // should be Bool after migration delegate_hash: LegacyMap::, total_delegated_to: LegacyMap::, + custom_proposal_type: LegacyMap::, // custom proposal type + custom_proposal_payload: LegacyMap::< + (u32, u32), felt252 + > // mapping from prop_id and index to calldata } #[derive(starknet::Event, Drop)] @@ -221,6 +231,17 @@ mod proposals { .update_calldata(to_addr, new_amount, calldata_span, new_list, index + 1_usize); } } + + fn assert_eligible_to_propose(self: @ComponentState) { + let user_address = get_caller_address(); + let govtoken_addr = get_governance_token_address_self(); + let caller_balance: u128 = IERC20Dispatcher { contract_address: govtoken_addr } + .balanceOf(user_address) + .low; + let total_supply = IERC20Dispatcher { contract_address: govtoken_addr }.totalSupply(); + let res: u256 = (caller_balance * constants::NEW_PROPOSAL_QUORUM).into(); + assert(total_supply < res, 'not enough tokens to submit'); + } } #[embeddable_as(ProposalsImpl)] @@ -275,15 +296,7 @@ mod proposals { ref self: ComponentState, payload: felt252, to_upgrade: ContractType ) -> felt252 { assert_correct_contract_type(to_upgrade); - let govtoken_addr = get_governance_token_address_self(); - let caller = get_caller_address(); - let caller_balance: u128 = IERC20Dispatcher { contract_address: govtoken_addr } - .balanceOf(caller) - .low; - let total_supply = IERC20Dispatcher { contract_address: govtoken_addr }.totalSupply(); - let res: u256 = (caller_balance * constants::NEW_PROPOSAL_QUORUM) - .into(); // TODO use such multiplication that u128 * u128 = u256 - assert(total_supply < res, 'not enough tokens to submit'); + self.assert_eligible_to_propose(); let prop_id = self.get_free_prop_id_timestamp(); let prop_details = PropDetails { payload: payload, to_upgrade: to_upgrade.into() }; @@ -297,6 +310,47 @@ mod proposals { prop_id } + fn submit_custom_proposal( + ref self: ComponentState, + custom_proposal_type: u32, + mut calldata: Span + ) -> u32 { + let config: CustomProposalConfig = self.custom_proposal_type.read(custom_proposal_type); + assert( + config.target.is_non_zero(), 'custom prop classhash 0' + ); // wrong custom proposal type? + assert( + config.selector.is_non_zero(), 'custom prop selector 0' + ); // wrong custom proposal type? + self.assert_eligible_to_propose(); + + let prop_id_felt = self.get_free_prop_id_timestamp(); + let prop_id: u32 = prop_id_felt.try_into().unwrap(); + let payload = custom_proposal_type.into(); + let prop_details = PropDetails { + payload, to_upgrade: 5 + }; // to_upgrade = 5 – custom proposal type. + 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; + self.proposal_vote_end_timestamp.write(prop_id_felt, end_timestamp); + self.emit(Proposed { prop_id: prop_id_felt, payload, to_upgrade: 5 }); + + self.custom_proposal_payload.write((prop_id, 0), calldata.len().into()); + let mut i: u32 = 1; + loop { + match calldata.pop_front() { + Option::Some(argument) => { + self.custom_proposal_payload.write((prop_id, i), *argument); + i += 1; + }, + Option::None(()) => { break (); } + } + }; + prop_id + } + // fn delegate_vote( // ref self: ComponentState, diff --git a/src/types.cairo b/src/types.cairo index b2984def..df232ee6 100644 --- a/src/types.cairo +++ b/src/types.cairo @@ -1,6 +1,5 @@ use starknet::SyscallResult; -use starknet::syscalls::storage_read_syscall; -use starknet::syscalls::storage_write_syscall; +use starknet::syscalls::{storage_read_syscall, storage_write_syscall, ClassHash}; use starknet::storage_address_from_base_and_offset; use core::serde::Serde; @@ -16,8 +15,15 @@ struct VoteCounts { } type BlockNumber = felt252; -type VoteStatus = felt252; // 0 = not voted, 1 = yay, -1 = nay +type VoteStatus = felt252; // 0 = not voted, 1 = yay, 2 = nay type ContractType = - u64; // for Carmine 0 = amm, 1 = governance, 2 = CARM token, 3 = merkle tree root, 4 = no-op/signal vote + u64; // for Carmine 0 = amm, 1 = governance, 2 = CARM token, 3 = merkle tree root, 4 = no-op/signal vote, 5 = custom proposal type OptionSide = felt252; type OptionType = felt252; + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct CustomProposalConfig { + target: felt252, //class hash if library call, contract address if regular call + selector: felt252, + library_call: bool +} diff --git a/src/upgrades.cairo b/src/upgrades.cairo index b7ec6f1e..e05982f9 100644 --- a/src/upgrades.cairo +++ b/src/upgrades.cairo @@ -5,6 +5,8 @@ trait IUpgrades { #[starknet::component] mod upgrades { + use core::result::ResultTrait; + use core::array::ArrayTrait; use traits::TryInto; use option::OptionTrait; use traits::Into; @@ -16,7 +18,7 @@ mod upgrades { use starknet::ContractAddress; use starknet::class_hash; - use governance::types::PropDetails; + use governance::types::{CustomProposalConfig, PropDetails}; use governance::contract::Governance; use governance::contract::Governance::ContractState; @@ -92,6 +94,43 @@ mod upgrades { } else if (contract_type == 3) { let mut airdrop_comp = get_dep_component_mut!(ref self, Airdrop); airdrop_comp.merkle_root.write(impl_hash); + } else if (contract_type == 5) { + // custom proposal + let custom_proposal_type: u32 = impl_hash + .try_into() + .expect('custom prop type fit in u32'); + let config: CustomProposalConfig = proposals_comp + .custom_proposal_type + .read(custom_proposal_type); + + let prop_id_: u32 = prop_id.try_into().unwrap(); + let mut calldata_len = proposals_comp + .custom_proposal_payload + .read((prop_id_, 0)); + let mut calldata: Array = ArrayTrait::new(); + let mut i: u32 = 1; + while (calldata_len != 0) { + calldata + .append(proposals_comp.custom_proposal_payload.read((prop_id_, i))); + i += 1; + calldata_len -= 1; + }; + + if (config.library_call) { + let res = syscalls::library_call_syscall( + config.target.try_into().expect('unable to convert>classhash'), + config.selector, + calldata.span() + ); + res.expect('libcall failed'); + } else { + let res = syscalls::call_contract_syscall( + config.target.try_into().expect('unable to convert>addr'), + config.selector, + calldata.span() + ); + res.expect('contract call failed'); + } } else { assert( contract_type == 4, 'invalid contract_type'