diff --git a/pallets/governance/src/benchmarks.rs b/pallets/governance/src/benchmarks.rs index fcb7275..e6d220a 100644 --- a/pallets/governance/src/benchmarks.rs +++ b/pallets/governance/src/benchmarks.rs @@ -193,4 +193,13 @@ benchmarks! { disable_vote_delegation { let module_key: T::AccountId = account("ModuleKey", 0, 2); }: _(RawOrigin::Signed(module_key)) + + add_emission_proposal { + let module_key: T::AccountId = account("ModuleKey", 0, 2); + + let config = crate::GlobalGovernanceConfig::::get(); + let cost = config.proposal_cost; + let _ = ::Currency::deposit_creating(&module_key, cost); + + }: _(RawOrigin::Signed(module_key.clone()), Percent::from_parts(40), Percent::from_parts(40), data) } diff --git a/pallets/governance/src/lib.rs b/pallets/governance/src/lib.rs index 0e2588e..86338b1 100644 --- a/pallets/governance/src/lib.rs +++ b/pallets/governance/src/lib.rs @@ -305,6 +305,23 @@ pub mod pallet { let delegator = ensure_signed(origin)?; voting::disable_delegation::(delegator) } + + #[pallet::call_index(17)] + #[pallet::weight(0)] + pub fn add_emission_proposal( + origin: OriginFor, + recycling_percentage: Percent, + treasury_percentage: Percent, + data: Vec, + ) -> DispatchResult { + let proposer = ensure_signed(origin)?; + proposal::add_emission_proposal::( + proposer, + recycling_percentage, + treasury_percentage, + data, + ) + } } #[pallet::event] @@ -425,6 +442,8 @@ pub mod pallet { InvalidMinWeightControlFee, /// Invalid minimum staking fee in proposal InvalidMinStakingFee, + /// Invalid params given to Emission proposal + InvalidEmissionProposalData, } } diff --git a/pallets/governance/src/proposal.rs b/pallets/governance/src/proposal.rs index 5357d50..b6716da 100644 --- a/pallets/governance/src/proposal.rs +++ b/pallets/governance/src/proposal.rs @@ -12,6 +12,7 @@ use codec::{Decode, Encode, MaxEncodedLen}; use polkadot_sdk::frame_election_provider_support::Get; use polkadot_sdk::frame_support::traits::Currency; use polkadot_sdk::frame_support::traits::WithdrawReasons; +use polkadot_sdk::polkadot_sdk_frame::traits::CheckedAdd; use polkadot_sdk::sp_runtime::SaturatedConversion; use polkadot_sdk::sp_std::{collections::btree_set::BTreeSet, vec::Vec}; use polkadot_sdk::{ @@ -24,7 +25,7 @@ use substrate_fixed::types::I92F36; pub type ProposalId = u64; -#[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen)] +#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Proposal { pub id: ProposalId, @@ -44,6 +45,13 @@ impl Proposal { matches!(self.status, ProposalStatus::Open { .. }) } + pub fn execution_block(&self) -> Block { + match self.data { + ProposalData::Emission { .. } => self.creation_block + 21_600, + _ => self.expiration_block, + } + } + /// Marks a proposal as accepted and overrides the storage value. pub fn accept( mut self, @@ -250,11 +258,15 @@ impl GlobalParamsData { } } -#[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] +#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] #[scale_info(skip_type_params(T))] pub enum ProposalData { GlobalParams(GlobalParamsData), GlobalCustom, + Emission { + recycling_percentage: Percent, + treasury_percentage: Percent, + }, TransferDaoTreasury { account: AccountIdOf, amount: BalanceOf, @@ -265,6 +277,7 @@ impl ProposalData { #[must_use] pub fn required_stake(&self) -> Percent { match self { + Self::Emission { .. } => Percent::from_parts(10), Self::GlobalCustom | Self::TransferDaoTreasury { .. } => Percent::from_parts(50), Self::GlobalParams { .. } => Percent::from_parts(40), } @@ -312,6 +325,27 @@ pub fn add_dao_treasury_transfer_proposal( add_proposal::(proposer, data, metadata) } +pub fn add_emission_proposal( + proposer: AccountIdOf, + recycling_percentage: Percent, + treasury_percentage: Percent, + metadata: Vec, +) -> DispatchResult { + ensure!( + recycling_percentage + .checked_add(&treasury_percentage) + .is_some(), + crate::Error::::InvalidEmissionProposalData + ); + + let data = ProposalData::::Emission { + recycling_percentage, + treasury_percentage, + }; + + add_proposal::(proposer, data, metadata) +} + fn add_proposal( proposer: AccountIdOf, data: ProposalData, @@ -442,7 +476,10 @@ fn tick_proposal( *stake_for = stake_for_sum; *stake_against = stake_against_sum; } - Proposals::::set(proposal.id, Some(proposal)); + Proposals::::set(proposal.id, Some(proposal.clone())); + } + + if block_number < proposal.execution_block() { return Ok(()); } @@ -450,6 +487,27 @@ fn tick_proposal( let minimal_stake_to_execute = get_minimal_stake_to_execute_with_percentage::(proposal.data.required_stake()); + if total_stake >= minimal_stake_to_execute { + create_unrewarded_proposal::(proposal.id, block_number, votes_for, votes_against); + if stake_against_sum > stake_for_sum { + proposal.refuse(block_number, stake_for_sum, stake_against_sum) + } else { + proposal.accept(block_number, stake_for_sum, stake_against_sum) + } + } else if block_number >= proposal.expiration_block { + create_unrewarded_proposal::(proposal.id, block_number, votes_for, votes_against); + proposal.expire(block_number) + } else { + Ok(()) + } +} + +fn create_unrewarded_proposal( + proposal_id: u64, + block_number: Block, + votes_for: Vec<(AccountIdOf, BalanceOf)>, + votes_against: Vec<(AccountIdOf, BalanceOf)>, +) { let mut reward_votes_for = BoundedBTreeMap::new(); for (key, value) in votes_for { reward_votes_for @@ -469,23 +527,13 @@ fn tick_proposal( } UnrewardedProposals::::insert( - proposal.id, + proposal_id, UnrewardedProposal:: { block: block_number, votes_for: reward_votes_for, votes_against: reward_votes_against, }, ); - - if total_stake >= minimal_stake_to_execute { - if stake_against_sum > stake_for_sum { - proposal.refuse(block_number, stake_for_sum, stake_against_sum) - } else { - proposal.accept(block_number, stake_for_sum, stake_against_sum) - } - } else { - proposal.expire(block_number) - } } #[inline] diff --git a/pallets/governance/tests/voting.rs b/pallets/governance/tests/voting.rs index 8a08525..171846d 100644 --- a/pallets/governance/tests/voting.rs +++ b/pallets/governance/tests/voting.rs @@ -3,7 +3,8 @@ use pallet_governance::{ proposal::{GlobalParamsData, ProposalStatus}, DaoTreasuryAddress, Error, GlobalGovernanceConfig, Proposals, }; -use polkadot_sdk::frame_support::assert_err; +use polkadot_sdk::frame_support::traits::Get; +use polkadot_sdk::{frame_support::assert_err, sp_runtime::BoundedBTreeSet}; use polkadot_sdk::{frame_support::assert_ok, sp_runtime::Percent}; use test_utils::{ add_balance, get_balance, get_origin, new_test_ext, step_block, to_nano, zero_min_burn, @@ -349,6 +350,163 @@ fn creates_treasury_transfer_proposal_and_transfers() { }); } +#[test] +fn creates_emission_proposal_and_it_runs_after_2_days() { + new_test_ext().execute_with(|| { + zero_min_burn(); + + let default_proposal_expiration: u64 = + ::DefaultProposalExpiration::get(); + + config(1, default_proposal_expiration); + + let origin = get_origin(0); + add_balance(0, to_nano(2)); + register(0, 0, 0, to_nano(1)); + pallet_torus0::TotalStake::::set(to_nano(10)); + + assert_ok!(pallet_governance::Pallet::::add_emission_proposal( + origin.clone(), + Percent::from_parts(20), + Percent::from_parts(20), + vec![b'0'; 64], + )); + + vote(0, 0, true); + + step_block(21_600); + + assert_eq!( + Proposals::::get(0).unwrap().status, + ProposalStatus::Accepted { + block: 21_600, + stake_for: to_nano(1), + stake_against: 0 + } + ); + }); +} + +#[test] +fn creates_emission_proposal_and_it_runs_before_expiration() { + new_test_ext().execute_with(|| { + zero_min_burn(); + + let default_proposal_expiration: u64 = + ::DefaultProposalExpiration::get(); + + let min_stake: u128 = ::DefaultMinAllowedStake::get(); + + config(1, default_proposal_expiration); + + let origin = get_origin(0); + add_balance(0, to_nano(2)); + register(0, 0, 0, to_nano(1) - min_stake); + pallet_torus0::TotalStake::::set(to_nano(10)); + + assert_ok!(pallet_governance::Pallet::::add_emission_proposal( + origin.clone(), + Percent::from_parts(20), + Percent::from_parts(20), + vec![b'0'; 64], + )); + + vote(0, 0, true); + + step_block(21_600); + + let mut votes_for = BoundedBTreeSet::new(); + votes_for.try_insert(0).unwrap(); + + assert_eq!( + Proposals::::get(0).unwrap().status, + ProposalStatus::Open { + votes_for, + votes_against: BoundedBTreeSet::new(), + stake_for: to_nano(1) - min_stake, + stake_against: 0 + } + ); + + stake(0, 0, min_stake); + pallet_torus0::TotalStake::::set(to_nano(10)); + + step_block(100); + + assert_eq!( + Proposals::::get(0).unwrap().status, + ProposalStatus::Accepted { + block: 21_700, + stake_for: to_nano(1), + stake_against: 0 + } + ); + }); +} + +#[test] +fn creates_emission_proposal_and_it_expires() { + new_test_ext().execute_with(|| { + zero_min_burn(); + + let default_proposal_expiration: u64 = + ::DefaultProposalExpiration::get(); + + let min_stake: u128 = ::DefaultMinAllowedStake::get(); + + config(1, default_proposal_expiration); + + let origin = get_origin(0); + add_balance(0, to_nano(2)); + register(0, 0, 0, to_nano(1) - min_stake); + pallet_torus0::TotalStake::::set(to_nano(10)); + + assert_ok!(pallet_governance::Pallet::::add_emission_proposal( + origin.clone(), + Percent::from_parts(20), + Percent::from_parts(20), + vec![b'0'; 64], + )); + + vote(0, 0, true); + + step_block(default_proposal_expiration); + + assert_eq!( + Proposals::::get(0).unwrap().status, + ProposalStatus::Expired + ); + }); +} + +#[test] +fn creates_emission_proposal_with_invalid_params_and_it_fails() { + new_test_ext().execute_with(|| { + zero_min_burn(); + + let default_proposal_expiration: u64 = + ::DefaultProposalExpiration::get(); + + let min_stake: u128 = ::DefaultMinAllowedStake::get(); + + config(1, default_proposal_expiration); + + let origin = get_origin(0); + add_balance(0, to_nano(2)); + register(0, 0, 0, to_nano(1) - min_stake); + + assert_err!( + pallet_governance::Pallet::::add_emission_proposal( + origin.clone(), + Percent::from_parts(51), + Percent::from_parts(50), + vec![b'0'; 64], + ), + Error::::InvalidEmissionProposalData + ); + }); +} + #[test] fn rewards_wont_exceed_treasury() { new_test_ext().execute_with(|| {