diff --git a/pallets/communities/src/functions.rs b/pallets/communities/src/functions.rs index 51561dcb..f95b22c8 100644 --- a/pallets/communities/src/functions.rs +++ b/pallets/communities/src/functions.rs @@ -139,6 +139,34 @@ impl Pallet { }) } + pub(crate) fn do_remove_vote( + who: &AccountIdOf, + community_id: &CommunityIdOf, + poll_index: PollIndexOf, + ) -> DispatchResult { + T::Polls::try_access_poll(poll_index, |poll_status| { + if let Some((tally, class)) = poll_status.ensure_ongoing() { + ensure!(community_id == &class, Error::::InvalidTrack); + let vote = Self::community_vote_of(who, poll_index).ok_or(Error::::NoVoteCasted)?; + + tally.remove_vote(vote.clone().into(), vote.clone().into()); + + let reason = HoldReason::VoteCasted(poll_index).into(); + CommunityVotes::::remove(who, poll_index); + + match vote { + Vote::AssetBalance(_, asset_id, amount) => { + T::Assets::release(asset_id.clone(), &reason, who, amount, Precision::BestEffort).map(|_| ()) + } + Vote::NativeBalance(..) => T::Balances::thaw(&reason, who), + _ => Ok(()), + } + } else { + Err(Error::::NotOngoing.into()) + } + }) + } + fn do_lock_for_vote(who: &AccountIdOf, poll_index: &PollIndexOf, vote: &VoteOf) -> DispatchResult { let reason = HoldReason::VoteCasted(*poll_index).into(); CommunityVotes::::insert(who.clone(), poll_index, vote.clone()); diff --git a/pallets/communities/src/lib.rs b/pallets/communities/src/lib.rs index 81ed8e85..a3ab1eb0 100644 --- a/pallets/communities/src/lib.rs +++ b/pallets/communities/src/lib.rs @@ -353,6 +353,9 @@ pub mod pallet { /// The vote type does not correspond with the community's selected /// [`DecisionMethod`][`origin::DecisionMethod`] InvalidVoteType, + /// The signer tried to remove a vote from a poll they haven't + /// casted a vote yet, or they have already removed it from. + NoVoteCasted, /// The poll NoLocksInPlace, } @@ -537,6 +540,20 @@ pub mod pallet { /// #[pallet::call_index(8)] + pub fn remove_vote( + origin: OriginFor, + membership_id: MembershipIdOf, + #[pallet::compact] poll_index: PollIndexOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let _info = T::MemberMgmt::get_membership(membership_id.clone(), &who).ok_or(Error::::NotAMember)?; + let community_id = CommunityIdOf::::from(membership_id); + + Self::do_remove_vote(&who, &community_id, poll_index) + } + + /// + #[pallet::call_index(9)] pub fn unlock(origin: OriginFor, #[pallet::compact] poll_index: PollIndexOf) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::::AlreadyOngoing); diff --git a/pallets/communities/src/tests/governance.rs b/pallets/communities/src/tests/governance.rs index 841eff06..c6016012 100644 --- a/pallets/communities/src/tests/governance.rs +++ b/pallets/communities/src/tests/governance.rs @@ -835,6 +835,107 @@ mod vote { } } +mod remove_vote { + use frame_support::traits::{fungible::Inspect, Polling}; + + use super::*; + + #[test] + fn fails_if_not_a_member() { + new_test_ext().execute_with(|| { + assert_noop!( + Communities::remove_vote(RuntimeOrigin::signed(BOB), MembershipId(COMMUNITY_A, 2), 0,), + Error::NotAMember + ); + }); + } + + #[test] + fn fails_if_trying_to_remove_vote_from_invalid_track() { + new_test_ext().execute_with(|| { + assert_noop!( + Communities::remove_vote(RuntimeOrigin::signed(ALICE), MembershipId(COMMUNITY_A, 1), 1), + Error::InvalidTrack + ); + }); + } + + #[test] + fn fails_if_poll_is_no_vote_casted() { + new_test_ext().execute_with(|| { + assert_noop!( + Communities::remove_vote(RuntimeOrigin::signed(ALICE), MembershipId(COMMUNITY_A, 1), 0), + Error::NoVoteCasted + ); + }); + } + + #[test] + fn it_works() { + new_test_ext().execute_with(|| { + assert_ok!(Communities::vote( + RuntimeOrigin::signed(ALICE), + MembershipId(COMMUNITY_A, 1), + 0, + Vote::Standard(true) + )); + + tick_block(); + + assert_ok!(Communities::remove_vote( + RuntimeOrigin::signed(ALICE), + MembershipId(COMMUNITY_A, 1), + 0 + )); + + assert_eq!( + Referenda::as_ongoing(0).expect("we already created poll 0; qed").0, + Tally::default() + ); + }); + + new_test_ext().execute_with(|| { + assert_ok!(Communities::vote( + RuntimeOrigin::signed(ALICE), + MembershipId(COMMUNITY_C, 1), + 2, + Vote::NativeBalance(true, 15) + )); + + assert_eq!( + Balances::reducible_balance( + &ALICE, + frame_support::traits::tokens::Preservation::Expendable, + frame_support::traits::tokens::Fortitude::Polite + ), + 0 + ); + + tick_block(); + + assert_ok!(Communities::remove_vote( + RuntimeOrigin::signed(ALICE), + MembershipId(COMMUNITY_C, 1), + 2 + )); + + assert_eq!( + Referenda::as_ongoing(2).expect("we already created poll 2; qed").0, + Tally::default() + ); + + assert_eq!( + Balances::reducible_balance( + &ALICE, + frame_support::traits::tokens::Preservation::Expendable, + frame_support::traits::tokens::Fortitude::Polite + ), + 7 + ); + }); + } +} + mod unlock { use super::*; diff --git a/pallets/communities/src/weights.rs b/pallets/communities/src/weights.rs index 3fe99538..076fd9e7 100644 --- a/pallets/communities/src/weights.rs +++ b/pallets/communities/src/weights.rs @@ -42,6 +42,7 @@ pub trait WeightInfo { fn remove_member() -> Weight; fn set_decision_method () -> Weight; fn vote() -> Weight; + fn remove_vote() -> Weight; fn unlock() -> Weight; } @@ -136,6 +137,17 @@ impl WeightInfo for SubstrateWeight { .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: Communities Something (r:0 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn remove_vote() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Communities Something (r:0 w:1) /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) fn unlock() -> Weight { @@ -238,6 +250,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: Communities Something (r:0 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn remove_vote() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Communities Something (r:0 w:1) /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) fn unlock() -> Weight {