Skip to content

Commit ad8a23a

Browse files
paritytech-release-backport-bot[bot]laurogripagithub-actions[bot]bkchrsigurpol
authored
[stable2512] Backport #11154 (#11280)
Backport #11154 into `stable2512` from laurogripa. See the [documentation](https://github.com/paritytech/polkadot-sdk/blob/master/docs/BACKPORT.md) on how to use this bot. <!-- # To be used by other automation, do not modify: original-pr-number: #${pull_number} --> --------- Co-authored-by: Lauro Gripa <laurogripa@gmail.com> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Bastian Köcher <git@kchr.de> Co-authored-by: Paolo La Camera <paolo@parity.io>
1 parent 3dd5b89 commit ad8a23a

File tree

5 files changed

+195
-0
lines changed

5 files changed

+195
-0
lines changed

prdoc/pr_11154.prdoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
title: Add kick_member to Society pallet
2+
doc:
3+
- audience: Runtime Dev
4+
description: Adds a kick_member extrinsic to pallet-society, callable by the Founder,
5+
that removes a member, slashes its payouts and returns them to the society pot.
6+
crates:
7+
- name: pallet-society
8+
bump: minor
9+
validate: false

substrate/frame/society/src/benchmarking.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,43 @@ mod benchmarks {
558558
Ok(())
559559
}
560560

561+
#[benchmark]
562+
fn kick_member() -> Result<(), BenchmarkError> {
563+
// Add member
564+
let founder = setup_funded_society::<T, I>()?;
565+
let member: T::AccountId = account("member", 0, 0);
566+
let member_lookup: <T::Lookup as StaticLookup>::Source =
567+
T::Lookup::unlookup(member.clone());
568+
569+
let _ = Society::<T, I>::insert_member(&member, 0u32.into());
570+
let mut record = Members::<T, I>::get(&member).ok_or("Member not found")?;
571+
record.vouching = Some(VouchingStatus::Vouching);
572+
Members::<T, I>::insert(&member, &record);
573+
574+
// Populate payouts to max to cover worst-case slashing scenario
575+
for i in 0..T::MaxPayouts::get() {
576+
Society::<T, I>::bump_payout(&member, i.into(), 1u32.into());
577+
}
578+
579+
// Add vouch to cover worst-case scenario
580+
let vouched: T::AccountId = account("vouched", 0, 0);
581+
let mut bids = Bids::<T, I>::get();
582+
Society::<T, I>::insert_bid(
583+
&mut bids,
584+
&vouched,
585+
10u32.into(),
586+
BidKind::Vouch(member.clone(), 0u32.into()),
587+
);
588+
Bids::<T, I>::put(bids);
589+
590+
#[extrinsic_call]
591+
_(RawOrigin::Signed(founder), member_lookup);
592+
593+
assert!(!Members::<T, I>::contains_key(&member));
594+
assert!(!SuspendedMembers::<T, I>::contains_key(&member));
595+
Ok(())
596+
}
597+
561598
impl_benchmark_test_suite!(
562599
Society,
563600
sp_io::TestExternalities::from(

substrate/frame/society/src/lib.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,8 @@ pub mod pallet {
671671
old_deposit: BalanceOf<T, I>,
672672
new_deposit: BalanceOf<T, I>,
673673
},
674+
/// A member was kicked by the founder.
675+
MemberKicked { member: T::AccountId },
674676
}
675677

676678
/// Old name generated by `decl_event`.
@@ -1461,6 +1463,35 @@ pub mod pallet {
14611463

14621464
Ok(Pays::No.into())
14631465
}
1466+
1467+
/// Kick a member from the society. Callable only by the Signed origin of the Founder.
1468+
///
1469+
/// The member is fully removed (not suspended). All unclaimed payouts are slashed and
1470+
/// returned to the society pot.
1471+
///
1472+
/// Parameters:
1473+
/// - `who`: The member to be removed.
1474+
#[pallet::call_index(21)]
1475+
#[pallet::weight(T::WeightInfo::kick_member())]
1476+
pub fn kick_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
1477+
ensure!(
1478+
Some(ensure_signed(origin)?) == Founder::<T, I>::get(),
1479+
Error::<T, I>::NotFounder
1480+
);
1481+
let who = T::Lookup::lookup(who)?;
1482+
1483+
let _ = Self::remove_member(&who)?;
1484+
1485+
let payout_record = Payouts::<T, I>::take(&who);
1486+
let total = payout_record
1487+
.payouts
1488+
.into_iter()
1489+
.fold(Zero::zero(), |acc: BalanceOf<T, I>, x| acc.saturating_add(x.1));
1490+
Self::unreserve_payout(total);
1491+
1492+
Self::deposit_event(Event::<T, I>::MemberKicked { member: who });
1493+
Ok(())
1494+
}
14641495
}
14651496
}
14661497

substrate/frame/society/src/tests.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,3 +1660,70 @@ fn intake_idempotency() {
16601660
assert_eq!(Balances::free_balance(20), 50);
16611661
});
16621662
}
1663+
1664+
#[test]
1665+
fn kick_member_works() {
1666+
EnvBuilder::new().execute(|| {
1667+
// Given: 20 is a regular member.
1668+
place_members([20]);
1669+
assert!(Members::<Test>::contains_key(20));
1670+
1671+
// When: a non-founder tries to kick → fails.
1672+
assert_noop!(Society::kick_member(Origin::signed(20), 20), Error::<Test>::NotFounder);
1673+
1674+
// When: founder kicks member 20.
1675+
assert_ok!(Society::kick_member(Origin::signed(10), 20));
1676+
1677+
// Then: member is fully removed, not suspended.
1678+
assert!(!Members::<Test>::contains_key(20));
1679+
assert!(!SuspendedMembers::<Test>::contains_key(20));
1680+
1681+
// Then: event is emitted.
1682+
System::assert_has_event(Event::<Test>::MemberKicked { member: 20 }.into());
1683+
1684+
// Then: kicking again fails — not a member anymore.
1685+
assert_noop!(Society::kick_member(Origin::signed(10), 20), Error::<Test>::NotMember);
1686+
});
1687+
}
1688+
1689+
#[test]
1690+
fn kicked_member_cannot_claim_payout() {
1691+
EnvBuilder::new().execute(|| {
1692+
// Given: members 20 and 30 exist, and 30 has a pending payout of 100.
1693+
place_members([20, 30]);
1694+
// Simulate a reserved payout for member 30.
1695+
Society::bump_payout(&30, 1, 100);
1696+
let pot_after_reserve = Pot::<Test>::get();
1697+
1698+
// When: founder kicks member 30.
1699+
assert_ok!(Society::kick_member(Origin::signed(10), 30));
1700+
1701+
// Then: member is fully removed, not suspended.
1702+
assert!(!Members::<Test>::contains_key(30));
1703+
assert!(!SuspendedMembers::<Test>::contains_key(30));
1704+
1705+
// Then: payouts are cleared.
1706+
assert_eq!(
1707+
Payouts::<Test>::get(30),
1708+
PayoutRecord { paid: 0, payouts: vec![].try_into().unwrap() }
1709+
);
1710+
1711+
// Then: slashed amount (100) is returned to the pot.
1712+
assert_eq!(Pot::<Test>::get(), pot_after_reserve + 100);
1713+
1714+
// Then: kicked member cannot claim payout.
1715+
assert_noop!(Society::payout(Origin::signed(30)), Error::<Test>::NotMember);
1716+
});
1717+
}
1718+
1719+
#[test]
1720+
fn founder_cannot_kick_head() {
1721+
EnvBuilder::new().execute(|| {
1722+
// Given: member 20 is the current Head.
1723+
place_members([20]);
1724+
Head::<Test>::put(20);
1725+
1726+
// When: founder tries to kick Head → fails.
1727+
assert_noop!(Society::kick_member(Origin::signed(10), 20), Error::<Test>::Head);
1728+
});
1729+
}

substrate/frame/society/src/weights.rs

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)