diff --git a/Cargo.lock b/Cargo.lock
index 2f2571bfa85feac6f8f8ed60f8e10cd386343377..a4b1cd6e0e15cf89ab6a5e10faadb6eec51968aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9656,6 +9656,7 @@ dependencies = [
  "frame-support",
  "frame-system",
  "log",
+ "pallet-ranked-collective",
  "parity-scale-codec",
  "scale-info",
  "sp-arithmetic",
diff --git a/substrate/frame/core-fellowship/Cargo.toml b/substrate/frame/core-fellowship/Cargo.toml
index 0a1706ae22d3c63e3cf4dd5c142db0696488b67b..dc1a0a6c4af8f877aea26e92d7005b42d16b3a84 100644
--- a/substrate/frame/core-fellowship/Cargo.toml
+++ b/substrate/frame/core-fellowship/Cargo.toml
@@ -27,6 +27,7 @@ sp-core = { path = "../../primitives/core", default-features = false }
 sp-io = { path = "../../primitives/io", default-features = false }
 sp-runtime = { path = "../../primitives/runtime", default-features = false }
 sp-std = { path = "../../primitives/std", default-features = false }
+pallet-ranked-collective = { path = "../ranked-collective", default-features = false }
 
 [features]
 default = ["std"]
@@ -34,6 +35,7 @@ std = [
 	"codec/std",
 	"frame-benchmarking?/std",
 	"frame-support/std",
+	"frame-support/experimental",
 	"frame-system/std",
 	"log/std",
 	"scale-info/std",
@@ -42,15 +44,18 @@ std = [
 	"sp-io/std",
 	"sp-runtime/std",
 	"sp-std/std",
+	"pallet-ranked-collective/std"
 ]
 runtime-benchmarks = [
 	"frame-benchmarking/runtime-benchmarks",
 	"frame-support/runtime-benchmarks",
 	"frame-system/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
+	"pallet-ranked-collective/runtime-benchmarks"
 ]
 try-runtime = [
 	"frame-support/try-runtime",
 	"frame-system/try-runtime",
 	"sp-runtime/try-runtime",
+	"pallet-ranked-collective/try-runtime"
 ]
diff --git a/substrate/frame/core-fellowship/src/lib.rs b/substrate/frame/core-fellowship/src/lib.rs
index a0a45c7c594da76c50d6c7bab30a3cae30b4904a..4c824b43bb7b3126b9e324e5bd2ddcc54a23bb63 100644
--- a/substrate/frame/core-fellowship/src/lib.rs
+++ b/substrate/frame/core-fellowship/src/lib.rs
@@ -47,7 +47,7 @@
 //! this explicitly when external factors to this pallet have caused the tracked account to become
 //! unranked. At rank 0, there is not a "demotion" period after which the account may be bumped to
 //! become offboarded but rather an "offboard timeout".
-//!
+//!>
 //! Candidates may be introduced (i.e. an account to go from unranked to rank of 0) by an origin
 //! of a different privilege to that for promotion. This allows the possibility for even a single
 //! existing member to introduce a new candidate without payment.
@@ -65,13 +65,16 @@ use sp_runtime::RuntimeDebug;
 use sp_std::{marker::PhantomData, prelude::*};
 
 use frame_support::{
+	defensive,
 	dispatch::DispatchResultWithPostInfo,
 	ensure, impl_ensure_origin_with_arg_ignoring_arg,
 	traits::{
 		tokens::Balance as BalanceTrait, EnsureOrigin, EnsureOriginWithArg, Get, RankedMembers,
+		RankedMembersSwapHandler,
 	},
 	BoundedVec,
 };
+use pallet_ranked_collective::Rank;
 
 #[cfg(test)]
 mod tests;
@@ -249,6 +252,8 @@ pub mod pallet {
 		},
 		/// Pre-ranked account has been inducted at their current rank.
 		Imported { who: T::AccountId, rank: RankOf<T, I> },
+		/// A member had its AccountId swapped.
+		Swapped { who: T::AccountId, new_who: T::AccountId },
 	}
 
 	#[pallet::error]
@@ -603,3 +608,29 @@ impl_ensure_origin_with_arg_ignoring_arg! {
 		EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureInducted<T, I, MIN_RANK>
 	{}
 }
+
+impl<T: Config<I>, I: 'static> RankedMembersSwapHandler<T::AccountId, Rank> for Pallet<T, I> {
+	fn swapped(old: &T::AccountId, new: &T::AccountId, _rank: Rank) {
+		if old == new {
+			defensive!("Should not try to swap with self");
+			return
+		}
+		if !Member::<T, I>::contains_key(old) {
+			defensive!("Should not try to swap non-member");
+			return
+		}
+		if Member::<T, I>::contains_key(new) {
+			defensive!("Should not try to overwrite existing member");
+			return
+		}
+
+		if let Some(member) = Member::<T, I>::take(old) {
+			Member::<T, I>::insert(new, member);
+		}
+		if let Some(we) = MemberEvidence::<T, I>::take(old) {
+			MemberEvidence::<T, I>::insert(new, we);
+		}
+
+		Self::deposit_event(Event::<T, I>::Swapped { who: old.clone(), new_who: new.clone() });
+	}
+}
diff --git a/substrate/frame/core-fellowship/src/tests/integration.rs b/substrate/frame/core-fellowship/src/tests/integration.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ded12aaf1a4d3521733209eb106aa539652f776e
--- /dev/null
+++ b/substrate/frame/core-fellowship/src/tests/integration.rs
@@ -0,0 +1,389 @@
+// This file is part of Substrate.
+
+// Copyright (C) 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.
+
+//! Integration test together with the ranked-collective pallet.
+
+use std::collections::BTreeMap;
+
+use frame_support::{
+	assert_noop, assert_ok, derive_impl, hypothetically, ord_parameter_types,
+	pallet_prelude::Weight,
+	parameter_types,
+	traits::{
+		ConstU16, ConstU32, ConstU64, EitherOf, Everything, IsInVec, MapSuccess, PollStatus,
+		Polling, TryMapSuccess,
+	},
+};
+use frame_system::EnsureSignedBy;
+use pallet_ranked_collective::{EnsureRanked, Geometric, Rank, Tally, TallyOf, Votes};
+use sp_core::{Get, H256};
+use sp_runtime::{
+	traits::{BlakeTwo256, Convert, IdentityLookup, ReduceBy, TryMorphInto},
+	BuildStorage, DispatchError, DispatchResult,
+};
+type Class = Rank;
+use sp_std::cell::RefCell;
+
+use crate as pallet_core_fellowship;
+use crate::*;
+
+type Block = frame_system::mocking::MockBlock<Test>;
+
+frame_support::construct_runtime!(
+	pub enum Test
+	{
+		System: frame_system,
+		CoreFellowship: pallet_core_fellowship,
+		Club: pallet_ranked_collective,
+	}
+);
+
+parameter_types! {
+	pub BlockWeights: frame_system::limits::BlockWeights =
+		frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1_000_000, u64::max_value()));
+}
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
+impl frame_system::Config for Test {
+	type BaseCallFilter = Everything;
+	type BlockWeights = ();
+	type BlockLength = ();
+	type DbWeight = ();
+	type RuntimeOrigin = RuntimeOrigin;
+	type Nonce = u64;
+	type RuntimeCall = RuntimeCall;
+	type Hash = H256;
+	type Hashing = BlakeTwo256;
+	type AccountId = u64;
+	type Lookup = IdentityLookup<Self::AccountId>;
+	type Block = Block;
+	type RuntimeEvent = RuntimeEvent;
+	type BlockHashCount = ConstU64<250>;
+	type Version = ();
+	type PalletInfo = PalletInfo;
+	type AccountData = ();
+	type OnNewAccount = ();
+	type OnKilledAccount = ();
+	type SystemWeightInfo = ();
+	type SS58Prefix = ();
+	type OnSetCode = ();
+	type MaxConsumers = ConstU32<16>;
+}
+
+parameter_types! {
+	pub static MinRankOfClassDelta: Rank = 0;
+}
+
+thread_local! {
+	pub static CLUB: RefCell<BTreeMap<u64, u16>> = RefCell::new(BTreeMap::new());
+}
+
+pub struct TestClub;
+impl RankedMembers for TestClub {
+	type AccountId = u64;
+	type Rank = u16;
+	fn min_rank() -> Self::Rank {
+		0
+	}
+	fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
+		CLUB.with(|club| club.borrow().get(who).cloned())
+	}
+	fn induct(who: &Self::AccountId) -> DispatchResult {
+		CLUB.with(|club| club.borrow_mut().insert(*who, 0));
+		Ok(())
+	}
+	fn promote(who: &Self::AccountId) -> DispatchResult {
+		CLUB.with(|club| {
+			club.borrow_mut().entry(*who).and_modify(|r| *r += 1);
+		});
+		Ok(())
+	}
+	fn demote(who: &Self::AccountId) -> DispatchResult {
+		CLUB.with(|club| match Self::rank_of(who) {
+			None => Err(sp_runtime::DispatchError::Unavailable),
+			Some(0) => {
+				club.borrow_mut().remove(&who);
+				Ok(())
+			},
+			Some(_) => {
+				club.borrow_mut().entry(*who).and_modify(|x| *x -= 1);
+				Ok(())
+			},
+		})
+	}
+}
+
+fn set_rank(who: u64, rank: u16) {
+	CLUB.with(|club| club.borrow_mut().insert(who, rank));
+}
+
+parameter_types! {
+	pub ZeroToNine: Vec<u64> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+	pub EvidenceSize: u32 = 1024;
+}
+ord_parameter_types! {
+	pub const One: u64 = 1;
+}
+
+impl Config for Test {
+	type WeightInfo = ();
+	type RuntimeEvent = RuntimeEvent;
+	type Members = TestClub;
+	type Balance = u64;
+	type ParamsOrigin = EnsureSignedBy<One, u64>;
+	type InductOrigin = EnsureInducted<Test, (), 1>;
+	type ApproveOrigin = TryMapSuccess<EnsureSignedBy<IsInVec<ZeroToNine>, u64>, TryMorphInto<u16>>;
+	type PromoteOrigin = TryMapSuccess<EnsureSignedBy<IsInVec<ZeroToNine>, u64>, TryMorphInto<u16>>;
+	type EvidenceSize = EvidenceSize;
+}
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum TestPollState {
+	Ongoing(TallyOf<Test>, Rank),
+	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), 1)),
+	].into_iter().collect();
+}
+
+pub struct TestPolls;
+impl Polling<TallyOf<Test>> for TestPolls {
+	type Index = u8;
+	type Votes = Votes;
+	type Moment = u64;
+	type Class = Class;
+	fn classes() -> Vec<Self::Class> {
+		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>, Self::Moment, Self::Class>) -> 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>, Self::Moment, Self::Class>,
+		) -> 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::new(class), 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(())
+	}
+}
+
+/// Convert the tally class into the minimum rank required to vote on the poll.
+/// MinRank(Class) = Class - Delta
+pub struct MinRankOfClass<Delta>(PhantomData<Delta>);
+impl<Delta: Get<Rank>> Convert<Class, Rank> for MinRankOfClass<Delta> {
+	fn convert(a: Class) -> Rank {
+		a.saturating_sub(Delta::get())
+	}
+}
+
+impl pallet_ranked_collective::Config for Test {
+	type WeightInfo = ();
+	type RuntimeEvent = RuntimeEvent;
+	type PromoteOrigin = EitherOf<
+		// Root can promote arbitrarily.
+		frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>,
+		// Members can promote up to the rank of 2 below them.
+		MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>,
+	>;
+	type DemoteOrigin = EitherOf<
+		// Root can demote arbitrarily.
+		frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>,
+		// Members can demote up to the rank of 3 below them.
+		MapSuccess<EnsureRanked<Test, (), 3>, ReduceBy<ConstU16<3>>>,
+	>;
+	type ExchangeOrigin = EitherOf<
+		// Root can exchange arbitrarily.
+		frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>,
+		// Members can exchange up to the rank of 2 below them.
+		MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>,
+	>;
+	type Polls = TestPolls;
+	type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>;
+	type VoteWeight = Geometric;
+	type MemberSwappedHandler = CoreFellowship;
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+	let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| {
+		let params = ParamsType {
+			active_salary: [10, 20, 30, 40, 50, 60, 70, 80, 90],
+			passive_salary: [1, 2, 3, 4, 5, 6, 7, 8, 9],
+			demotion_period: [2, 4, 6, 8, 10, 12, 14, 16, 18],
+			min_promotion_period: [3, 6, 9, 12, 15, 18, 21, 24, 27],
+			offboard_timeout: 1,
+		};
+		assert_ok!(CoreFellowship::set_params(signed(1), Box::new(params)));
+		System::set_block_number(1);
+	});
+	ext
+}
+
+fn signed(who: u64) -> RuntimeOrigin {
+	RuntimeOrigin::signed(who)
+}
+
+fn assert_last_event(generic_event: <Test as Config>::RuntimeEvent) {
+	let events = frame_system::Pallet::<Test>::events();
+	let system_event: <Test as frame_system::Config>::RuntimeEvent = generic_event.into();
+	let frame_system::EventRecord { event, .. } = events.last().expect("Event expected");
+	assert_eq!(event, &system_event.into());
+}
+
+fn evidence(e: u32) -> Evidence<Test, ()> {
+	e.encode()
+		.into_iter()
+		.cycle()
+		.take(1024)
+		.collect::<Vec<_>>()
+		.try_into()
+		.expect("Static length matches")
+}
+
+#[test]
+fn swap_simple_works() {
+	new_test_ext().execute_with(|| {
+		for i in 0u16..9 {
+			let acc = i as u64;
+
+			assert_ok!(Club::add_member(RuntimeOrigin::root(), acc));
+			set_rank(acc, i);
+			assert_ok!(CoreFellowship::import(signed(acc)));
+
+			// Swapping normally works:
+			assert_ok!(Club::exchange_member(RuntimeOrigin::root(), acc, acc + 10));
+			assert_last_event(Event::Swapped { who: acc, new_who: acc + 10 }.into());
+		}
+	});
+}
+
+/// Exhaustively test that adding member `1` is equivalent to adding member `0` and then swapping.
+///
+/// The member also submits evidence before the swap.
+#[test]
+fn swap_exhaustive_works() {
+	new_test_ext().execute_with(|| {
+		let root_add = hypothetically!({
+			assert_ok!(Club::add_member(RuntimeOrigin::root(), 1));
+			set_rank(1, 4);
+			assert_ok!(CoreFellowship::import(signed(1)));
+			assert_ok!(CoreFellowship::submit_evidence(signed(1), Wish::Retention, evidence(1)));
+
+			// The events mess up the storage root:
+			System::reset_events();
+			sp_io::storage::root(sp_runtime::StateVersion::V1)
+		});
+
+		let root_swap = hypothetically!({
+			assert_ok!(Club::add_member(RuntimeOrigin::root(), 0));
+			set_rank(0, 4);
+			assert_ok!(CoreFellowship::import(signed(0)));
+			assert_ok!(CoreFellowship::submit_evidence(signed(0), Wish::Retention, evidence(1)));
+
+			// Now we swap:
+			assert_ok!(Club::exchange_member(RuntimeOrigin::root(), 0, 1));
+
+			System::reset_events();
+			sp_io::storage::root(sp_runtime::StateVersion::V1)
+		});
+
+		assert_eq!(root_add, root_swap);
+	});
+}
+
+#[test]
+fn swap_bad_noops() {
+	new_test_ext().execute_with(|| {
+		assert_ok!(Club::add_member(RuntimeOrigin::root(), 0));
+		set_rank(0, 0);
+		assert_ok!(CoreFellowship::import(signed(0)));
+		assert_ok!(Club::add_member(RuntimeOrigin::root(), 1));
+		set_rank(1, 1);
+		assert_ok!(CoreFellowship::import(signed(1)));
+
+		// Swapping for another member is a noop:
+		assert_noop!(
+			Club::exchange_member(RuntimeOrigin::root(), 0, 1),
+			pallet_ranked_collective::Error::<Test>::AlreadyMember
+		);
+		// Swapping for the same member is a noop:
+		assert_noop!(
+			Club::exchange_member(RuntimeOrigin::root(), 0, 0),
+			pallet_ranked_collective::Error::<Test>::SameMember
+		);
+	});
+}
diff --git a/substrate/frame/core-fellowship/src/tests/mod.rs b/substrate/frame/core-fellowship/src/tests/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4c657a8e877798f89ef26b5c185bf6349661457b
--- /dev/null
+++ b/substrate/frame/core-fellowship/src/tests/mod.rs
@@ -0,0 +1,21 @@
+// This file is part of Substrate.
+
+// Copyright (C) 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.
+
+//! Tests for this crate.
+
+mod integration;
+mod unit;
diff --git a/substrate/frame/core-fellowship/src/tests.rs b/substrate/frame/core-fellowship/src/tests/unit.rs
similarity index 99%
rename from substrate/frame/core-fellowship/src/tests.rs
rename to substrate/frame/core-fellowship/src/tests/unit.rs
index 838bd9bbdc80e57a760ea13693bf2c07fdcd62b4..26f88100d08ba8b9e36a706b6646d434b081664f 100644
--- a/substrate/frame/core-fellowship/src/tests.rs
+++ b/substrate/frame/core-fellowship/src/tests/unit.rs
@@ -15,7 +15,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! The crate's tests.
+//! The crate's unit tests.
 
 use std::collections::BTreeMap;
 
@@ -33,8 +33,8 @@ use sp_runtime::{
 };
 use sp_std::cell::RefCell;
 
-use super::*;
 use crate as pallet_core_fellowship;
+use crate::*;
 
 type Block = frame_system::mocking::MockBlock<Test>;
 
diff --git a/substrate/frame/ranked-collective/src/lib.rs b/substrate/frame/ranked-collective/src/lib.rs
index 88631884c5a781520323befe8f802c172a6fa48e..c8291ab46093b18d2e8256eeb2e3a2f6eb87062e 100644
--- a/substrate/frame/ranked-collective/src/lib.rs
+++ b/substrate/frame/ranked-collective/src/lib.rs
@@ -54,7 +54,10 @@ use sp_std::{marker::PhantomData, prelude::*};
 use frame_support::{
 	dispatch::{DispatchResultWithPostInfo, PostDispatchInfo},
 	ensure, impl_ensure_origin_with_arg_ignoring_arg,
-	traits::{EnsureOrigin, EnsureOriginWithArg, PollStatus, Polling, RankedMembers, VoteTally},
+	traits::{
+		EnsureOrigin, EnsureOriginWithArg, PollStatus, Polling, RankedMembers,
+		RankedMembersSwapHandler, VoteTally,
+	},
 	CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
 };
 
@@ -402,6 +405,12 @@ pub mod pallet {
 		/// "a rank of at least the poll class".
 		type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
 
+		/// An external handler that will be notified when two members are swapped.
+		type MemberSwappedHandler: RankedMembersSwapHandler<
+			<Pallet<Self, I> as RankedMembers>::AccountId,
+			<Pallet<Self, I> as RankedMembers>::Rank,
+		>;
+
 		/// Convert a rank_delta into a number of votes the rank gets.
 		///
 		/// Rank_delta is defined as the number of ranks above the minimum required to take part
@@ -679,7 +688,12 @@ pub mod pallet {
 			Self::do_remove_member_from_rank(&who, rank)?;
 			Self::do_add_member_to_rank(new_who.clone(), rank, false)?;
 
-			Self::deposit_event(Event::MemberExchanged { who, new_who });
+			Self::deposit_event(Event::MemberExchanged {
+				who: who.clone(),
+				new_who: new_who.clone(),
+			});
+			T::MemberSwappedHandler::swapped(&who, &new_who, rank);
+
 			Ok(())
 		}
 	}
diff --git a/substrate/frame/ranked-collective/src/tests.rs b/substrate/frame/ranked-collective/src/tests.rs
index f5e83c48acd05498d25865b44585633fc462c0ff..5c4ef0673c16fa96dca27722606a757ed4d203f9 100644
--- a/substrate/frame/ranked-collective/src/tests.rs
+++ b/substrate/frame/ranked-collective/src/tests.rs
@@ -173,6 +173,7 @@ impl Config for Test {
 	type Polls = TestPolls;
 	type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>;
 	type VoteWeight = Geometric;
+	type MemberSwappedHandler = ();
 }
 
 pub fn new_test_ext() -> sp_io::TestExternalities {
@@ -602,3 +603,21 @@ fn exchange_member_works() {
 		);
 	});
 }
+
+#[test]
+fn exchange_member_same_noops() {
+	new_test_ext().execute_with(|| {
+		assert_ok!(Club::add_member(RuntimeOrigin::root(), 1));
+		assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1));
+		assert_ok!(Club::add_member(RuntimeOrigin::root(), 2));
+		assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2));
+
+		// Swapping the same accounts is a noop:
+		assert_noop!(Club::exchange_member(RuntimeOrigin::root(), 1, 1), Error::<Test>::SameMember);
+		// Swapping with a different member is a noop:
+		assert_noop!(
+			Club::exchange_member(RuntimeOrigin::root(), 1, 2),
+			Error::<Test>::AlreadyMember
+		);
+	});
+}
diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs
index a18faee680596340999cd784597825dee00f3391..2a42fca76b3c57623c74e3724b342eadf57737cd 100644
--- a/substrate/frame/support/src/traits.rs
+++ b/substrate/frame/support/src/traits.rs
@@ -37,7 +37,7 @@ pub use members::{AllowAll, DenyAll, Filter};
 pub use members::{
 	AsContains, ChangeMembers, Contains, ContainsLengthBound, ContainsPair, Equals, Everything,
 	EverythingBut, FromContainsPair, InitializeMembers, InsideBoth, IsInVec, Nothing,
-	RankedMembers, SortedMembers, TheseExcept,
+	RankedMembers, RankedMembersSwapHandler, SortedMembers, TheseExcept,
 };
 
 mod validation;
diff --git a/substrate/frame/support/src/traits/members.rs b/substrate/frame/support/src/traits/members.rs
index d667eaa7e9d8d2b46b39254fccf5e0c6052c9751..892374bdbca472c418ccc744fe7f4e7902d62818 100644
--- a/substrate/frame/support/src/traits/members.rs
+++ b/substrate/frame/support/src/traits/members.rs
@@ -297,6 +297,16 @@ pub trait RankedMembers {
 	fn demote(who: &Self::AccountId) -> DispatchResult;
 }
 
+/// Handler that can deal with the swap of two members.
+pub trait RankedMembersSwapHandler<AccountId, Rank> {
+	/// Member `old` was swapped with `new` at `rank`.
+	fn swapped(who: &AccountId, new_who: &AccountId, rank: Rank);
+}
+
+impl<AccountId, Rank> RankedMembersSwapHandler<AccountId, Rank> for () {
+	fn swapped(_: &AccountId, _: &AccountId, _: Rank) {}
+}
+
 /// Trait for type that can handle the initialization of account IDs at genesis.
 pub trait InitializeMembers<AccountId> {
 	/// Initialize the members to the given `members`.