Society pallet (#4170)

* Introduce efficient Hash-based RNG streamer

* Initial draft of the society module

* Introduce a test

* Dual-pot logic

* Vouching

* Use chacha

* Half way through moving to cliff payout.

* Fixes

* Add some tests

* Remove printlns

* Merge remote-tracking branch 'origin/gav-verified-id' into gav-verified-id

# Conflicts:
#	frame/identity/src/lib.rs

* Merge remote-tracking branch 'origin/gav-verified-id' into gav-verified-id

# Conflicts:
#	frame/identity/src/lib.rs

* Fix `slash_payout`, add test

* Test for multi-slash_payout

* Add docs to `put_bid` function and `bidding_works` test

* Add strikes to test

* Add comments to `rotate_period`

* Implement `suspend_member`

* Off chain iteration of suspended members using linked_map

* Half of suspended candidate

* Finish suspend_candidate, need tests

* Resolve mistakes and feedback, add `suspended_candidate_rejected` test

* Remove logic which increases payout time after un-suspension

* Fix error in `slash_suspended_candidates`, add member check to `vote`

* Fix vouch rewards, dont create zero payouts, add tests for vouch

* Test unvouch

* Unbid tests

* Add lifecycle events, fix `add_member` to update `MembershipChanged`

* Head cannot be removed from society

* Use `add_member` in `found` to ensure `MembershipChanged` is triggered

* Use `Judgement` enum for suspended candidate judgement

* Make society instantiable

* Implement challenges

* Remove extra text in test

* Remove `BlockNumber` return from `slash_payout`

* Add bad vote causes slash test

* Update frame/society/src/lib.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* Add consts to module metadata

* Check `suspended_member` cant bid

* Increase strength of payout check, **must** be a member.

* Start pallet documentation

* Finish docs

* Update library names, use decl_error

* Prevent double bids, add test

* Use `map` for vouching member, and introduce banned vouchers

* Remove leftover docs

* Vouching handles removed member lifecycles w/ tests

* `take` the votes when tallying, add comprehensive checks before vouch or bid

* Check votes are cleaned up

* Check vote is for a valid candidate, add vote event

* Defender vote event

* Fix `judge_suspended_candidate`, add weight docs

* First pass fixes (blank lines, formatting, no operational)

* Bump copyright year

* Make `add_member` infallible

* More feedback updates

* Add storage access complexity

* Fix logic for AutoUnbid

* Complete weight documentation

* Optimize logic which used to result in double storage read.

* Use Bid struct rather than tuple

* Introduce `MaxMembers` configuration constant

* Add comment about fringe scenario where `MaxMembers` could go over, NBD

* Change MaxMembership to configurable storage item with ability for root to update

* Make membership challenges skew toward failure.

If no one at all votes, or the vote is tied, the user will be suspended from society. This means, that the user simply needs to vote for themselves to stay in society assuming no one else participates.

* Refactor `is_candidate`as to avoid possible double storage reads in the future.

* Blank lines

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: thiolliere <gui.thiolliere@gmail.com>
This commit is contained in:
Gavin Wood
2020-01-10 10:55:16 +01:00
committed by GitHub
parent fd6b29dd2c
commit c81e9df162
7 changed files with 2511 additions and 1 deletions
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "pallet-society"
version = "2.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
serde = { version = "1.0.101", optional = true }
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
sp-io ={ path = "../../primitives/io", default-features = false }
sp-runtime = { version = "2.0.0", default-features = false, path = "../../primitives/runtime" }
sp-std = { version = "2.0.0", default-features = false, path = "../../primitives/std" }
frame-support = { version = "2.0.0", default-features = false, path = "../support" }
frame-system = { version = "2.0.0", default-features = false, path = "../system" }
rand_chacha = { version = "0.2", default-features = false }
[dev-dependencies]
sp-core = { version = "2.0.0", path = "../../primitives/core" }
pallet-balances = { version = "2.0.0", path = "../balances" }
[features]
default = ["std"]
std = [
"codec/std",
"serde",
"sp-io/std",
"sp-runtime/std",
"rand_chacha/std",
"sp-std/std",
"frame-support/std",
"frame-system/std",
]
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Test utilities
use super::*;
use frame_support::{impl_outer_origin, parameter_types};
use sp_core::H256;
// The testing primitives are very useful for avoiding having to work with signatures
// or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
use sp_runtime::{
Perbill, traits::{BlakeTwo256, IdentityLookup, OnInitialize, OnFinalize}, testing::Header,
};
use frame_system::EnsureSignedBy;
impl_outer_origin! {
pub enum Origin for Test {}
}
// For testing the module, we construct most of a mock runtime. This means
// first constructing a configuration type (`Test`) which `impl`s each of the
// configuration traits of modules we want to use.
#[derive(Clone, Eq, PartialEq)]
pub struct Test;
parameter_types! {
pub const CandidateDeposit: u64 = 25;
pub const WrongSideDeduction: u64 = 2;
pub const MaxStrikes: u32 = 2;
pub const RotationPeriod: u64 = 4;
pub const PeriodSpend: u64 = 1000;
pub const MaxLockDuration: u64 = 100;
pub const FounderSetAccount: u64 = 1;
pub const SuspensionJudgementSetAccount: u64 = 2;
pub const ChallengePeriod: u64 = 8;
pub const MaxMembers: u32 = 100;
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: u32 = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
pub const ExistentialDeposit: u64 = 0;
pub const TransferFee: u64 = 0;
pub const CreationFee: u64 = 0;
}
impl frame_system::Trait for Test {
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Call = ();
type Hashing = BlakeTwo256;
type AccountId = u128;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = ();
type BlockHashCount = BlockHashCount;
type MaximumBlockWeight = MaximumBlockWeight;
type MaximumBlockLength = MaximumBlockLength;
type AvailableBlockRatio = AvailableBlockRatio;
type Version = ();
type ModuleToIndex = ();
}
impl pallet_balances::Trait for Test {
type Balance = u64;
type OnFreeBalanceZero = ();
type OnNewAccount = ();
type Event = ();
type TransferPayment = ();
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type TransferFee = TransferFee;
type CreationFee = CreationFee;
}
impl Trait for Test {
type Event = ();
type Currency = pallet_balances::Module<Self>;
type Randomness = ();
type CandidateDeposit = CandidateDeposit;
type WrongSideDeduction = WrongSideDeduction;
type MaxStrikes = MaxStrikes;
type PeriodSpend = PeriodSpend;
type MembershipChanged = ();
type RotationPeriod = RotationPeriod;
type MaxLockDuration = MaxLockDuration;
type FounderOrigin = EnsureSignedBy<FounderSetAccount, u128>;
type SuspensionJudgementOrigin = EnsureSignedBy<SuspensionJudgementSetAccount, u128>;
type ChallengePeriod = ChallengePeriod;
}
pub type Society = Module<Test>;
pub type System = frame_system::Module<Test>;
pub type Balances = pallet_balances::Module<Test>;
pub struct EnvBuilder {
members: Vec<u128>,
balance: u64,
balances: Vec<(u128, u64)>,
pot: u64,
max_members: u32,
}
impl EnvBuilder {
pub fn new() -> Self {
Self {
members: vec![10],
balance: 10_000,
balances: vec![
(10, 50),
(20, 50),
(30, 50),
(40, 50),
(50, 50),
(60, 50),
],
pot: 0,
max_members: 100,
}
}
pub fn execute<R, F: FnOnce() -> R>(mut self, f: F) -> R {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
self.balances.push((Society::account_id(), self.balance.max(self.pot)));
pallet_balances::GenesisConfig::<Test> {
balances: self.balances,
vesting: vec![],
}.assimilate_storage(&mut t).unwrap();
GenesisConfig::<Test>{
members: self.members,
pot: self.pot,
max_members: self.max_members,
}.assimilate_storage(&mut t).unwrap();
let mut ext: sp_io::TestExternalities = t.into();
ext.execute_with(f)
}
#[allow(dead_code)]
pub fn with_members(mut self, m: Vec<u128>) -> Self {
self.members = m;
self
}
#[allow(dead_code)]
pub fn with_balances(mut self, b: Vec<(u128, u64)>) -> Self {
self.balances = b;
self
}
#[allow(dead_code)]
pub fn with_pot(mut self, p: u64) -> Self {
self.pot = p;
self
}
#[allow(dead_code)]
pub fn with_balance(mut self, b: u64) -> Self {
self.balance = b;
self
}
#[allow(dead_code)]
pub fn with_max_members(mut self, n: u32) -> Self {
self.max_members = n;
self
}
}
/// Run until a particular block.
pub fn run_to_block(n: u64) {
while System::block_number() < n {
if System::block_number() > 1 {
System::on_finalize(System::block_number());
}
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
Society::on_initialize(System::block_number());
}
}
/// Creates a bid struct using input parameters.
pub fn create_bid<AccountId, Balance>(
value: Balance,
who: AccountId,
kind: BidKind<AccountId, Balance>
) -> Bid<AccountId, Balance>
{
Bid {
who,
kind,
value
}
}
+744
View File
@@ -0,0 +1,744 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the module.
use super::*;
use mock::*;
use frame_support::{assert_ok, assert_noop};
use sp_runtime::traits::BadOrigin;
#[test]
fn founding_works() {
EnvBuilder::new().with_members(vec![]).execute(|| {
// Account 1 is set as the founder origin
// Account 5 cannot start a society
assert_noop!(Society::found(Origin::signed(5), 20), BadOrigin);
// Account 1 can start a society, where 10 is the founding member
assert_ok!(Society::found(Origin::signed(1), 10));
// Society members only include 10
assert_eq!(Society::members(), vec![10]);
// 10 is the head of the society
assert_eq!(Society::head(), Some(10));
// Cannot start another society
assert_noop!(Society::found(Origin::signed(1), 20), Error::<Test, _>::AlreadyFounded);
});
}
#[test]
fn basic_new_member_works() {
EnvBuilder::new().execute(|| {
assert_eq!(Balances::free_balance(20), 50);
// Bid causes Candidate Deposit to be reserved.
assert_ok!(Society::bid(Origin::signed(20), 0));
assert_eq!(Balances::free_balance(20), 25);
assert_eq!(Balances::reserved_balance(20), 25);
// Rotate period every 4 blocks
run_to_block(4);
// 20 is now a candidate
assert_eq!(Society::candidates(), vec![create_bid(0, 20, BidKind::Deposit(25))]);
// 10 (a member) can vote for the candidate
assert_ok!(Society::vote(Origin::signed(10), 20, true));
// Rotate period every 4 blocks
run_to_block(8);
// 20 is now a member of the society
assert_eq!(Society::members(), vec![10, 20]);
// Reserved balance is returned
assert_eq!(Balances::free_balance(20), 50);
assert_eq!(Balances::reserved_balance(20), 0);
});
}
#[test]
fn bidding_works() {
EnvBuilder::new().execute(|| {
// Users make bids of various amounts
assert_ok!(Society::bid(Origin::signed(60), 1900));
assert_ok!(Society::bid(Origin::signed(50), 500));
assert_ok!(Society::bid(Origin::signed(40), 400));
assert_ok!(Society::bid(Origin::signed(30), 300));
// Rotate period
run_to_block(4);
// Pot is 1000 after "PeriodSpend"
assert_eq!(Society::pot(), 1000);
assert_eq!(Balances::free_balance(Society::account_id()), 10_000);
// Choose smallest bidding users whose total is less than pot
assert_eq!(Society::candidates(), vec![
create_bid(300, 30, BidKind::Deposit(25)),
create_bid(400, 40, BidKind::Deposit(25)),
]);
// A member votes for these candidates to join the society
assert_ok!(Society::vote(Origin::signed(10), 30, true));
assert_ok!(Society::vote(Origin::signed(10), 40, true));
run_to_block(8);
// Candidates become members after a period rotation
assert_eq!(Society::members(), vec![10, 30, 40]);
// Pot is increased by 1000, but pays out 700 to the members
assert_eq!(Balances::free_balance(Society::account_id()), 9_300);
assert_eq!(Society::pot(), 1_300);
// Left over from the original bids is 50 who satisfies the condition of bid less than pot.
assert_eq!(Society::candidates(), vec![ create_bid(500, 50, BidKind::Deposit(25)) ]);
// 40, now a member, can vote for 50
assert_ok!(Society::vote(Origin::signed(40), 50, true));
run_to_block(12);
// 50 is now a member
assert_eq!(Society::members(), vec![10, 30, 40, 50]);
// Pot is increased by 1000, and 500 is paid out. Total payout so far is 1200.
assert_eq!(Society::pot(), 1_800);
assert_eq!(Balances::free_balance(Society::account_id()), 8_800);
// No more candidates satisfy the requirements
assert_eq!(Society::candidates(), vec![]);
assert_ok!(Society::defender_vote(Origin::signed(10), true)); // Keep defender around
// Next period
run_to_block(16);
// Same members
assert_eq!(Society::members(), vec![10, 30, 40, 50]);
// Pot is increased by 1000 again
assert_eq!(Society::pot(), 2_800);
// No payouts
assert_eq!(Balances::free_balance(Society::account_id()), 8_800);
// Candidate 60 now qualifies based on the increased pot size.
assert_eq!(Society::candidates(), vec![ create_bid(1900, 60, BidKind::Deposit(25)) ]);
// Candidate 60 is voted in.
assert_ok!(Society::vote(Origin::signed(50), 60, true));
run_to_block(20);
// 60 joins as a member
assert_eq!(Society::members(), vec![10, 30, 40, 50, 60]);
// Pay them
assert_eq!(Society::pot(), 1_900);
assert_eq!(Balances::free_balance(Society::account_id()), 6_900);
});
}
#[test]
fn unbidding_works() {
EnvBuilder::new().execute(|| {
// 20 and 30 make bids
assert_ok!(Society::bid(Origin::signed(20), 1000));
assert_ok!(Society::bid(Origin::signed(30), 0));
// Balances are reserved
assert_eq!(Balances::free_balance(30), 25);
assert_eq!(Balances::reserved_balance(30), 25);
// Must know right position to unbid + cannot unbid someone else
assert_noop!(Society::unbid(Origin::signed(30), 1), Error::<Test, _>::BadPosition);
// Can unbid themselves with the right position
assert_ok!(Society::unbid(Origin::signed(30), 0));
// Balance is returned
assert_eq!(Balances::free_balance(30), 50);
assert_eq!(Balances::reserved_balance(30), 0);
// 20 wins candidacy
run_to_block(4);
assert_eq!(Society::candidates(), vec![ create_bid(1000, 20, BidKind::Deposit(25)) ]);
});
}
#[test]
fn payout_works() {
EnvBuilder::new().execute(|| {
// Original balance of 50
assert_eq!(Balances::free_balance(20), 50);
assert_ok!(Society::bid(Origin::signed(20), 1000));
run_to_block(4);
assert_ok!(Society::vote(Origin::signed(10), 20, true));
run_to_block(8);
// payout not ready
assert_noop!(Society::payout(Origin::signed(20)), Error::<Test, _>::NoPayout);
run_to_block(9);
// payout should be here
assert_ok!(Society::payout(Origin::signed(20)));
assert_eq!(Balances::free_balance(20), 1050);
});
}
#[test]
fn basic_new_member_skeptic_works() {
EnvBuilder::new().execute(|| {
assert_eq!(Strikes::<Test>::get(10), 0);
assert_ok!(Society::bid(Origin::signed(20), 0));
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(0, 20, BidKind::Deposit(25))]);
run_to_block(8);
assert_eq!(Society::members(), vec![10]);
assert_eq!(Strikes::<Test>::get(10), 1);
});
}
#[test]
fn basic_new_member_reject_works() {
EnvBuilder::new().execute(|| {
// Starting Balance
assert_eq!(Balances::free_balance(20), 50);
// 20 makes a bid
assert_ok!(Society::bid(Origin::signed(20), 0));
assert_eq!(Balances::free_balance(20), 25);
assert_eq!(Balances::reserved_balance(20), 25);
// Rotation Period
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(0, 20, BidKind::Deposit(25))]);
// We say no
assert_ok!(Society::vote(Origin::signed(10), 20, false));
run_to_block(8);
// User is not added as member
assert_eq!(Society::members(), vec![10]);
// User is suspended
assert_eq!(Society::candidates(), vec![]);
assert_eq!(Society::suspended_candidate(20).is_some(), true);
});
}
#[test]
fn slash_payout_works() {
EnvBuilder::new().execute(|| {
assert_eq!(Balances::free_balance(20), 50);
assert_ok!(Society::bid(Origin::signed(20), 1000));
run_to_block(4);
assert_ok!(Society::vote(Origin::signed(10), 20, true));
run_to_block(8);
// payout in queue
assert_eq!(Payouts::<Test>::get(20), vec![(9, 1000)]);
assert_noop!(Society::payout(Origin::signed(20)), Error::<Test, _>::NoPayout);
// slash payout
assert_eq!(Society::slash_payout(&20, 500), 500);
assert_eq!(Payouts::<Test>::get(20), vec![(9, 500)]);
run_to_block(9);
// payout should be here, but 500 less
assert_ok!(Society::payout(Origin::signed(20)));
assert_eq!(Balances::free_balance(20), 550);
});
}
#[test]
fn slash_payout_multi_works() {
EnvBuilder::new().execute(|| {
assert_eq!(Balances::free_balance(20), 50);
// create a few payouts
Society::bump_payout(&20, 5, 100);
Society::bump_payout(&20, 10, 100);
Society::bump_payout(&20, 15, 100);
Society::bump_payout(&20, 20, 100);
// payouts in queue
assert_eq!(Payouts::<Test>::get(20), vec![(5, 100), (10, 100), (15, 100), (20, 100)]);
// slash payout
assert_eq!(Society::slash_payout(&20, 250), 250);
assert_eq!(Payouts::<Test>::get(20), vec![(15, 50), (20, 100)]);
// slash again
assert_eq!(Society::slash_payout(&20, 50), 50);
assert_eq!(Payouts::<Test>::get(20), vec![(20, 100)]);
});
}
#[test]
fn suspended_member_lifecycle_works() {
EnvBuilder::new().execute(|| {
// Add 20 to members, who is not the head and can be suspended/removed.
assert_ok!(Society::add_member(&20));
assert_eq!(<Members<Test>>::get(), vec![10, 20]);
assert_eq!(Strikes::<Test>::get(20), 0);
assert_eq!(<SuspendedMembers<Test>>::get(20), None);
// Let's suspend account 20 by giving them 2 strikes by not voting
assert_ok!(Society::bid(Origin::signed(30), 0));
run_to_block(8);
assert_eq!(Strikes::<Test>::get(20), 1);
assert_ok!(Society::bid(Origin::signed(40), 0));
run_to_block(16);
// Strike 2 is accumulated, and 20 is suspended :(
assert_eq!(<SuspendedMembers<Test>>::get(20), Some(()));
assert_eq!(<Members<Test>>::get(), vec![10]);
// Suspended members cannot get payout
Society::bump_payout(&20, 10, 100);
assert_noop!(Society::payout(Origin::signed(20)), Error::<Test, _>::NotMember);
// Normal people cannot make judgement
assert_noop!(Society::judge_suspended_member(Origin::signed(20), 20, true), BadOrigin);
// Suspension judgment origin can judge thee
// Suspension judgement origin forgives the suspended member
assert_ok!(Society::judge_suspended_member(Origin::signed(2), 20, true));
assert_eq!(<SuspendedMembers<Test>>::get(20), None);
assert_eq!(<Members<Test>>::get(), vec![10, 20]);
// Let's suspend them again, directly
Society::suspend_member(&20);
assert_eq!(<SuspendedMembers<Test>>::get(20), Some(()));
// Suspension judgement origin does not forgive the suspended member
assert_ok!(Society::judge_suspended_member(Origin::signed(2), 20, false));
// Cleaned up
assert_eq!(<SuspendedMembers<Test>>::get(20), None);
assert_eq!(<Members<Test>>::get(), vec![10]);
assert_eq!(<Payouts<Test>>::get(20), vec![]);
});
}
#[test]
fn suspended_candidate_rejected_works() {
EnvBuilder::new().execute(|| {
// Starting Balance
assert_eq!(Balances::free_balance(20), 50);
assert_eq!(Balances::free_balance(Society::account_id()), 10000);
// 20 makes a bid
assert_ok!(Society::bid(Origin::signed(20), 0));
assert_eq!(Balances::free_balance(20), 25);
assert_eq!(Balances::reserved_balance(20), 25);
// Rotation Period
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(0, 20, BidKind::Deposit(25))]);
// We say no
assert_ok!(Society::vote(Origin::signed(10), 20, false));
run_to_block(8);
// User is not added as member
assert_eq!(Society::members(), vec![10]);
// User is suspended
assert_eq!(Society::candidates(), vec![]);
assert_eq!(Society::suspended_candidate(20).is_some(), true);
// Normal user cannot make judgement on suspended candidate
assert_noop!(Society::judge_suspended_candidate(Origin::signed(20), 20, Judgement::Approve), BadOrigin);
// Suspension judgement origin makes no direct judgement
assert_ok!(Society::judge_suspended_candidate(Origin::signed(2), 20, Judgement::Rebid));
// They are placed back in bid pool, repeat suspension process
// Rotation Period
run_to_block(12);
assert_eq!(Society::candidates(), vec![create_bid(0, 20, BidKind::Deposit(25))]);
// We say no
assert_ok!(Society::vote(Origin::signed(10), 20, false));
run_to_block(16);
// User is not added as member
assert_eq!(Society::members(), vec![10]);
// User is suspended
assert_eq!(Society::candidates(), vec![]);
assert_eq!(Society::suspended_candidate(20).is_some(), true);
// Suspension judgement origin rejects the candidate
assert_ok!(Society::judge_suspended_candidate(Origin::signed(2), 20, Judgement::Reject));
// User is slashed
assert_eq!(Balances::free_balance(20), 25);
assert_eq!(Balances::reserved_balance(20), 0);
// Funds are deposited to society account
assert_eq!(Balances::free_balance(Society::account_id()), 10025);
// Cleaned up
assert_eq!(Society::candidates(), vec![]);
assert_eq!(<SuspendedCandidates<Test>>::get(20), None);
});
}
#[test]
fn vouch_works() {
EnvBuilder::new().execute(|| {
// 10 is the only member
assert_eq!(Society::members(), vec![10]);
// A non-member cannot vouch
assert_noop!(Society::vouch(Origin::signed(1), 20, 1000, 100), Error::<Test, _>::NotMember);
// A member can though
assert_ok!(Society::vouch(Origin::signed(10), 20, 1000, 100));
assert_eq!(<Vouching<Test>>::get(10), Some(VouchingStatus::Vouching));
// A member cannot vouch twice at the same time
assert_noop!(Society::vouch(Origin::signed(10), 30, 100, 0), Error::<Test, _>::AlreadyVouching);
// Vouching creates the right kind of bid
assert_eq!(<Bids<Test>>::get(), vec![create_bid(1000, 20, BidKind::Vouch(10, 100))]);
// Vouched user can become candidate
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(1000, 20, BidKind::Vouch(10, 100))]);
// Vote yes
assert_ok!(Society::vote(Origin::signed(10), 20, true));
// Vouched user can win
run_to_block(8);
assert_eq!(Society::members(), vec![10, 20]);
// Voucher wins a portion of the payment
assert_eq!(<Payouts<Test>>::get(10), vec![(9, 100)]);
// Vouched user wins the rest
assert_eq!(<Payouts<Test>>::get(20), vec![(9, 900)]);
// 10 is no longer vouching
assert_eq!(<Vouching<Test>>::get(10), None);
});
}
#[test]
fn voucher_cannot_win_more_than_bid() {
EnvBuilder::new().execute(|| {
// 10 is the only member
assert_eq!(Society::members(), vec![10]);
// 10 vouches, but asks for more than the bid
assert_ok!(Society::vouch(Origin::signed(10), 20, 100, 1000));
// Vouching creates the right kind of bid
assert_eq!(<Bids<Test>>::get(), vec![create_bid(100, 20, BidKind::Vouch(10, 1000))]);
// Vouched user can become candidate
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(100, 20, BidKind::Vouch(10, 1000))]);
// Vote yes
assert_ok!(Society::vote(Origin::signed(10), 20, true));
// Vouched user can win
run_to_block(8);
assert_eq!(Society::members(), vec![10, 20]);
// Voucher wins as much as the bid
assert_eq!(<Payouts<Test>>::get(10), vec![(9, 100)]);
// Vouched user gets nothing
assert_eq!(<Payouts<Test>>::get(20), vec![]);
});
}
#[test]
fn unvouch_works() {
EnvBuilder::new().execute(|| {
// 10 is the only member
assert_eq!(Society::members(), vec![10]);
// 10 vouches for 20
assert_ok!(Society::vouch(Origin::signed(10), 20, 100, 0));
// 20 has a bid
assert_eq!(<Bids<Test>>::get(), vec![create_bid(100, 20, BidKind::Vouch(10, 0))]);
// 10 is vouched
assert_eq!(<Vouching<Test>>::get(10), Some(VouchingStatus::Vouching));
// To unvouch, you must know the right bid position
assert_noop!(Society::unvouch(Origin::signed(10), 2), Error::<Test, _>::BadPosition);
// 10 can unvouch with the right position
assert_ok!(Society::unvouch(Origin::signed(10), 0));
// 20 no longer has a bid
assert_eq!(<Bids<Test>>::get(), vec![]);
// 10 is no longer vouching
assert_eq!(<Vouching<Test>>::get(10), None);
// Cannot unvouch after they become candidate
assert_ok!(Society::vouch(Origin::signed(10), 20, 100, 0));
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(100, 20, BidKind::Vouch(10, 0))]);
assert_noop!(Society::unvouch(Origin::signed(10), 0), Error::<Test, _>::BadPosition);
// 10 is still vouching until candidate is approved or rejected
assert_eq!(<Vouching<Test>>::get(10), Some(VouchingStatus::Vouching));
run_to_block(8);
// In this case candidate is denied and suspended
assert!(Society::suspended_candidate(&20).is_some());
assert_eq!(Society::members(), vec![10]);
// User is stuck vouching until judgement origin resolves suspended candidate
assert_eq!(<Vouching<Test, _>>::get(10), Some(VouchingStatus::Vouching));
// Judge denies candidate
assert_ok!(Society::judge_suspended_candidate(Origin::signed(2), 20, Judgement::Reject));
// 10 is banned from vouching
assert_eq!(<Vouching<Test, _>>::get(10), Some(VouchingStatus::Banned));
assert_eq!(Society::members(), vec![10]);
// 10 cannot vouch again
assert_noop!(Society::vouch(Origin::signed(10), 30, 100, 0), Error::<Test, _>::AlreadyVouching);
// 10 cannot unvouch either, so they are banned forever.
assert_noop!(Society::unvouch(Origin::signed(10), 0), Error::<Test, _>::NotVouching);
});
}
#[test]
fn unbid_vouch_works() {
EnvBuilder::new().execute(|| {
// 10 is the only member
assert_eq!(Society::members(), vec![10]);
// 10 vouches for 20
assert_ok!(Society::vouch(Origin::signed(10), 20, 100, 0));
// 20 has a bid
assert_eq!(<Bids<Test>>::get(), vec![create_bid(100, 20, BidKind::Vouch(10, 0))]);
// 10 is vouched
assert_eq!(<Vouching<Test>>::get(10), Some(VouchingStatus::Vouching));
// 20 doesn't want to be a member and can unbid themselves.
assert_ok!(Society::unbid(Origin::signed(20), 0));
// Everything is cleaned up
assert_eq!(<Vouching<Test>>::get(10), None);
assert_eq!(<Bids<Test>>::get(), vec![]);
});
}
#[test]
fn head_cannot_be_removed() {
EnvBuilder::new().execute(|| {
// 10 is the only member and head
assert_eq!(Society::members(), vec![10]);
assert_eq!(Society::head(), Some(10));
// 10 can still accumulate strikes
assert_ok!(Society::bid(Origin::signed(20), 0));
run_to_block(8);
assert_eq!(Strikes::<Test>::get(10), 1);
assert_ok!(Society::bid(Origin::signed(30), 0));
run_to_block(16);
assert_eq!(Strikes::<Test>::get(10), 2);
// Awkwardly they can obtain more than MAX_STRIKES...
assert_ok!(Society::bid(Origin::signed(40), 0));
run_to_block(24);
assert_eq!(Strikes::<Test>::get(10), 3);
// Replace the head
assert_ok!(Society::bid(Origin::signed(50), 0));
run_to_block(28);
assert_ok!(Society::vote(Origin::signed(10), 50, true));
assert_ok!(Society::defender_vote(Origin::signed(10), true)); // Keep defender around
run_to_block(32);
assert_eq!(Society::members(), vec![10, 50]);
assert_eq!(Society::head(), Some(50));
// 10 can now be suspended for strikes
assert_ok!(Society::bid(Origin::signed(60), 0));
run_to_block(36);
// The candidate is rejected, so voting approve will give a strike
assert_ok!(Society::vote(Origin::signed(10), 60, true));
run_to_block(40);
assert_eq!(Strikes::<Test>::get(10), 0);
assert_eq!(<SuspendedMembers<Test>>::get(10), Some(()));
assert_eq!(Society::members(), vec![50]);
});
}
#[test]
fn challenges_work() {
EnvBuilder::new().execute(|| {
// Add some members
assert_ok!(Society::add_member(&20));
assert_ok!(Society::add_member(&30));
assert_ok!(Society::add_member(&40));
// Check starting point
assert_eq!(Society::members(), vec![10, 20, 30, 40]);
assert_eq!(Society::defender(), None);
// 20 will be challenged during the challenge rotation
run_to_block(8);
assert_eq!(Society::defender(), Some(20));
// They can always free vote for themselves
assert_ok!(Society::defender_vote(Origin::signed(20), true));
// If no one else votes, nothing happens
run_to_block(16);
assert_eq!(Society::members(), vec![10, 20, 30, 40]);
// New challenge period
assert_eq!(Society::defender(), Some(20));
// Non-member cannot challenge
assert_noop!(Society::defender_vote(Origin::signed(1), true), Error::<Test, _>::NotMember);
// 3 people say accept, 1 reject
assert_ok!(Society::defender_vote(Origin::signed(10), true));
assert_ok!(Society::defender_vote(Origin::signed(20), true));
assert_ok!(Society::defender_vote(Origin::signed(30), true));
assert_ok!(Society::defender_vote(Origin::signed(40), false));
run_to_block(24);
// 20 survives
assert_eq!(Society::members(), vec![10, 20, 30, 40]);
// One more time
assert_eq!(Society::defender(), Some(20));
// 2 people say accept, 2 reject
assert_ok!(Society::defender_vote(Origin::signed(10), true));
assert_ok!(Society::defender_vote(Origin::signed(20), true));
assert_ok!(Society::defender_vote(Origin::signed(30), false));
assert_ok!(Society::defender_vote(Origin::signed(40), false));
run_to_block(32);
// 20 is suspended
assert_eq!(Society::members(), vec![10, 30, 40]);
assert_eq!(Society::suspended_member(20), Some(()));
// New defender is chosen
assert_eq!(Society::defender(), Some(40));
});
}
#[test]
fn bad_vote_slash_works() {
EnvBuilder::new().execute(|| {
// Add some members
assert_ok!(Society::add_member(&20));
assert_ok!(Society::add_member(&30));
assert_ok!(Society::add_member(&40));
// Create some payouts
Society::bump_payout(&10, 5, 100);
Society::bump_payout(&20, 5, 100);
Society::bump_payout(&30, 5, 100);
Society::bump_payout(&40, 5, 100);
// Check starting point
assert_eq!(Society::members(), vec![10, 20, 30, 40]);
assert_eq!(<Payouts<Test>>::get(10), vec![(5, 100)]);
assert_eq!(<Payouts<Test>>::get(20), vec![(5, 100)]);
assert_eq!(<Payouts<Test>>::get(30), vec![(5, 100)]);
assert_eq!(<Payouts<Test>>::get(40), vec![(5, 100)]);
// Create a new bid
assert_ok!(Society::bid(Origin::signed(50), 1000));
run_to_block(4);
assert_ok!(Society::vote(Origin::signed(10), 50, false));
assert_ok!(Society::vote(Origin::signed(20), 50, true));
assert_ok!(Society::vote(Origin::signed(30), 50, false));
assert_ok!(Society::vote(Origin::signed(40), 50, false));
run_to_block(8);
// Wrong voter gained a strike
assert_eq!(<Strikes<Test>>::get(10), 0);
assert_eq!(<Strikes<Test>>::get(20), 1);
assert_eq!(<Strikes<Test>>::get(30), 0);
assert_eq!(<Strikes<Test>>::get(40), 0);
// Their payout is slashed, a random person is rewarded
assert_eq!(<Payouts<Test>>::get(10), vec![(5, 100), (9,2)]);
assert_eq!(<Payouts<Test>>::get(20), vec![(5, 98)]);
assert_eq!(<Payouts<Test>>::get(30), vec![(5, 100)]);
assert_eq!(<Payouts<Test>>::get(40), vec![(5, 100)]);
});
}
#[test]
fn user_cannot_bid_twice() {
EnvBuilder::new().execute(|| {
// Cannot bid twice
assert_ok!(Society::bid(Origin::signed(20), 100));
assert_noop!(Society::bid(Origin::signed(20), 100), Error::<Test, _>::AlreadyBid);
// Cannot bid when vouched
assert_ok!(Society::vouch(Origin::signed(10), 30, 100, 100));
assert_noop!(Society::bid(Origin::signed(30), 100), Error::<Test, _>::AlreadyBid);
// Cannot vouch when already bid
assert_ok!(Society::add_member(&50));
assert_noop!(Society::vouch(Origin::signed(50), 20, 100, 100), Error::<Test, _>::AlreadyBid);
});
}
#[test]
fn vouching_handles_removed_member_with_bid() {
EnvBuilder::new().execute(|| {
// Add a member
assert_ok!(Society::add_member(&20));
// Have that member vouch for a user
assert_ok!(Society::vouch(Origin::signed(20), 30, 1000, 100));
// That user is now a bid and the member is vouching
assert_eq!(<Bids<Test>>::get(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
assert_eq!(<Vouching<Test>>::get(20), Some(VouchingStatus::Vouching));
// Suspend that member
Society::suspend_member(&20);
assert_eq!(<SuspendedMembers<Test>>::get(20), Some(()));
// Nothing changes yet
assert_eq!(<Bids<Test>>::get(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
assert_eq!(<Vouching<Test>>::get(20), Some(VouchingStatus::Vouching));
// Remove member
assert_ok!(Society::judge_suspended_member(Origin::signed(2), 20, false));
// Bid is removed, vouching status is removed
assert_eq!(<Bids<Test>>::get(), vec![]);
assert_eq!(<Vouching<Test>>::get(20), None);
});
}
#[test]
fn vouching_handles_removed_member_with_candidate() {
EnvBuilder::new().execute(|| {
// Add a member
assert_ok!(Society::add_member(&20));
// Have that member vouch for a user
assert_ok!(Society::vouch(Origin::signed(20), 30, 1000, 100));
// That user is now a bid and the member is vouching
assert_eq!(<Bids<Test>>::get(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
assert_eq!(<Vouching<Test>>::get(20), Some(VouchingStatus::Vouching));
// Make that bid a candidate
run_to_block(4);
assert_eq!(Society::candidates(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
// Suspend that member
Society::suspend_member(&20);
assert_eq!(<SuspendedMembers<Test>>::get(20), Some(()));
// Nothing changes yet
assert_eq!(Society::candidates(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
assert_eq!(<Vouching<Test>>::get(20), Some(VouchingStatus::Vouching));
// Remove member
assert_ok!(Society::judge_suspended_member(Origin::signed(2), 20, false));
// Vouching status is removed, but candidate is still in the queue
assert_eq!(<Vouching<Test>>::get(20), None);
assert_eq!(Society::candidates(), vec![create_bid(1000, 30, BidKind::Vouch(20, 100))]);
// Candidate wins
assert_ok!(Society::vote(Origin::signed(10), 30, true));
run_to_block(8);
assert_eq!(Society::members(), vec![10, 30]);
// Payout does not go to removed member
assert_eq!(<Payouts<Test>>::get(20), vec![]);
assert_eq!(<Payouts<Test>>::get(30), vec![(9, 1000)]);
});
}
#[test]
fn votes_are_working() {
EnvBuilder::new().execute(|| {
// Users make bids of various amounts
assert_ok!(Society::bid(Origin::signed(50), 500));
assert_ok!(Society::bid(Origin::signed(40), 400));
assert_ok!(Society::bid(Origin::signed(30), 300));
// Rotate period
run_to_block(4);
// A member votes for these candidates to join the society
assert_ok!(Society::vote(Origin::signed(10), 30, true));
assert_ok!(Society::vote(Origin::signed(10), 40, true));
// You cannot vote for a non-candidate
assert_noop!(Society::vote(Origin::signed(10), 50, true), Error::<Test, _>::NotCandidate);
// Votes are stored
assert_eq!(<Votes<Test>>::get(30, 10), Some(Vote::Approve));
assert_eq!(<Votes<Test>>::get(40, 10), Some(Vote::Approve));
assert_eq!(<Votes<Test>>::get(50, 10), None);
run_to_block(8);
// Candidates become members after a period rotation
assert_eq!(Society::members(), vec![10, 30, 40]);
// Votes are cleaned up
assert_eq!(<Votes<Test>>::get(30, 10), None);
assert_eq!(<Votes<Test>>::get(40, 10), None);
});
}
#[test]
fn max_limits_work() {
EnvBuilder::new().with_pot(100000).execute(|| {
// Max bids is 1000, when extra bids come in, it pops the larger ones off the stack.
// Try to put 1010 users into the bid pool
for i in (100..1110).rev() {
// Give them some funds
let _ = Balances::make_free_balance_be(&(i as u128), 1000);
assert_ok!(Society::bid(Origin::signed(i as u128), i));
}
let bids = <Bids<Test>>::get();
// Length is 1000
assert_eq!(bids.len(), 1000);
// First bid is smallest number (100)
assert_eq!(bids[0], create_bid(100, 100, BidKind::Deposit(25)));
// Last bid is smallest number + 99 (1099)
assert_eq!(bids[999], create_bid(1099, 1099, BidKind::Deposit(25)));
// Rotate period
run_to_block(4);
// Max of 10 candidates
assert_eq!(Society::candidates().len(), 10);
// Fill up membership, max 100, we will do just 95
for i in 2000..2095 {
assert_ok!(Society::add_member(&(i as u128)));
}
// Remember there was 1 original member, so 96 total
assert_eq!(Society::members().len(), 96);
// Rotate period
run_to_block(8);
// Only of 4 candidates possible now
assert_eq!(Society::candidates().len(), 4);
// Fill up members with suspended candidates from the first rotation
for i in 100..104 {
assert_ok!(Society::judge_suspended_candidate(Origin::signed(2), i, Judgement::Approve));
}
assert_eq!(Society::members().len(), 100);
// Can't add any more members
assert_noop!(Society::add_member(&98), Error::<Test, _>::MaxMembers);
// However, a fringe scenario allows for in-progress candidates to increase the membership
// pool, but it has no real after-effects.
for i in Society::members().iter() {
assert_ok!(Society::vote(Origin::signed(*i), 110, true));
assert_ok!(Society::vote(Origin::signed(*i), 111, true));
assert_ok!(Society::vote(Origin::signed(*i), 112, true));
}
// Rotate period
run_to_block(12);
// Members length is over 100, no problem...
assert_eq!(Society::members().len(), 103);
// No candidates because full
assert_eq!(Society::candidates().len(), 0);
// Increase member limit
assert_ok!(Society::set_max_members(Origin::ROOT, 200));
// Rotate period
run_to_block(16);
// Candidates are back!
assert_eq!(Society::candidates().len(), 10);
});
}