diff --git a/Cargo.lock b/Cargo.lock index 2ef7e3898..00067762f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4938,7 +4938,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "247.0.0" +version = "248.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", @@ -4989,7 +4989,7 @@ dependencies = [ "pallet-collective", "pallet-currencies", "pallet-dca", - "pallet-democracy 4.2.1", + "pallet-democracy 4.3.0", "pallet-duster", "pallet-dynamic-evm-fee", "pallet-dynamic-fees", @@ -7976,7 +7976,7 @@ dependencies = [ [[package]] name = "pallet-democracy" -version = "4.2.1" +version = "4.3.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9071,7 +9071,7 @@ dependencies = [ "orml-tokens", "orml-traits", "pallet-balances", - "pallet-democracy 4.2.1", + "pallet-democracy 4.3.0", "pallet-uniques", "parity-scale-codec", "pretty_assertions", @@ -11971,7 +11971,7 @@ dependencies = [ "pallet-collective", "pallet-currencies", "pallet-dca", - "pallet-democracy 4.2.1", + "pallet-democracy 4.3.0", "pallet-duster", "pallet-dynamic-evm-fee", "pallet-dynamic-fees", diff --git a/pallets/democracy/Cargo.toml b/pallets/democracy/Cargo.toml index f3304d7cc..e81a14033 100644 --- a/pallets/democracy/Cargo.toml +++ b/pallets/democracy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-democracy" -version = "4.2.1" +version = "4.3.0" authors = ["Parity Technologies "] edition = "2021" license = "Apache-2.0" diff --git a/pallets/democracy/src/lib.rs b/pallets/democracy/src/lib.rs index 14a355812..5e6e37b47 100644 --- a/pallets/democracy/src/lib.rs +++ b/pallets/democracy/src/lib.rs @@ -341,6 +341,9 @@ pub mod pallet { /// Hooks are actions that are executed on certain events. /// Eg: on_vote type DemocracyHooks: DemocracyHooks>; + + /// Origin for anyone able to force remove a vote. + type VoteRemovalOrigin: EnsureOrigin; } /// The number of (public) proposals that have been made so far. @@ -988,7 +991,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::remove_vote(T::MaxVotes::get()))] pub fn remove_vote(origin: OriginFor, index: ReferendumIndex) -> DispatchResult { let who = ensure_signed(origin)?; - Self::try_remove_vote(&who, index, UnvoteScope::Any) + Self::try_remove_vote(&who, index, UnvoteScope::Any, false) } /// Remove a vote for a referendum. @@ -1020,7 +1023,7 @@ pub mod pallet { } else { UnvoteScope::OnlyExpired }; - Self::try_remove_vote(&target, index, scope)?; + Self::try_remove_vote(&target, index, scope, false)?; Ok(()) } @@ -1165,6 +1168,39 @@ pub mod pallet { } Ok(()) } + + /// Allow to force remove a vote for a referendum. + /// + /// Same as `remove_other_vote`, except the scope is overriden by forced flag. + /// The dispatch origin of this call must be `VoteRemovalOrigin`. + /// + /// Only allowed if the referendum is finished. + /// + /// The dispatch origin of this call must be _Signed_. + /// + /// - `target`: The account of the vote to be removed; this account must have voted for + /// referendum `index`. + /// - `index`: The index of referendum of the vote to be removed. + /// + /// Weight: `O(R + log R)` where R is the number of referenda that `target` has voted on. + /// Weight is calculated for the maximum number of vote. + #[pallet::call_index(19)] + #[pallet::weight(T::WeightInfo::remove_other_vote(T::MaxVotes::get()))] + pub fn force_remove_vote( + origin: OriginFor, + target: crate::AccountIdLookupOf, + index: crate::types::ReferendumIndex, + ) -> DispatchResult { + let who = T::VoteRemovalOrigin::ensure_origin(origin)?; + let target = T::Lookup::lookup(target)?; + let scope = if target == who { + crate::types::UnvoteScope::Any + } else { + crate::types::UnvoteScope::OnlyExpired + }; + Self::try_remove_vote(&target, index, scope, true)?; + Ok(()) + } } } @@ -1332,7 +1368,12 @@ impl Pallet { /// - The referendum has finished and the voter's lock period is up. /// /// This will generally be combined with a call to `unlock`. - fn try_remove_vote(who: &T::AccountId, ref_index: ReferendumIndex, scope: UnvoteScope) -> DispatchResult { + fn try_remove_vote( + who: &T::AccountId, + ref_index: ReferendumIndex, + scope: UnvoteScope, + forced: bool, + ) -> DispatchResult { let info = ReferendumInfoOf::::get(ref_index); VotingOf::::try_mutate(who, |voting| -> DispatchResult { if let Voting::Direct { @@ -1361,7 +1402,7 @@ impl Pallet { end.saturating_add(T::VoteLockingPeriod::get().saturating_mul(lock_periods.into())); let now = frame_system::Pallet::::block_number(); if now < unlock_at { - ensure!(matches!(scope, UnvoteScope::Any), Error::::NoPermission); + ensure!(forced || matches!(scope, UnvoteScope::Any), Error::::NoPermission); prior.accumulate(unlock_at, balance) } } else { @@ -1375,7 +1416,7 @@ impl Pallet { ); let now = frame_system::Pallet::::block_number(); if now < unlock_at { - ensure!(matches!(scope, UnvoteScope::Any), Error::::NoPermission); + ensure!(forced || matches!(scope, UnvoteScope::Any), Error::::NoPermission); prior.accumulate(unlock_at, to_lock) } } diff --git a/pallets/democracy/src/tests.rs b/pallets/democracy/src/tests.rs index df3db254c..8da5228be 100644 --- a/pallets/democracy/src/tests.rs +++ b/pallets/democracy/src/tests.rs @@ -170,6 +170,7 @@ ord_parameter_types! { pub const Four: u64 = 4; pub const Five: u64 = 5; pub const Six: u64 = 6; + pub const Seven: u64 = 7; } pub struct OneToFive; impl SortedMembers for OneToFive { @@ -211,6 +212,7 @@ impl Config for Test { type MaxProposals = ConstU32<100>; type Preimages = Preimage; type DemocracyHooks = HooksHandler; + type VoteRemovalOrigin = EnsureSignedBy; } pub fn new_test_ext() -> sp_io::TestExternalities { diff --git a/pallets/democracy/src/tests/voting.rs b/pallets/democracy/src/tests/voting.rs index d922ea7f6..17ffe9bdf 100644 --- a/pallets/democracy/src/tests/voting.rs +++ b/pallets/democracy/src/tests/voting.rs @@ -194,3 +194,61 @@ fn passing_low_turnout_voting_should_work() { assert_eq!(Balances::free_balance(42), 2); }); } + +#[test] +fn force_remove_vote_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(propose_set_balance(1, 2, 1)); + let r = 0; + assert!(Democracy::referendum_info(r).is_none()); + + // start of 2 => next referendum scheduled. + fast_forward_to(2); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, big_aye(1))); + + assert_eq!(Democracy::referendum_count(), 1); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { + ayes: 10, + nays: 0, + turnout: 10 + }, + }) + ); + + fast_forward_to(3); + + // referendum still running + assert_ok!(Democracy::referendum_status(0)); + + // not allowed to force remove a vote when referendum is ongoing + assert_noop!( + Democracy::force_remove_vote(RuntimeOrigin::signed(7), 1, 0), + Error::::NoPermission + ); + + // referendum runs during 2 and 3, ends @ start of 4. + fast_forward_to(4); + + assert_noop!(Democracy::referendum_status(0), Error::::ReferendumInvalid); + assert!(pallet_scheduler::Agenda::::get(6)[0].is_some()); + + // referendum passes and wait another two blocks for enactment. + fast_forward_to(6); + + assert_eq!(Balances::free_balance(42), 2); + + // Not allowed origin + assert_noop!(Democracy::force_remove_vote(RuntimeOrigin::signed(4), 1, 0), BadOrigin); + + // Allowed origin + assert_ok!(Democracy::force_remove_vote(RuntimeOrigin::signed(7), 1, 0)); + }); +} diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 758161f11..f30b7aedf 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "247.0.0" +version = "248.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/governance.rs b/runtime/hydradx/src/governance.rs index 670a68f5e..dd462e301 100644 --- a/runtime/hydradx/src/governance.rs +++ b/runtime/hydradx/src/governance.rs @@ -334,6 +334,8 @@ impl pallet_democracy::Config for Runtime { type PalletsOrigin = OriginCaller; type Slash = Treasury; type DemocracyHooks = pallet_staking::integrations::democracy::StakingDemocracy; + // Any single technical committee member may remove a vote. + type VoteRemovalOrigin = frame_system::EnsureSignedBy; } parameter_types! { diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 0543cc488..2e34909ec 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -113,7 +113,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 247, + spec_version: 248, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1,