Referenda and Conviction Voting pallets (#10195)

* Initial draft of new referendum state machine.

* Docs

* Fixes

* Fixes

* Add conviction-voting pallet

* Basic build

* Building

* Some TODOs

* Tests building

* Add missing file

* Basic lifecycle test

* Add couple of tests

* Another test

* More tests

* Fixes

* Fixes

* Formatting

* Fixes

* Tests

* Fixes

* Fixes

* More tests

* Formatting

* First few benchmarks

* First few benchmarks

* Defered queue servicing

* More testing

* Benchmarks

* Fiddly benchmark

* Final nudge benchmarks

* Formatting

* Formatting

* Finished up benchmarks

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_referenda --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/referenda/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Events finished

* Missing file

* No GenesisConfig for Referenda

* Formatting

* Docs

* Docs

* Docs

* Per-class conviction voting

* New test & mock utils

* More tests

* Tests

* Tests finished 🎉

* Benchmarking stuff

* Fixes

* Test harness

* Test harness

* Benchmarks for Conviction=Voting

* Benchmarking pipeline complete

* Docs

* Formatting

* Remove unneeded warning

* Fix UI tests

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_conviction_voting --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/conviction-voting/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Docs

* Update frame/conviction-voting/src/vote.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* update sp-runtime version

* MEL Fixes for Referenda and Conviction Voting (#10725)

* free maxencodedlen

* more maxencodedlen

* more MEL

* more mel

* disable storage info

* More Referenda Patches (#10760)

* basic fixes

* fix benchmarking

* fix license

* prevent panic in curve math

* fmt

* bump crate versions

* Update mock.rs

Co-authored-by: Parity Bot <admin@parity.io>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Gavin Wood
2022-02-06 12:51:12 +01:00
committed by GitHub
parent 074ff19dbc
commit a6891951fb
34 changed files with 6542 additions and 106 deletions
@@ -0,0 +1,278 @@
// This file is part of Substrate.
// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! ConvictionVoting pallet benchmarking.
use super::*;
use assert_matches::assert_matches;
use frame_benchmarking::{account, benchmarks, whitelist_account};
use frame_support::{
dispatch::RawOrigin,
traits::{fungible, Currency, Get},
};
use sp_runtime::traits::Bounded;
use sp_std::collections::btree_map::BTreeMap;
use crate::Pallet as ConvictionVoting;
const SEED: u32 = 0;
/// Fill all classes as much as possible up to `MaxVotes` and return the Class with the most votes
/// ongoing.
fn fill_voting<T: Config>() -> (ClassOf<T>, BTreeMap<ClassOf<T>, Vec<IndexOf<T>>>) {
let mut r = BTreeMap::<ClassOf<T>, Vec<IndexOf<T>>>::new();
for class in T::Polls::classes().into_iter() {
for _ in 0..T::MaxVotes::get() {
match T::Polls::create_ongoing(class.clone()) {
Ok(i) => r.entry(class.clone()).or_default().push(i),
Err(()) => break,
}
}
}
let c = r.iter().max_by_key(|(_, ref v)| v.len()).unwrap().0.clone();
(c, r)
}
fn funded_account<T: Config>(name: &'static str, index: u32) -> T::AccountId {
let caller: T::AccountId = account(name, index, SEED);
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
caller
}
fn account_vote<T: Config>(b: BalanceOf<T>) -> AccountVote<BalanceOf<T>> {
let v = Vote { aye: true, conviction: Conviction::Locked1x };
AccountVote::Standard { vote: v, balance: b }
}
benchmarks! {
where_clause { where T::MaxVotes: core::fmt::Debug }
vote_new {
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let account_vote = account_vote::<T>(100u32.into());
let (class, all_polls) = fill_voting::<T>();
let polls = &all_polls[&class];
let r = polls.len() - 1;
// We need to create existing votes
for i in polls.iter().skip(1) {
ConvictionVoting::<T>::vote(RawOrigin::Signed(caller.clone()).into(), *i, account_vote.clone())?;
}
let votes = match VotingFor::<T>::get(&caller, &class) {
Voting::Casting(Casting { votes, .. }) => votes,
_ => return Err("Votes are not direct".into()),
};
assert_eq!(votes.len(), r as usize, "Votes were not recorded.");
let index = polls[0];
}: vote(RawOrigin::Signed(caller.clone()), index, account_vote)
verify {
assert_matches!(
VotingFor::<T>::get(&caller, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == (r + 1) as usize
);
}
vote_existing {
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let old_account_vote = account_vote::<T>(100u32.into());
let (class, all_polls) = fill_voting::<T>();
let polls = &all_polls[&class];
let r = polls.len();
// We need to create existing votes
for i in polls.iter() {
ConvictionVoting::<T>::vote(RawOrigin::Signed(caller.clone()).into(), *i, old_account_vote.clone())?;
}
let votes = match VotingFor::<T>::get(&caller, &class) {
Voting::Casting(Casting { votes, .. }) => votes,
_ => return Err("Votes are not direct".into()),
};
assert_eq!(votes.len(), r, "Votes were not recorded.");
let new_account_vote = account_vote::<T>(200u32.into());
let index = polls[0];
}: vote(RawOrigin::Signed(caller.clone()), index, new_account_vote)
verify {
assert_matches!(
VotingFor::<T>::get(&caller, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize
);
}
remove_vote {
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let old_account_vote = account_vote::<T>(100u32.into());
let (class, all_polls) = fill_voting::<T>();
let polls = &all_polls[&class];
let r = polls.len();
// We need to create existing votes
for i in polls.iter() {
ConvictionVoting::<T>::vote(RawOrigin::Signed(caller.clone()).into(), *i, old_account_vote.clone())?;
}
let votes = match VotingFor::<T>::get(&caller, &class) {
Voting::Casting(Casting { votes, .. }) => votes,
_ => return Err("Votes are not direct".into()),
};
assert_eq!(votes.len(), r, "Votes were not recorded.");
let index = polls[0];
}: _(RawOrigin::Signed(caller.clone()), Some(class.clone()), index)
verify {
assert_matches!(
VotingFor::<T>::get(&caller, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == (r - 1) as usize
);
}
remove_other_vote {
let caller = funded_account::<T>("caller", 0);
let voter = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let old_account_vote = account_vote::<T>(100u32.into());
let (class, all_polls) = fill_voting::<T>();
let polls = &all_polls[&class];
let r = polls.len();
// We need to create existing votes
for i in polls.iter() {
ConvictionVoting::<T>::vote(RawOrigin::Signed(voter.clone()).into(), *i, old_account_vote.clone())?;
}
let votes = match VotingFor::<T>::get(&caller, &class) {
Voting::Casting(Casting { votes, .. }) => votes,
_ => return Err("Votes are not direct".into()),
};
assert_eq!(votes.len(), r, "Votes were not recorded.");
let index = polls[0];
assert!(T::Polls::end_ongoing(index, false).is_ok());
}: _(RawOrigin::Signed(caller.clone()), voter.clone(), class.clone(), index)
verify {
assert_matches!(
VotingFor::<T>::get(&voter, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == (r - 1) as usize
);
}
delegate {
let r in 0 .. T::MaxVotes::get().min(T::Polls::max_ongoing().1);
let all_polls = fill_voting::<T>().1;
let class = T::Polls::max_ongoing().0;
let polls = &all_polls[&class];
let voter = funded_account::<T>("voter", 0);
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let delegated_balance: BalanceOf<T> = 1000u32.into();
let delegate_vote = account_vote::<T>(delegated_balance);
// We need to create existing delegations
for i in polls.iter().take(r as usize) {
ConvictionVoting::<T>::vote(RawOrigin::Signed(voter.clone()).into(), *i, delegate_vote.clone())?;
}
assert_matches!(
VotingFor::<T>::get(&voter, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize
);
}: _(RawOrigin::Signed(caller.clone()), class.clone(), voter.clone(), Conviction::Locked1x, delegated_balance)
verify {
assert_matches!(VotingFor::<T>::get(&caller, &class), Voting::Delegating(_));
}
undelegate {
let r in 0 .. T::MaxVotes::get().min(T::Polls::max_ongoing().1);
let all_polls = fill_voting::<T>().1;
let class = T::Polls::max_ongoing().0;
let polls = &all_polls[&class];
let voter = funded_account::<T>("voter", 0);
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let delegated_balance: BalanceOf<T> = 1000u32.into();
let delegate_vote = account_vote::<T>(delegated_balance);
ConvictionVoting::<T>::delegate(
RawOrigin::Signed(caller.clone()).into(),
class.clone(),
voter.clone(),
Conviction::Locked1x,
delegated_balance,
)?;
// We need to create delegations
for i in polls.iter().take(r as usize) {
ConvictionVoting::<T>::vote(RawOrigin::Signed(voter.clone()).into(), *i, delegate_vote.clone())?;
}
assert_matches!(
VotingFor::<T>::get(&voter, &class),
Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize
);
assert_matches!(VotingFor::<T>::get(&caller, &class), Voting::Delegating(_));
}: _(RawOrigin::Signed(caller.clone()), class.clone())
verify {
assert_matches!(VotingFor::<T>::get(&caller, &class), Voting::Casting(_));
}
unlock {
let caller = funded_account::<T>("caller", 0);
whitelist_account!(caller);
let normal_account_vote = account_vote::<T>(T::Currency::free_balance(&caller) - 100u32.into());
let big_account_vote = account_vote::<T>(T::Currency::free_balance(&caller));
// Fill everything up to the max by filling all classes with votes and voting on them all.
let (class, all_polls) = fill_voting::<T>();
assert!(all_polls.len() > 0);
for (class, polls) in all_polls.iter() {
assert!(polls.len() > 0);
for i in polls.iter() {
ConvictionVoting::<T>::vote(RawOrigin::Signed(caller.clone()).into(), *i, normal_account_vote.clone())?;
}
}
let orig_usable = <T::Currency as fungible::Inspect<T::AccountId>>::reducible_balance(&caller, false);
let polls = &all_polls[&class];
// Vote big on the class with the most ongoing votes of them to bump the lock and make it
// hard to recompute when removed.
ConvictionVoting::<T>::vote(RawOrigin::Signed(caller.clone()).into(), polls[0], big_account_vote.clone())?;
let now_usable = <T::Currency as fungible::Inspect<T::AccountId>>::reducible_balance(&caller, false);
assert_eq!(orig_usable - now_usable, 100u32.into());
// Remove the vote
ConvictionVoting::<T>::remove_vote(RawOrigin::Signed(caller.clone()).into(), Some(class.clone()), polls[0])?;
// We can now unlock on `class` from 200 to 100...
}: _(RawOrigin::Signed(caller.clone()), class, caller.clone())
verify {
assert_eq!(orig_usable, <T::Currency as fungible::Inspect<T::AccountId>>::reducible_balance(&caller, false));
}
impl_benchmark_test_suite!(
ConvictionVoting,
crate::tests::new_test_ext(),
crate::tests::Test
);
}
@@ -0,0 +1,131 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The conviction datatype.
use crate::types::Delegations;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Bounded, CheckedDiv, CheckedMul, Zero},
RuntimeDebug,
};
use sp_std::{convert::TryFrom, result::Result};
/// A value denoting the strength of conviction of a vote.
#[derive(
Encode,
Decode,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub enum Conviction {
/// 0.1x votes, unlocked.
None,
/// 1x votes, locked for an enactment period following a successful vote.
Locked1x,
/// 2x votes, locked for 2x enactment periods following a successful vote.
Locked2x,
/// 3x votes, locked for 4x...
Locked3x,
/// 4x votes, locked for 8x...
Locked4x,
/// 5x votes, locked for 16x...
Locked5x,
/// 6x votes, locked for 32x...
Locked6x,
}
impl Default for Conviction {
fn default() -> Self {
Conviction::None
}
}
impl From<Conviction> for u8 {
fn from(c: Conviction) -> u8 {
match c {
Conviction::None => 0,
Conviction::Locked1x => 1,
Conviction::Locked2x => 2,
Conviction::Locked3x => 3,
Conviction::Locked4x => 4,
Conviction::Locked5x => 5,
Conviction::Locked6x => 6,
}
}
}
impl TryFrom<u8> for Conviction {
type Error = ();
fn try_from(i: u8) -> Result<Conviction, ()> {
Ok(match i {
0 => Conviction::None,
1 => Conviction::Locked1x,
2 => Conviction::Locked2x,
3 => Conviction::Locked3x,
4 => Conviction::Locked4x,
5 => Conviction::Locked5x,
6 => Conviction::Locked6x,
_ => return Err(()),
})
}
}
impl Conviction {
/// The amount of time (in number of periods) that our conviction implies a successful voter's
/// balance should be locked for.
pub fn lock_periods(self) -> u32 {
match self {
Conviction::None => 0,
Conviction::Locked1x => 1,
Conviction::Locked2x => 2,
Conviction::Locked3x => 4,
Conviction::Locked4x => 8,
Conviction::Locked5x => 16,
Conviction::Locked6x => 32,
}
}
/// The votes of a voter of the given `balance` with our conviction.
pub fn votes<B: From<u8> + Zero + Copy + CheckedMul + CheckedDiv + Bounded>(
self,
capital: B,
) -> Delegations<B> {
let votes = match self {
Conviction::None => capital.checked_div(&10u8.into()).unwrap_or_else(Zero::zero),
x => capital.checked_mul(&u8::from(x).into()).unwrap_or_else(B::max_value),
};
Delegations { votes, capital }
}
}
impl Bounded for Conviction {
fn min_value() -> Self {
Conviction::None
}
fn max_value() -> Self {
Conviction::Locked6x
}
}
@@ -0,0 +1,632 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Voting Pallet
//!
//! - [`Config`]
//! - [`Call`]
//!
//! ## Overview
//!
//! Pallet for managing actual voting in polls.
#![recursion_limit = "256"]
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
ensure,
traits::{
fungible, Currency, Get, LockIdentifier, LockableCurrency, PollStatus, Polling,
ReservableCurrency, WithdrawReasons,
},
};
use sp_runtime::{
traits::{AtLeast32BitUnsigned, Saturating, Zero},
ArithmeticError, DispatchError, DispatchResult, Perbill,
};
use sp_std::prelude::*;
mod conviction;
mod types;
mod vote;
pub mod weights;
pub use conviction::Conviction;
pub use pallet::*;
pub use types::{Delegations, Tally, UnvoteScope};
pub use vote::{AccountVote, Casting, Delegating, Vote, Voting};
pub use weights::WeightInfo;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
const CONVICTION_VOTING_ID: LockIdentifier = *b"pyconvot";
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type VotingOf<T> = Voting<
BalanceOf<T>,
<T as frame_system::Config>::AccountId,
<T as frame_system::Config>::BlockNumber,
PollIndexOf<T>,
<T as Config>::MaxVotes,
>;
#[allow(dead_code)]
type DelegatingOf<T> = Delegating<
BalanceOf<T>,
<T as frame_system::Config>::AccountId,
<T as frame_system::Config>::BlockNumber,
>;
pub type TallyOf<T> = Tally<BalanceOf<T>, <T as Config>::MaxTurnout>;
pub type VotesOf<T> = BalanceOf<T>;
type PollIndexOf<T> = <<T as Config>::Polls as Polling<TallyOf<T>>>::Index;
#[cfg(feature = "runtime-benchmarks")]
type IndexOf<T> = <<T as Config>::Polls as Polling<TallyOf<T>>>::Index;
type ClassOf<T> = <<T as Config>::Polls as Polling<TallyOf<T>>>::Class;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_runtime::DispatchResult;
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + Sized {
// System level stuff.
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Currency type with which voting happens.
type Currency: ReservableCurrency<Self::AccountId>
+ LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>
+ fungible::Inspect<Self::AccountId>;
/// The implementation of the logic which conducts polls.
type Polls: Polling<TallyOf<Self>, Votes = BalanceOf<Self>, Moment = Self::BlockNumber>;
/// The maximum amount of tokens which may be used for voting. May just be
/// `Currency::total_issuance`, but you might want to reduce this in order to account for
/// funds in the system which are unable to vote (e.g. parachain auction deposits).
type MaxTurnout: Get<BalanceOf<Self>>;
/// The maximum number of concurrent votes an account may have.
///
/// Also used to compute weight, an overly large value can
/// lead to extrinsic with large weight estimation: see `delegate` for instance.
#[pallet::constant]
type MaxVotes: Get<u32>;
/// The minimum period of vote locking.
///
/// It should be no shorter than enactment period to ensure that in the case of an approval,
/// those successful voters are locked into the consequences that their votes entail.
#[pallet::constant]
type VoteLockingPeriod: Get<Self::BlockNumber>;
}
/// All voting for a particular voter in a particular voting class. We store the balance for the
/// number of votes that we have recorded.
#[pallet::storage]
pub type VotingFor<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
ClassOf<T>,
VotingOf<T>,
ValueQuery,
>;
/// The voting classes which have a non-zero lock requirement and the lock amounts which they
/// require. The actual amount locked on behalf of this pallet should always be the maximum of
/// this list.
#[pallet::storage]
pub type ClassLocksFor<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, Vec<(ClassOf<T>, BalanceOf<T>)>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An account has delegated their vote to another account. \[who, target\]
Delegated(T::AccountId, T::AccountId),
/// An \[account\] has cancelled a previous delegation operation.
Undelegated(T::AccountId),
}
#[pallet::error]
pub enum Error<T> {
/// Poll is not ongoing.
NotOngoing,
/// The given account did not vote on the poll.
NotVoter,
/// The actor has no permission to conduct the action.
NoPermission,
/// The actor has no permission to conduct the action right now but will do in the future.
NoPermissionYet,
/// The account is already delegating.
AlreadyDelegating,
/// The account currently has votes attached to it and the operation cannot succeed until
/// these are removed, either through `unvote` or `reap_vote`.
AlreadyVoting,
/// Too high a balance was provided that the account cannot afford.
InsufficientFunds,
/// The account is not currently delegating.
NotDelegating,
/// Delegation to oneself makes no sense.
Nonsense,
/// Maximum number of votes reached.
MaxVotesReached,
/// The class must be supplied since it is not easily determinable from the state.
ClassNeeded,
/// The class ID supplied is invalid.
BadClass,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Vote in a poll. If `vote.is_aye()`, the vote is to enact the proposal;
/// otherwise it is a vote to keep the status quo.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `poll_index`: The index of the poll to vote for.
/// - `vote`: The vote configuration.
///
/// Weight: `O(R)` where R is the number of polls the voter has voted on.
#[pallet::weight(T::WeightInfo::vote_new().max(T::WeightInfo::vote_existing()))]
pub fn vote(
origin: OriginFor<T>,
#[pallet::compact] poll_index: PollIndexOf<T>,
vote: AccountVote<BalanceOf<T>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::try_vote(&who, poll_index, vote)
}
/// Delegate the voting power (with some given conviction) of the sending account for a
/// particular class of polls.
///
/// The balance delegated is locked for as long as it's delegated, and thereafter for the
/// time appropriate for the conviction's lock period.
///
/// The dispatch origin of this call must be _Signed_, and the signing account must either:
/// - be delegating already; or
/// - have no voting activity (if there is, then it will need to be removed/consolidated
/// through `reap_vote` or `unvote`).
///
/// - `to`: The account whose voting the `target` account's voting power will follow.
/// - `class`: The class of polls to delegate. To delegate multiple classes, multiple calls
/// to this function are required.
/// - `conviction`: The conviction that will be attached to the delegated votes. When the
/// account is undelegated, the funds will be locked for the corresponding period.
/// - `balance`: The amount of the account's balance to be used in delegating. This must not
/// be more than the account's current balance.
///
/// Emits `Delegated`.
///
/// Weight: `O(R)` where R is the number of polls the voter delegating to has
/// voted on. Weight is initially charged as if maximum votes, but is refunded later.
// NOTE: weight must cover an incorrect voting of origin with max votes, this is ensure
// because a valid delegation cover decoding a direct voting with max votes.
#[pallet::weight(T::WeightInfo::delegate(T::MaxVotes::get()))]
pub fn delegate(
origin: OriginFor<T>,
class: ClassOf<T>,
to: T::AccountId,
conviction: Conviction,
balance: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let votes = Self::try_delegate(who, class, to, conviction, balance)?;
Ok(Some(T::WeightInfo::delegate(votes)).into())
}
/// Undelegate the voting power of the sending account for a particular class of polls.
///
/// Tokens may be unlocked following once an amount of time consistent with the lock period
/// of the conviction with which the delegation was issued.
///
/// The dispatch origin of this call must be _Signed_ and the signing account must be
/// currently delegating.
///
/// - `class`: The class of polls to remove the delegation from.
///
/// Emits `Undelegated`.
///
/// Weight: `O(R)` where R is the number of polls the voter delegating to has
/// voted on. Weight is initially charged as if maximum votes, but is refunded later.
// NOTE: weight must cover an incorrect voting of origin with max votes, this is ensure
// because a valid delegation cover decoding a direct voting with max votes.
#[pallet::weight(T::WeightInfo::undelegate(T::MaxVotes::get().into()))]
pub fn undelegate(origin: OriginFor<T>, class: ClassOf<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let votes = Self::try_undelegate(who, class)?;
Ok(Some(T::WeightInfo::undelegate(votes)).into())
}
/// Remove the lock caused prior voting/delegating which has expired within a particluar
/// class.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `class`: The class of polls to unlock.
/// - `target`: The account to remove the lock on.
///
/// Weight: `O(R)` with R number of vote of target.
#[pallet::weight(T::WeightInfo::unlock())]
pub fn unlock(
origin: OriginFor<T>,
class: ClassOf<T>,
target: T::AccountId,
) -> DispatchResult {
ensure_signed(origin)?;
Self::update_lock(&class, &target);
Ok(())
}
/// Remove a vote for a poll.
///
/// If:
/// - the poll was cancelled, or
/// - the poll is ongoing, or
/// - the poll has ended such that
/// - the vote of the account was in opposition to the result; or
/// - there was no conviction to the account's vote; or
/// - the account made a split vote
/// ...then the vote is removed cleanly and a following call to `unlock` may result in more
/// funds being available.
///
/// If, however, the poll has ended and:
/// - it finished corresponding to the vote of the account, and
/// - the account made a standard vote with conviction, and
/// - the lock period of the conviction is not over
/// ...then the lock will be aggregated into the overall account's lock, which may involve
/// *overlocking* (where the two locks are combined into a single lock that is the maximum
/// of both the amount locked and the time is it locked for).
///
/// The dispatch origin of this call must be _Signed_, and the signer must have a vote
/// registered for poll `index`.
///
/// - `index`: The index of poll of the vote to be removed.
/// - `class`: Optional parameter, if given it indicates the class of the poll. For polls
/// which have finished or are cancelled, this must be `Some`.
///
/// Weight: `O(R + log R)` where R is the number of polls that `target` has voted on.
/// Weight is calculated for the maximum number of vote.
#[pallet::weight(T::WeightInfo::remove_vote())]
pub fn remove_vote(
origin: OriginFor<T>,
class: Option<ClassOf<T>>,
index: PollIndexOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::try_remove_vote(&who, index, class, UnvoteScope::Any)
}
/// Remove a vote for a poll.
///
/// If the `target` is equal to the signer, then this function is exactly equivalent to
/// `remove_vote`. If not equal to the signer, then the vote must have expired,
/// either because the poll was cancelled, because the voter lost the poll or
/// because the conviction period is over.
///
/// The dispatch origin of this call must be _Signed_.
///
/// - `target`: The account of the vote to be removed; this account must have voted for poll
/// `index`.
/// - `index`: The index of poll of the vote to be removed.
/// - `class`: The class of the poll.
///
/// Weight: `O(R + log R)` where R is the number of polls that `target` has voted on.
/// Weight is calculated for the maximum number of vote.
#[pallet::weight(T::WeightInfo::remove_other_vote())]
pub fn remove_other_vote(
origin: OriginFor<T>,
target: T::AccountId,
class: ClassOf<T>,
index: PollIndexOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let scope = if target == who { UnvoteScope::Any } else { UnvoteScope::OnlyExpired };
Self::try_remove_vote(&target, index, Some(class), scope)?;
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
/// Actually enact a vote, if legit.
fn try_vote(
who: &T::AccountId,
poll_index: PollIndexOf<T>,
vote: AccountVote<BalanceOf<T>>,
) -> DispatchResult {
ensure!(vote.balance() <= T::Currency::free_balance(who), Error::<T>::InsufficientFunds);
T::Polls::try_access_poll(poll_index, |poll_status| {
let (tally, class) = poll_status.ensure_ongoing().ok_or(Error::<T>::NotOngoing)?;
VotingFor::<T>::try_mutate(who, &class, |voting| {
if let Voting::Casting(Casting { ref mut votes, delegations, .. }) = voting {
match votes.binary_search_by_key(&poll_index, |i| i.0) {
Ok(i) => {
// Shouldn't be possible to fail, but we handle it gracefully.
tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?;
if let Some(approve) = votes[i].1.as_standard() {
tally.reduce(approve, *delegations);
}
votes[i].1 = vote;
},
Err(i) => {
votes
.try_insert(i, (poll_index, vote))
.map_err(|()| Error::<T>::MaxVotesReached)?;
},
}
// Shouldn't be possible to fail, but we handle it gracefully.
tally.add(vote).ok_or(ArithmeticError::Overflow)?;
if let Some(approve) = vote.as_standard() {
tally.increase(approve, *delegations);
}
} else {
return Err(Error::<T>::AlreadyDelegating.into())
}
// Extend the lock to `balance` (rather than setting it) since we don't know what
// other votes are in place.
Self::extend_lock(who, &class, vote.balance());
Ok(())
})
})
}
/// Remove the account's vote for the given poll if possible. This is possible when:
/// - The poll has not finished.
/// - The poll has finished and the voter lost their direction.
/// - The poll 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,
poll_index: PollIndexOf<T>,
class_hint: Option<ClassOf<T>>,
scope: UnvoteScope,
) -> DispatchResult {
let class = class_hint
.or_else(|| Some(T::Polls::as_ongoing(poll_index)?.1))
.ok_or(Error::<T>::ClassNeeded)?;
VotingFor::<T>::try_mutate(who, class, |voting| {
if let Voting::Casting(Casting { ref mut votes, delegations, ref mut prior }) = voting {
let i = votes
.binary_search_by_key(&poll_index, |i| i.0)
.map_err(|_| Error::<T>::NotVoter)?;
let v = votes.remove(i);
T::Polls::try_access_poll(poll_index, |poll_status| match poll_status {
PollStatus::Ongoing(tally, _) => {
ensure!(matches!(scope, UnvoteScope::Any), Error::<T>::NoPermission);
// Shouldn't be possible to fail, but we handle it gracefully.
tally.remove(v.1).ok_or(ArithmeticError::Underflow)?;
if let Some(approve) = v.1.as_standard() {
tally.reduce(approve, *delegations);
}
Ok(())
},
PollStatus::Completed(end, approved) => {
if let Some((lock_periods, balance)) = v.1.locked_if(approved) {
let unlock_at = end.saturating_add(
T::VoteLockingPeriod::get().saturating_mul(lock_periods.into()),
);
let now = frame_system::Pallet::<T>::block_number();
if now < unlock_at {
ensure!(
matches!(scope, UnvoteScope::Any),
Error::<T>::NoPermissionYet
);
prior.accumulate(unlock_at, balance)
}
}
Ok(())
},
PollStatus::None => Ok(()), // Poll was cancelled.
})
} else {
Ok(())
}
})
}
/// Return the number of votes for `who`
fn increase_upstream_delegation(
who: &T::AccountId,
class: &ClassOf<T>,
amount: Delegations<BalanceOf<T>>,
) -> u32 {
VotingFor::<T>::mutate(who, class, |voting| match voting {
Voting::Delegating(Delegating { delegations, .. }) => {
// We don't support second level delegating, so we don't need to do anything more.
*delegations = delegations.saturating_add(amount);
1
},
Voting::Casting(Casting { votes, delegations, .. }) => {
*delegations = delegations.saturating_add(amount);
for &(poll_index, account_vote) in votes.iter() {
if let AccountVote::Standard { vote, .. } = account_vote {
T::Polls::access_poll(poll_index, |poll_status| {
if let PollStatus::Ongoing(tally, _) = poll_status {
tally.increase(vote.aye, amount);
}
});
}
}
votes.len() as u32
},
})
}
/// Return the number of votes for `who`
fn reduce_upstream_delegation(
who: &T::AccountId,
class: &ClassOf<T>,
amount: Delegations<BalanceOf<T>>,
) -> u32 {
VotingFor::<T>::mutate(who, class, |voting| match voting {
Voting::Delegating(Delegating { delegations, .. }) => {
// We don't support second level delegating, so we don't need to do anything more.
*delegations = delegations.saturating_sub(amount);
1
},
Voting::Casting(Casting { votes, delegations, .. }) => {
*delegations = delegations.saturating_sub(amount);
for &(poll_index, account_vote) in votes.iter() {
if let AccountVote::Standard { vote, .. } = account_vote {
T::Polls::access_poll(poll_index, |poll_status| {
if let PollStatus::Ongoing(tally, _) = poll_status {
tally.reduce(vote.aye, amount);
}
});
}
}
votes.len() as u32
},
})
}
/// Attempt to delegate `balance` times `conviction` of voting power from `who` to `target`.
///
/// Return the upstream number of votes.
fn try_delegate(
who: T::AccountId,
class: ClassOf<T>,
target: T::AccountId,
conviction: Conviction,
balance: BalanceOf<T>,
) -> Result<u32, DispatchError> {
ensure!(who != target, Error::<T>::Nonsense);
T::Polls::classes().binary_search(&class).map_err(|_| Error::<T>::BadClass)?;
ensure!(balance <= T::Currency::free_balance(&who), Error::<T>::InsufficientFunds);
let votes =
VotingFor::<T>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
let old = sp_std::mem::replace(
voting,
Voting::Delegating(Delegating {
balance,
target: target.clone(),
conviction,
delegations: Default::default(),
prior: Default::default(),
}),
);
match old {
Voting::Delegating(Delegating { .. }) => Err(Error::<T>::AlreadyDelegating)?,
Voting::Casting(Casting { votes, delegations, prior }) => {
// here we just ensure that we're currently idling with no votes recorded.
ensure!(votes.is_empty(), Error::<T>::AlreadyVoting);
voting.set_common(delegations, prior);
},
}
let votes =
Self::increase_upstream_delegation(&target, &class, conviction.votes(balance));
// Extend the lock to `balance` (rather than setting it) since we don't know what
// other votes are in place.
Self::extend_lock(&who, &class, balance);
Ok(votes)
})?;
Self::deposit_event(Event::<T>::Delegated(who, target));
Ok(votes)
}
/// Attempt to end the current delegation.
///
/// Return the number of votes of upstream.
fn try_undelegate(who: T::AccountId, class: ClassOf<T>) -> Result<u32, DispatchError> {
let votes =
VotingFor::<T>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
match sp_std::mem::replace(voting, Voting::default()) {
Voting::Delegating(Delegating {
balance,
target,
conviction,
delegations,
mut prior,
}) => {
// remove any delegation votes to our current target.
let votes = Self::reduce_upstream_delegation(
&target,
&class,
conviction.votes(balance),
);
let now = frame_system::Pallet::<T>::block_number();
let lock_periods = conviction.lock_periods().into();
prior.accumulate(
now.saturating_add(
T::VoteLockingPeriod::get().saturating_mul(lock_periods),
),
balance,
);
voting.set_common(delegations, prior);
Ok(votes)
},
Voting::Casting(_) => Err(Error::<T>::NotDelegating.into()),
}
})?;
Self::deposit_event(Event::<T>::Undelegated(who));
Ok(votes)
}
fn extend_lock(who: &T::AccountId, class: &ClassOf<T>, amount: BalanceOf<T>) {
ClassLocksFor::<T>::mutate(who, |locks| match locks.iter().position(|x| &x.0 == class) {
Some(i) => locks[i].1 = locks[i].1.max(amount),
None => locks.push((class.clone(), amount)),
});
T::Currency::extend_lock(CONVICTION_VOTING_ID, who, amount, WithdrawReasons::TRANSFER);
}
/// Rejig the lock on an account. It will never get more stringent (since that would indicate
/// a security hole) but may be reduced from what they are currently.
fn update_lock(class: &ClassOf<T>, who: &T::AccountId) {
let class_lock_needed = VotingFor::<T>::mutate(who, class, |voting| {
voting.rejig(frame_system::Pallet::<T>::block_number());
voting.locked_balance()
});
let lock_needed = ClassLocksFor::<T>::mutate(who, |locks| {
locks.retain(|x| &x.0 != class);
if !class_lock_needed.is_zero() {
locks.push((class.clone(), class_lock_needed));
}
locks.iter().map(|x| x.1).max().unwrap_or(Zero::zero())
});
if lock_needed.is_zero() {
T::Currency::remove_lock(CONVICTION_VOTING_ID, who);
} else {
T::Currency::set_lock(
CONVICTION_VOTING_ID,
who,
lock_needed,
WithdrawReasons::TRANSFER,
);
}
}
}
@@ -0,0 +1,820 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The crate's tests.
use std::collections::BTreeMap;
use super::*;
use crate as pallet_conviction_voting;
use frame_support::{
assert_noop, assert_ok, parameter_types,
traits::{ConstU32, ConstU64, Contains, Polling},
};
use sp_core::H256;
use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, IdentityLookup},
};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Voting: pallet_conviction_voting::{Pallet, Call, Storage, Event<T>},
}
);
// Test that a fitlered call can be dispatched.
pub struct BaseFilter;
impl Contains<Call> for BaseFilter {
fn contains(call: &Call) -> bool {
!matches!(call, &Call::Balances(pallet_balances::Call::set_balance { .. }))
}
}
parameter_types! {
pub BlockWeights: frame_system::limits::BlockWeights =
frame_system::limits::BlockWeights::simple_max(1_000_000);
}
impl frame_system::Config for Test {
type BaseCallFilter = BaseFilter;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Call = Call;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = Event;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
impl pallet_balances::Config for Test {
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type MaxLocks = ConstU32<10>;
type Balance = u64;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<1>;
type AccountStore = System;
type WeightInfo = ();
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TestPollState {
Ongoing(TallyOf<Test>, u8),
Completed(u64, bool),
}
use TestPollState::*;
parameter_types! {
pub static Polls: BTreeMap<u8, TestPollState> = vec![
(1, Completed(1, true)),
(2, Completed(2, false)),
(3, Ongoing(Tally::from_parts(0, 0, 0), 0)),
].into_iter().collect();
}
pub struct TestPolls;
impl Polling<TallyOf<Test>> for TestPolls {
type Index = u8;
type Votes = u64;
type Moment = u64;
type Class = u8;
fn classes() -> Vec<u8> {
vec![0, 1, 2]
}
fn as_ongoing(index: u8) -> Option<(TallyOf<Test>, Self::Class)> {
Polls::get().remove(&index).and_then(|x| {
if let TestPollState::Ongoing(t, c) = x {
Some((t, c))
} else {
None
}
})
}
fn access_poll<R>(
index: Self::Index,
f: impl FnOnce(PollStatus<&mut TallyOf<Test>, u64, u8>) -> R,
) -> R {
let mut polls = Polls::get();
let entry = polls.get_mut(&index);
let r = match entry {
Some(Ongoing(ref mut tally_mut_ref, class)) =>
f(PollStatus::Ongoing(tally_mut_ref, *class)),
Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)),
None => f(PollStatus::None),
};
Polls::set(polls);
r
}
fn try_access_poll<R>(
index: Self::Index,
f: impl FnOnce(PollStatus<&mut TallyOf<Test>, u64, u8>) -> Result<R, DispatchError>,
) -> Result<R, DispatchError> {
let mut polls = Polls::get();
let entry = polls.get_mut(&index);
let r = match entry {
Some(Ongoing(ref mut tally_mut_ref, class)) =>
f(PollStatus::Ongoing(tally_mut_ref, *class)),
Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)),
None => f(PollStatus::None),
}?;
Polls::set(polls);
Ok(r)
}
#[cfg(feature = "runtime-benchmarks")]
fn create_ongoing(class: Self::Class) -> Result<Self::Index, ()> {
let mut polls = Polls::get();
let i = polls.keys().rev().next().map_or(0, |x| x + 1);
polls.insert(i, Ongoing(Tally::default(), class));
Polls::set(polls);
Ok(i)
}
#[cfg(feature = "runtime-benchmarks")]
fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> {
let mut polls = Polls::get();
match polls.get(&index) {
Some(Ongoing(..)) => {},
_ => return Err(()),
}
let now = frame_system::Pallet::<Test>::block_number();
polls.insert(index, Completed(now, approved));
Polls::set(polls);
Ok(())
}
}
impl Config for Test {
type Event = Event;
type Currency = pallet_balances::Pallet<Self>;
type VoteLockingPeriod = ConstU64<3>;
type MaxVotes = ConstU32<3>;
type WeightInfo = ();
type MaxTurnout = frame_support::traits::TotalIssuanceOf<Balances, Self::AccountId>;
type Polls = TestPolls;
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)],
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
#[test]
fn params_should_work() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::free_balance(42), 0);
assert_eq!(Balances::total_issuance(), 210);
});
}
fn next_block() {
System::set_block_number(System::block_number() + 1);
}
#[allow(dead_code)]
fn run_to(n: u64) {
while System::block_number() < n {
next_block();
}
}
fn aye(amount: u64, conviction: u8) -> AccountVote<u64> {
let vote = Vote { aye: true, conviction: conviction.try_into().unwrap() };
AccountVote::Standard { vote, balance: amount }
}
fn nay(amount: u64, conviction: u8) -> AccountVote<u64> {
let vote = Vote { aye: false, conviction: conviction.try_into().unwrap() };
AccountVote::Standard { vote, balance: amount }
}
fn tally(index: u8) -> TallyOf<Test> {
<TestPolls as Polling<TallyOf<Test>>>::as_ongoing(index).expect("No poll").0
}
fn class(index: u8) -> u8 {
<TestPolls as Polling<TallyOf<Test>>>::as_ongoing(index).expect("No poll").1
}
#[test]
#[ignore]
#[should_panic(expected = "No poll")]
fn unknown_poll_should_panic() {
let _ = tally(0);
}
#[test]
#[ignore]
#[should_panic(expected = "No poll")]
fn completed_poll_should_panic() {
let _ = tally(1);
}
#[test]
fn basic_stuff() {
new_test_ext().execute_with(|| {
assert_eq!(tally(3), Tally::from_parts(0, 0, 0));
});
}
#[test]
fn basic_voting_works() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5)));
assert_eq!(tally(3), Tally::from_parts(10, 0, 2));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5)));
assert_eq!(tally(3), Tally::from_parts(0, 10, 2));
assert_eq!(Balances::usable_balance(1), 8);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1)));
assert_eq!(tally(3), Tally::from_parts(5, 0, 5));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1)));
assert_eq!(tally(3), Tally::from_parts(0, 5, 5));
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0)));
assert_eq!(tally(3), Tally::from_parts(1, 0, 10));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0)));
assert_eq!(tally(3), Tally::from_parts(0, 1, 10));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3));
assert_eq!(tally(3), Tally::from_parts(0, 0, 0));
assert_ok!(Voting::unlock(Origin::signed(1), class(3), 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn voting_balance_gets_locked() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5)));
assert_eq!(tally(3), Tally::from_parts(10, 0, 2));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5)));
assert_eq!(tally(3), Tally::from_parts(0, 10, 2));
assert_eq!(Balances::usable_balance(1), 8);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1)));
assert_eq!(tally(3), Tally::from_parts(5, 0, 5));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1)));
assert_eq!(tally(3), Tally::from_parts(0, 5, 5));
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0)));
assert_eq!(tally(3), Tally::from_parts(1, 0, 10));
assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0)));
assert_eq!(tally(3), Tally::from_parts(0, 1, 10));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3));
assert_eq!(tally(3), Tally::from_parts(0, 0, 0));
assert_ok!(Voting::unlock(Origin::signed(1), class(3), 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn successful_but_zero_conviction_vote_balance_can_be_unlocked() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(1, 1)));
assert_ok!(Voting::vote(Origin::signed(2), 3, nay(20, 0)));
let c = class(3);
Polls::set(vec![(3, Completed(3, false))].into_iter().collect());
assert_ok!(Voting::remove_vote(Origin::signed(2), Some(c), 3));
assert_ok!(Voting::unlock(Origin::signed(2), c, 2));
assert_eq!(Balances::usable_balance(2), 20);
});
}
#[test]
fn unsuccessful_conviction_vote_balance_can_be_unlocked() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(1, 1)));
assert_ok!(Voting::vote(Origin::signed(2), 3, nay(20, 0)));
let c = class(3);
Polls::set(vec![(3, Completed(3, false))].into_iter().collect());
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(c), 3));
assert_ok!(Voting::unlock(Origin::signed(1), c, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn successful_conviction_vote_balance_stays_locked_for_correct_time() {
new_test_ext().execute_with(|| {
for i in 1..=5 {
assert_ok!(Voting::vote(Origin::signed(i), 3, aye(10, i as u8)));
}
let c = class(3);
Polls::set(vec![(3, Completed(3, true))].into_iter().collect());
for i in 1..=5 {
assert_ok!(Voting::remove_vote(Origin::signed(i), Some(c), 3));
}
for block in 1..=(3 + 5 * 3) {
run_to(block);
for i in 1..=5 {
assert_ok!(Voting::unlock(Origin::signed(i), c, i));
let expired = block >= (3 << (i - 1)) + 3;
assert_eq!(Balances::usable_balance(i), i * 10 - if expired { 0 } else { 10 });
}
}
});
}
#[test]
fn classwise_delegation_works() {
new_test_ext().execute_with(|| {
Polls::set(
vec![
(0, Ongoing(Tally::default(), 0)),
(1, Ongoing(Tally::default(), 1)),
(2, Ongoing(Tally::default(), 2)),
(3, Ongoing(Tally::default(), 2)),
]
.into_iter()
.collect(),
);
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5));
assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 5));
assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 5));
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::vote(Origin::signed(2), 0, aye(10, 0)));
assert_ok!(Voting::vote(Origin::signed(2), 1, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(2), 2, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(3), 0, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(3), 1, aye(10, 0)));
assert_ok!(Voting::vote(Origin::signed(3), 2, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(4), 0, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(4), 1, nay(10, 0)));
assert_ok!(Voting::vote(Origin::signed(4), 2, aye(10, 0)));
// 4 hasn't voted yet
assert_eq!(
Polls::get(),
vec![
(0, Ongoing(Tally::from_parts(6, 2, 35), 0)),
(1, Ongoing(Tally::from_parts(6, 2, 35), 1)),
(2, Ongoing(Tally::from_parts(6, 2, 35), 2)),
(3, Ongoing(Tally::from_parts(0, 0, 0), 2)),
]
.into_iter()
.collect()
);
// 4 votes nay to 3.
assert_ok!(Voting::vote(Origin::signed(4), 3, nay(10, 0)));
assert_eq!(
Polls::get(),
vec![
(0, Ongoing(Tally::from_parts(6, 2, 35), 0)),
(1, Ongoing(Tally::from_parts(6, 2, 35), 1)),
(2, Ongoing(Tally::from_parts(6, 2, 35), 2)),
(3, Ongoing(Tally::from_parts(0, 6, 15), 2)),
]
.into_iter()
.collect()
);
// Redelegate for class 2 to account 3.
assert_ok!(Voting::undelegate(Origin::signed(1), 2));
assert_ok!(Voting::delegate(Origin::signed(1), 2, 3, Conviction::Locked1x, 5));
assert_eq!(
Polls::get(),
vec![
(0, Ongoing(Tally::from_parts(6, 2, 35), 0)),
(1, Ongoing(Tally::from_parts(6, 2, 35), 1)),
(2, Ongoing(Tally::from_parts(1, 7, 35), 2)),
(3, Ongoing(Tally::from_parts(0, 1, 10), 2)),
]
.into_iter()
.collect()
);
// Redelegating with a lower lock does not forget previous lock and updates correctly.
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::undelegate(Origin::signed(1), 1));
assert_ok!(Voting::undelegate(Origin::signed(1), 2));
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 3));
assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 3));
assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 3));
assert_eq!(
Polls::get(),
vec![
(0, Ongoing(Tally::from_parts(4, 2, 33), 0)),
(1, Ongoing(Tally::from_parts(4, 2, 33), 1)),
(2, Ongoing(Tally::from_parts(4, 2, 33), 2)),
(3, Ongoing(Tally::from_parts(0, 4, 13), 2)),
]
.into_iter()
.collect()
);
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
// unlock does nothing since the delegation already took place.
assert_eq!(Balances::usable_balance(1), 5);
// Redelegating with higher amount extends previous lock.
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 6));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 4);
assert_ok!(Voting::undelegate(Origin::signed(1), 1));
assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 7));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_eq!(Balances::usable_balance(1), 3);
assert_ok!(Voting::undelegate(Origin::signed(1), 2));
assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 8));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 2);
assert_eq!(
Polls::get(),
vec![
(0, Ongoing(Tally::from_parts(7, 2, 36), 0)),
(1, Ongoing(Tally::from_parts(8, 2, 37), 1)),
(2, Ongoing(Tally::from_parts(9, 2, 38), 2)),
(3, Ongoing(Tally::from_parts(0, 9, 18), 2)),
]
.into_iter()
.collect()
);
});
}
#[test]
fn redelegation_after_vote_ending_should_keep_lock() {
new_test_ext().execute_with(|| {
Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect());
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5));
assert_ok!(Voting::vote(Origin::signed(2), 0, aye(10, 1)));
Polls::set(vec![(0, Completed(1, true))].into_iter().collect());
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::delegate(Origin::signed(1), 0, 3, Conviction::Locked1x, 3));
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 5);
});
}
#[test]
fn lock_amalgamation_valid_with_multiple_removed_votes() {
new_test_ext().execute_with(|| {
Polls::set(
vec![
(0, Ongoing(Tally::default(), 0)),
(1, Ongoing(Tally::default(), 0)),
(2, Ongoing(Tally::default(), 0)),
]
.into_iter()
.collect(),
);
assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1)));
assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 1)));
assert_ok!(Voting::vote(Origin::signed(1), 2, aye(5, 2)));
assert_eq!(Balances::usable_balance(1), 0);
Polls::set(
vec![(0, Completed(1, true)), (1, Completed(1, true)), (2, Completed(1, true))]
.into_iter()
.collect(),
);
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 1));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 2));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert!(Balances::usable_balance(1) <= 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn lock_amalgamation_valid_with_multiple_delegations() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 10));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked2x, 5));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert!(Balances::usable_balance(1) <= 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn lock_amalgamation_valid_with_move_roundtrip_to_delegation() {
new_test_ext().execute_with(|| {
Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect());
assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1)));
Polls::set(vec![(0, Completed(1, true))].into_iter().collect());
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 5);
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 10));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
Polls::set(vec![(1, Ongoing(Tally::default(), 0))].into_iter().collect());
assert_ok!(Voting::vote(Origin::signed(1), 1, aye(5, 2)));
Polls::set(vec![(1, Completed(1, true))].into_iter().collect());
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 1));
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert!(Balances::usable_balance(1) <= 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn lock_amalgamation_valid_with_move_roundtrip_to_casting() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 5);
Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect());
assert_ok!(Voting::vote(Origin::signed(1), 0, aye(10, 1)));
Polls::set(vec![(0, Completed(1, true))].into_iter().collect());
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0));
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked2x, 10));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert!(Balances::usable_balance(1) <= 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn lock_aggregation_over_different_classes_with_delegation_works() {
new_test_ext().execute_with(|| {
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5));
assert_ok!(Voting::delegate(Origin::signed(1), 1, 2, Conviction::Locked2x, 5));
assert_ok!(Voting::delegate(Origin::signed(1), 2, 2, Conviction::Locked1x, 10));
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
assert_ok!(Voting::undelegate(Origin::signed(1), 1));
assert_ok!(Voting::undelegate(Origin::signed(1), 2));
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn lock_aggregation_over_different_classes_with_casting_works() {
new_test_ext().execute_with(|| {
Polls::set(
vec![
(0, Ongoing(Tally::default(), 0)),
(1, Ongoing(Tally::default(), 1)),
(2, Ongoing(Tally::default(), 2)),
]
.into_iter()
.collect(),
);
assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1)));
assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 1)));
assert_ok!(Voting::vote(Origin::signed(1), 2, aye(5, 2)));
Polls::set(
vec![(0, Completed(1, true)), (1, Completed(1, true)), (2, Completed(1, true))]
.into_iter()
.collect(),
);
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0));
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(1), 1));
assert_ok!(Voting::remove_vote(Origin::signed(1), Some(2), 2));
run_to(3);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 0);
run_to(6);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 5);
run_to(7);
assert_ok!(Voting::unlock(Origin::signed(1), 0, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 1, 1));
assert_ok!(Voting::unlock(Origin::signed(1), 2, 1));
assert_eq!(Balances::usable_balance(1), 10);
});
}
#[test]
fn errors_with_vote_work() {
new_test_ext().execute_with(|| {
assert_noop!(Voting::vote(Origin::signed(1), 0, aye(10, 0)), Error::<Test>::NotOngoing);
assert_noop!(Voting::vote(Origin::signed(1), 1, aye(10, 0)), Error::<Test>::NotOngoing);
assert_noop!(Voting::vote(Origin::signed(1), 2, aye(10, 0)), Error::<Test>::NotOngoing);
assert_noop!(
Voting::vote(Origin::signed(1), 3, aye(11, 0)),
Error::<Test>::InsufficientFunds
);
assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 10));
assert_noop!(
Voting::vote(Origin::signed(1), 3, aye(10, 0)),
Error::<Test>::AlreadyDelegating
);
assert_ok!(Voting::undelegate(Origin::signed(1), 0));
Polls::set(
vec![
(0, Ongoing(Tally::default(), 0)),
(1, Ongoing(Tally::default(), 0)),
(2, Ongoing(Tally::default(), 0)),
(3, Ongoing(Tally::default(), 0)),
]
.into_iter()
.collect(),
);
assert_ok!(Voting::vote(Origin::signed(1), 0, aye(10, 0)));
assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 0)));
assert_ok!(Voting::vote(Origin::signed(1), 2, aye(10, 0)));
assert_noop!(
Voting::vote(Origin::signed(1), 3, aye(10, 0)),
Error::<Test>::MaxVotesReached
);
});
}
#[test]
fn errors_with_delegating_work() {
new_test_ext().execute_with(|| {
assert_noop!(
Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 11),
Error::<Test>::InsufficientFunds
);
assert_noop!(
Voting::delegate(Origin::signed(1), 3, 2, Conviction::None, 10),
Error::<Test>::BadClass
);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0)));
assert_noop!(
Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 10),
Error::<Test>::AlreadyVoting
);
assert_noop!(Voting::undelegate(Origin::signed(1), 0), Error::<Test>::NotDelegating);
});
}
#[test]
fn remove_other_vote_works() {
new_test_ext().execute_with(|| {
assert_noop!(
Voting::remove_other_vote(Origin::signed(2), 1, 0, 3),
Error::<Test>::NotVoter
);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 2)));
assert_noop!(
Voting::remove_other_vote(Origin::signed(2), 1, 0, 3),
Error::<Test>::NoPermission
);
Polls::set(vec![(3, Completed(1, true))].into_iter().collect());
run_to(6);
assert_noop!(
Voting::remove_other_vote(Origin::signed(2), 1, 0, 3),
Error::<Test>::NoPermissionYet
);
run_to(7);
assert_ok!(Voting::remove_other_vote(Origin::signed(2), 1, 0, 3));
});
}
#[test]
fn errors_with_remove_vote_work() {
new_test_ext().execute_with(|| {
assert_noop!(Voting::remove_vote(Origin::signed(1), Some(0), 3), Error::<Test>::NotVoter);
assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 2)));
Polls::set(vec![(3, Completed(1, true))].into_iter().collect());
assert_noop!(Voting::remove_vote(Origin::signed(1), None, 3), Error::<Test>::ClassNeeded);
});
}
@@ -0,0 +1,236 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Miscellaneous additional datatypes.
use sp_std::marker::PhantomData;
use super::*;
use crate::{AccountVote, Conviction, Vote};
use codec::{Codec, Decode, Encode, MaxEncodedLen};
use frame_support::{
traits::VoteTally, CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound,
RuntimeDebugNoBound,
};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Saturating, Zero},
RuntimeDebug,
};
/// Info regarding an ongoing referendum.
#[derive(
CloneNoBound,
DefaultNoBound,
PartialEqNoBound,
EqNoBound,
RuntimeDebugNoBound,
TypeInfo,
Encode,
Decode,
MaxEncodedLen,
)]
#[scale_info(skip_type_params(Total))]
pub struct Tally<
Votes: Clone + Default + PartialEq + Eq + sp_std::fmt::Debug + TypeInfo + Codec,
Total,
> {
/// The number of aye votes, expressed in terms of post-conviction lock-vote.
pub ayes: Votes,
/// The number of nay votes, expressed in terms of post-conviction lock-vote.
pub nays: Votes,
/// The amount of funds currently expressing its opinion. Pre-conviction.
pub turnout: Votes,
/// Dummy.
dummy: PhantomData<Total>,
}
impl<
Votes: Clone
+ Default
+ PartialEq
+ Eq
+ sp_std::fmt::Debug
+ Copy
+ AtLeast32BitUnsigned
+ TypeInfo
+ Codec,
Total: Get<Votes>,
> VoteTally<Votes> for Tally<Votes, Total>
{
fn ayes(&self) -> Votes {
self.ayes
}
fn turnout(&self) -> Perbill {
Perbill::from_rational(self.turnout, Total::get())
}
fn approval(&self) -> Perbill {
Perbill::from_rational(self.ayes, self.ayes.saturating_add(self.nays))
}
#[cfg(feature = "runtime-benchmarks")]
fn unanimity() -> Self {
Self { ayes: Total::get(), nays: Zero::zero(), turnout: Total::get(), dummy: PhantomData }
}
#[cfg(feature = "runtime-benchmarks")]
fn from_requirements(turnout: Perbill, approval: Perbill) -> Self {
let turnout = turnout.mul_ceil(Total::get());
let ayes = approval.mul_ceil(turnout);
Self { ayes, nays: turnout - ayes, turnout, dummy: PhantomData }
}
}
impl<
Votes: Clone
+ Default
+ PartialEq
+ Eq
+ sp_std::fmt::Debug
+ Copy
+ AtLeast32BitUnsigned
+ TypeInfo
+ Codec,
Total: Get<Votes>,
> Tally<Votes, Total>
{
/// Create a new tally.
pub fn new(vote: Vote, balance: Votes) -> Self {
let Delegations { votes, capital } = vote.conviction.votes(balance);
Self {
ayes: if vote.aye { votes } else { Zero::zero() },
nays: if vote.aye { Zero::zero() } else { votes },
turnout: capital,
dummy: PhantomData,
}
}
pub fn from_parts(ayes: Votes, nays: Votes, turnout: Votes) -> Self {
Self { ayes, nays, turnout, dummy: PhantomData }
}
/// Add an account's vote into the tally.
pub fn add(&mut self, vote: AccountVote<Votes>) -> Option<()> {
match vote {
AccountVote::Standard { vote, balance } => {
let Delegations { votes, capital } = vote.conviction.votes(balance);
self.turnout = self.turnout.checked_add(&capital)?;
match vote.aye {
true => self.ayes = self.ayes.checked_add(&votes)?,
false => self.nays = self.nays.checked_add(&votes)?,
}
},
AccountVote::Split { aye, nay } => {
let aye = Conviction::None.votes(aye);
let nay = Conviction::None.votes(nay);
self.turnout = self.turnout.checked_add(&aye.capital)?.checked_add(&nay.capital)?;
self.ayes = self.ayes.checked_add(&aye.votes)?;
self.nays = self.nays.checked_add(&nay.votes)?;
},
}
Some(())
}
/// Remove an account's vote from the tally.
pub fn remove(&mut self, vote: AccountVote<Votes>) -> Option<()> {
match vote {
AccountVote::Standard { vote, balance } => {
let Delegations { votes, capital } = vote.conviction.votes(balance);
self.turnout = self.turnout.checked_sub(&capital)?;
match vote.aye {
true => self.ayes = self.ayes.checked_sub(&votes)?,
false => self.nays = self.nays.checked_sub(&votes)?,
}
},
AccountVote::Split { aye, nay } => {
let aye = Conviction::None.votes(aye);
let nay = Conviction::None.votes(nay);
self.turnout = self.turnout.checked_sub(&aye.capital)?.checked_sub(&nay.capital)?;
self.ayes = self.ayes.checked_sub(&aye.votes)?;
self.nays = self.nays.checked_sub(&nay.votes)?;
},
}
Some(())
}
/// Increment some amount of votes.
pub fn increase(&mut self, approve: bool, delegations: Delegations<Votes>) {
self.turnout = self.turnout.saturating_add(delegations.capital);
match approve {
true => self.ayes = self.ayes.saturating_add(delegations.votes),
false => self.nays = self.nays.saturating_add(delegations.votes),
}
}
/// Decrement some amount of votes.
pub fn reduce(&mut self, approve: bool, delegations: Delegations<Votes>) {
self.turnout = self.turnout.saturating_sub(delegations.capital);
match approve {
true => self.ayes = self.ayes.saturating_sub(delegations.votes),
false => self.nays = self.nays.saturating_sub(delegations.votes),
}
}
}
/// Amount of votes and capital placed in delegation for an account.
#[derive(
Encode, Decode, Default, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen,
)]
pub struct Delegations<Balance> {
/// The number of votes (this is post-conviction).
pub votes: Balance,
/// The amount of raw capital, used for the turnout.
pub capital: Balance,
}
impl<Balance: Saturating> Saturating for Delegations<Balance> {
fn saturating_add(self, o: Self) -> Self {
Self {
votes: self.votes.saturating_add(o.votes),
capital: self.capital.saturating_add(o.capital),
}
}
fn saturating_sub(self, o: Self) -> Self {
Self {
votes: self.votes.saturating_sub(o.votes),
capital: self.capital.saturating_sub(o.capital),
}
}
fn saturating_mul(self, o: Self) -> Self {
Self {
votes: self.votes.saturating_mul(o.votes),
capital: self.capital.saturating_mul(o.capital),
}
}
fn saturating_pow(self, exp: usize) -> Self {
Self { votes: self.votes.saturating_pow(exp), capital: self.capital.saturating_pow(exp) }
}
}
/// Whether an `unvote` operation is able to make actions that are not strictly always in the
/// interest of an account.
pub enum UnvoteScope {
/// Permitted to do everything.
Any,
/// Permitted to do only the changes that do not need the owner's permission.
OnlyExpired,
}
@@ -0,0 +1,254 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The vote datatype.
use crate::{Conviction, Delegations};
use codec::{Decode, Encode, EncodeLike, Input, MaxEncodedLen, Output};
use frame_support::{pallet_prelude::Get, BoundedVec};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Saturating, Zero},
RuntimeDebug,
};
use sp_std::{convert::TryFrom, prelude::*, result::Result};
/// A number of lock periods, plus a vote, one way or the other.
#[derive(Copy, Clone, Eq, PartialEq, Default, RuntimeDebug, MaxEncodedLen)]
pub struct Vote {
pub aye: bool,
pub conviction: Conviction,
}
impl Encode for Vote {
fn encode_to<T: Output + ?Sized>(&self, output: &mut T) {
output.push_byte(u8::from(self.conviction) | if self.aye { 0b1000_0000 } else { 0 });
}
}
impl EncodeLike for Vote {}
impl Decode for Vote {
fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
let b = input.read_byte()?;
Ok(Vote {
aye: (b & 0b1000_0000) == 0b1000_0000,
conviction: Conviction::try_from(b & 0b0111_1111)
.map_err(|_| codec::Error::from("Invalid conviction"))?,
})
}
}
impl TypeInfo for Vote {
type Identity = Self;
fn type_info() -> scale_info::Type {
scale_info::Type::builder()
.path(scale_info::Path::new("Vote", module_path!()))
.composite(
scale_info::build::Fields::unnamed()
.field(|f| f.ty::<u8>().docs(&["Raw vote byte, encodes aye + conviction"])),
)
}
}
/// A vote for a referendum of a particular account.
#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum AccountVote<Balance> {
/// A standard vote, one-way (approve or reject) with a given amount of conviction.
Standard { vote: Vote, balance: Balance },
/// A split vote with balances given for both ways, and with no conviction, useful for
/// parachains when voting.
Split { aye: Balance, nay: Balance },
}
impl<Balance: Saturating> AccountVote<Balance> {
/// Returns `Some` of the lock periods that the account is locked for, assuming that the
/// referendum passed iff `approved` is `true`.
pub fn locked_if(self, approved: bool) -> Option<(u32, Balance)> {
// winning side: can only be removed after the lock period ends.
match self {
AccountVote::Standard { vote: Vote { conviction: Conviction::None, .. }, .. } => None,
AccountVote::Standard { vote, balance } if vote.aye == approved =>
Some((vote.conviction.lock_periods(), balance)),
_ => None,
}
}
/// The total balance involved in this vote.
pub fn balance(self) -> Balance {
match self {
AccountVote::Standard { balance, .. } => balance,
AccountVote::Split { aye, nay } => aye.saturating_add(nay),
}
}
/// Returns `Some` with whether the vote is an aye vote if it is standard, otherwise `None` if
/// it is split.
pub fn as_standard(self) -> Option<bool> {
match self {
AccountVote::Standard { vote, .. } => Some(vote.aye),
_ => None,
}
}
}
/// A "prior" lock, i.e. a lock for some now-forgotten reason.
#[derive(
Encode,
Decode,
Default,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub struct PriorLock<BlockNumber, Balance>(BlockNumber, Balance);
impl<BlockNumber: Ord + Copy + Zero, Balance: Ord + Copy + Zero> PriorLock<BlockNumber, Balance> {
/// Accumulates an additional lock.
pub fn accumulate(&mut self, until: BlockNumber, amount: Balance) {
self.0 = self.0.max(until);
self.1 = self.1.max(amount);
}
pub fn locked(&self) -> Balance {
self.1
}
pub fn rejig(&mut self, now: BlockNumber) {
if now >= self.0 {
self.0 = Zero::zero();
self.1 = Zero::zero();
}
}
}
/// Information concerning the delegation of some voting power.
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct Delegating<Balance, AccountId, BlockNumber> {
/// The amount of balance delegated.
pub balance: Balance,
/// The account to which the voting power is delegated.
pub target: AccountId,
/// The conviction with which the voting power is delegated. When this gets undelegated, the
/// relevant lock begins.
pub conviction: Conviction,
/// The total amount of delegations that this account has received, post-conviction-weighting.
pub delegations: Delegations<Balance>,
/// Any pre-existing locks from past voting/delegating activity.
pub prior: PriorLock<BlockNumber, Balance>,
}
/// Information concerning the direct vote-casting of some voting power.
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(MaxVotes))]
pub struct Casting<Balance, BlockNumber, PollIndex, MaxVotes>
where
MaxVotes: Get<u32>,
{
/// The current votes of the account.
pub votes: BoundedVec<(PollIndex, AccountVote<Balance>), MaxVotes>,
/// The total amount of delegations that this account has received, post-conviction-weighting.
pub delegations: Delegations<Balance>,
/// Any pre-existing locks from past voting/delegating activity.
pub prior: PriorLock<BlockNumber, Balance>,
}
/// An indicator for what an account is doing; it can either be delegating or voting.
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(MaxVotes))]
pub enum Voting<Balance, AccountId, BlockNumber, PollIndex, MaxVotes>
where
MaxVotes: Get<u32>,
{
/// The account is voting directly.
Casting(Casting<Balance, BlockNumber, PollIndex, MaxVotes>),
/// The account is delegating `balance` of its balance to a `target` account with `conviction`.
Delegating(Delegating<Balance, AccountId, BlockNumber>),
}
impl<Balance: Default, AccountId, BlockNumber: Zero, PollIndex, MaxVotes> Default
for Voting<Balance, AccountId, BlockNumber, PollIndex, MaxVotes>
where
MaxVotes: Get<u32>,
{
fn default() -> Self {
Voting::Casting(Casting {
votes: Default::default(),
delegations: Default::default(),
prior: PriorLock(Zero::zero(), Default::default()),
})
}
}
impl<Balance, AccountId, BlockNumber, PollIndex, MaxVotes> AsMut<PriorLock<BlockNumber, Balance>>
for Voting<Balance, AccountId, BlockNumber, PollIndex, MaxVotes>
where
MaxVotes: Get<u32>,
{
fn as_mut(&mut self) -> &mut PriorLock<BlockNumber, Balance> {
match self {
Voting::Casting(Casting { prior, .. }) => prior,
Voting::Delegating(Delegating { prior, .. }) => prior,
}
}
}
impl<
Balance: Saturating + Ord + Zero + Copy,
BlockNumber: Ord + Copy + Zero,
AccountId,
PollIndex,
MaxVotes,
> Voting<Balance, AccountId, BlockNumber, PollIndex, MaxVotes>
where
MaxVotes: Get<u32>,
{
pub fn rejig(&mut self, now: BlockNumber) {
AsMut::<PriorLock<BlockNumber, Balance>>::as_mut(self).rejig(now);
}
/// The amount of this account's balance that much currently be locked due to voting.
pub fn locked_balance(&self) -> Balance {
match self {
Voting::Casting(Casting { votes, prior, .. }) =>
votes.iter().map(|i| i.1.balance()).fold(prior.locked(), |a, i| a.max(i)),
Voting::Delegating(Delegating { balance, prior, .. }) => *balance.max(&prior.locked()),
}
}
pub fn set_common(
&mut self,
delegations: Delegations<Balance>,
prior: PriorLock<BlockNumber, Balance>,
) {
let (d, p) = match self {
Voting::Casting(Casting { ref mut delegations, ref mut prior, .. }) =>
(delegations, prior),
Voting::Delegating(Delegating { ref mut delegations, ref mut prior, .. }) =>
(delegations, prior),
};
*d = delegations;
*p = prior;
}
}
@@ -0,0 +1,201 @@
// This file is part of Substrate.
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Autogenerated weights for pallet_conviction_voting
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2022-01-09, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
// Executed Command:
// target/release/substrate
// benchmark
// --chain=dev
// --steps=50
// --repeat=20
// --pallet=pallet_conviction_voting
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --heap-pages=4096
// --output=./frame/conviction-voting/src/weights.rs
// --template=./.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use sp_std::marker::PhantomData;
/// Weight functions needed for pallet_conviction_voting.
pub trait WeightInfo {
fn vote_new() -> Weight;
fn vote_existing() -> Weight;
fn remove_vote() -> Weight;
fn remove_other_vote() -> Weight;
fn delegate(r: u32, ) -> Weight;
fn undelegate(r: u32, ) -> Weight;
fn unlock() -> Weight;
}
/// Weights for pallet_conviction_voting using the Substrate node and recommended hardware.
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn vote_new() -> Weight {
(159_647_000 as Weight)
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().writes(6 as Weight))
}
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn vote_existing() -> Weight {
(339_851_000 as Weight)
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().writes(6 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn remove_vote() -> Weight {
(317_673_000 as Weight)
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:0)
fn remove_other_vote() -> Weight {
(52_222_000 as Weight)
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:2 w:2)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn delegate(r: u32, ) -> Weight {
(61_553_000 as Weight)
// Standard Error: 123_000
.saturating_add((33_092_000 as Weight).saturating_mul(r as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
}
// Storage: ConvictionVoting VotingFor (r:2 w:2)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn undelegate(r: u32, ) -> Weight {
(42_037_000 as Weight)
// Standard Error: 582_000
.saturating_add((32_296_000 as Weight).saturating_mul(r as Weight))
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
fn unlock() -> Weight {
(69_017_000 as Weight)
.saturating_add(T::DbWeight::get().reads(3 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn vote_new() -> Weight {
(159_647_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().writes(6 as Weight))
}
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn vote_existing() -> Weight {
(339_851_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().writes(6 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn remove_vote() -> Weight {
(317_673_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:0)
fn remove_other_vote() -> Weight {
(52_222_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
// Storage: ConvictionVoting VotingFor (r:2 w:2)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn delegate(r: u32, ) -> Weight {
(61_553_000 as Weight)
// Standard Error: 123_000
.saturating_add((33_092_000 as Weight).saturating_mul(r as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
}
// Storage: ConvictionVoting VotingFor (r:2 w:2)
// Storage: Referenda ReferendumInfoFor (r:1 w:1)
// Storage: Scheduler Agenda (r:2 w:2)
fn undelegate(r: u32, ) -> Weight {
(42_037_000 as Weight)
// Standard Error: 582_000
.saturating_add((32_296_000 as Weight).saturating_mul(r as Weight))
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight)))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight)))
}
// Storage: ConvictionVoting VotingFor (r:1 w:1)
// Storage: ConvictionVoting ClassLocksFor (r:1 w:1)
// Storage: Balances Locks (r:1 w:1)
fn unlock() -> Weight {
(69_017_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
}