diff --git a/prdoc/pr_3889.prdoc b/prdoc/pr_3889.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..b32ffcc214c055879b546c4b19d3339f37dde71b
--- /dev/null
+++ b/prdoc/pr_3889.prdoc
@@ -0,0 +1,14 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Allow privileged virtual bond into pallet Staking
+
+doc:
+  - audience: Runtime Dev
+    description: |
+      Introduces a new low level API to allow privileged virtual bond into pallet Staking. This allows other pallets
+      to stake funds into staking pallet while managing the fund lock and unlocking process themselves.
+
+crates:
+  - name: pallet-staking
+
diff --git a/substrate/frame/nomination-pools/src/mock.rs b/substrate/frame/nomination-pools/src/mock.rs
index b9301a400953e0c4f9c7589b3f74241d094c597f..686402b843492e8f92bb26231a7ed3becf0c0e03 100644
--- a/substrate/frame/nomination-pools/src/mock.rs
+++ b/substrate/frame/nomination-pools/src/mock.rs
@@ -131,6 +131,10 @@ impl sp_staking::StakingInterface for StakingMock {
 		Ok(())
 	}
 
+	fn update_payee(_stash: &Self::AccountId, _reward_acc: &Self::AccountId) -> DispatchResult {
+		unimplemented!("method currently not used in testing")
+	}
+
 	fn chill(_: &Self::AccountId) -> sp_runtime::DispatchResult {
 		Ok(())
 	}
@@ -223,6 +227,10 @@ impl sp_staking::StakingInterface for StakingMock {
 	fn max_exposure_page_size() -> sp_staking::Page {
 		unimplemented!("method currently not used in testing")
 	}
+
+	fn slash_reward_fraction() -> Perbill {
+		unimplemented!("method currently not used in testing")
+	}
 }
 
 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs
index 9461daefed65e74c778a248a0bbaeeedd3ee6222..67a86b86226cfb1aa5da4c8bca794f315f348009 100644
--- a/substrate/frame/staking/src/ledger.rs
+++ b/substrate/frame/staking/src/ledger.rs
@@ -33,13 +33,14 @@
 
 use frame_support::{
 	defensive, ensure,
-	traits::{Defensive, LockableCurrency, WithdrawReasons},
+	traits::{Defensive, LockableCurrency},
 };
 use sp_staking::StakingAccount;
 use sp_std::prelude::*;
 
 use crate::{
-	BalanceOf, Bonded, Config, Error, Ledger, Payee, RewardDestination, StakingLedger, STAKING_ID,
+	BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger,
+	VirtualStakers, STAKING_ID,
 };
 
 #[cfg(any(feature = "runtime-benchmarks", test))]
@@ -187,7 +188,17 @@ impl<T: Config> StakingLedger<T> {
 			return Err(Error::<T>::NotStash)
 		}
 
-		T::Currency::set_lock(STAKING_ID, &self.stash, self.total, WithdrawReasons::all());
+		// We skip locking virtual stakers.
+		if !Pallet::<T>::is_virtual_staker(&self.stash) {
+			// for direct stakers, update lock on stash based on ledger.
+			T::Currency::set_lock(
+				STAKING_ID,
+				&self.stash,
+				self.total,
+				frame_support::traits::WithdrawReasons::all(),
+			);
+		}
+
 		Ledger::<T>::insert(
 			&self.controller().ok_or_else(|| {
 				defensive!("update called on a ledger that is not bonded.");
@@ -204,22 +215,22 @@ impl<T: Config> StakingLedger<T> {
 	/// It sets the reward preferences for the bonded stash.
 	pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
 		if <Bonded<T>>::contains_key(&self.stash) {
-			Err(Error::<T>::AlreadyBonded)
-		} else {
-			<Payee<T>>::insert(&self.stash, payee);
-			<Bonded<T>>::insert(&self.stash, &self.stash);
-			self.update()
+			return Err(Error::<T>::AlreadyBonded)
 		}
+
+		<Payee<T>>::insert(&self.stash, payee);
+		<Bonded<T>>::insert(&self.stash, &self.stash);
+		self.update()
 	}
 
 	/// Sets the ledger Payee.
 	pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
 		if !<Bonded<T>>::contains_key(&self.stash) {
-			Err(Error::<T>::NotStash)
-		} else {
-			<Payee<T>>::insert(&self.stash, payee);
-			Ok(())
+			return Err(Error::<T>::NotStash)
 		}
+
+		<Payee<T>>::insert(&self.stash, payee);
+		Ok(())
 	}
 
 	/// Sets the ledger controller to its stash.
@@ -252,12 +263,16 @@ impl<T: Config> StakingLedger<T> {
 		let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
 
 		<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
-			T::Currency::remove_lock(STAKING_ID, &ledger.stash);
 			Ledger::<T>::remove(controller);
-
 			<Bonded<T>>::remove(&stash);
 			<Payee<T>>::remove(&stash);
 
+			// kill virtual staker if it exists.
+			if <VirtualStakers<T>>::take(&stash).is_none() {
+				// if not virtual staker, clear locks.
+				T::Currency::remove_lock(STAKING_ID, &ledger.stash);
+			}
+
 			Ok(())
 		})?
 	}
diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs
index 6db462c1a70fbce4ed2d531bf9ad4d837e83b585..b46b863c016e5bf31213c903f46e3c05bfd1de5a 100644
--- a/substrate/frame/staking/src/mock.rs
+++ b/substrate/frame/staking/src/mock.rs
@@ -249,17 +249,21 @@ parameter_types! {
 	pub static LedgerSlashPerEra:
 		(BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) =
 		(Zero::zero(), BTreeMap::new());
+	pub static SlashObserver: BTreeMap<AccountId, BalanceOf<Test>> = BTreeMap::new();
 }
 
 pub struct EventListenerMock;
 impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
 	fn on_slash(
-		_pool_account: &AccountId,
+		pool_account: &AccountId,
 		slashed_bonded: Balance,
 		slashed_chunks: &BTreeMap<EraIndex, Balance>,
-		_total_slashed: Balance,
+		total_slashed: Balance,
 	) {
 		LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
+		SlashObserver::mutate(|map| {
+			map.insert(*pool_account, map.get(pool_account).unwrap_or(&0) + total_slashed)
+		});
 	}
 }
 
@@ -598,6 +602,21 @@ pub(crate) fn bond_nominator(who: AccountId, val: Balance, target: Vec<AccountId
 	assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
 }
 
+pub(crate) fn bond_virtual_nominator(
+	who: AccountId,
+	payee: AccountId,
+	val: Balance,
+	target: Vec<AccountId>,
+) {
+	// In a real scenario, `who` is a keyless account managed by another pallet which provides for
+	// it.
+	System::inc_providers(&who);
+
+	// Bond who virtually.
+	assert_ok!(<Staking as sp_staking::StakingUnchecked>::virtual_bond(&who, val, &payee));
+	assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
+}
+
 /// Progress to the given block, triggering session and era changes as we progress.
 ///
 /// This will finalize the previous block, initialize up to the given block, essentially simulating
diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs
index 2f43e4847e451a7cc10413bb58e3d98d82577e9f..0c0ef0dbf463c396e8f14c20cd1f87f276e5e39e 100644
--- a/substrate/frame/staking/src/pallet/impls.rs
+++ b/substrate/frame/staking/src/pallet/impls.rs
@@ -28,15 +28,18 @@ use frame_support::{
 	pallet_prelude::*,
 	traits::{
 		Currency, Defensive, DefensiveSaturating, EstimateNextNewSession, Get, Imbalance,
-		InspectLockableCurrency, Len, OnUnbalanced, TryCollect, UnixTime,
+		InspectLockableCurrency, Len, LockableCurrency, OnUnbalanced, TryCollect, UnixTime,
 	},
 	weights::Weight,
 };
 use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
 use pallet_session::historical;
 use sp_runtime::{
-	traits::{Bounded, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero},
-	Perbill, Percent,
+	traits::{
+		Bounded, CheckedAdd, CheckedSub, Convert, One, SaturatedConversion, Saturating,
+		StaticLookup, Zero,
+	},
+	ArithmeticError, Perbill, Percent,
 };
 use sp_staking::{
 	currency_to_vote::CurrencyToVote,
@@ -149,6 +152,39 @@ impl<T: Config> Pallet<T> {
 		Self::slashable_balance_of_vote_weight(who, issuance)
 	}
 
+	pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf<T>) -> DispatchResult {
+		let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?;
+
+		// for virtual stakers, we don't need to check the balance. Since they are only accessed
+		// via low level apis, we can assume that the caller has done the due diligence.
+		let extra = if Self::is_virtual_staker(stash) {
+			additional
+		} else {
+			// additional amount or actual balance of stash whichever is lower.
+			additional.min(
+				T::Currency::free_balance(stash)
+					.checked_sub(&ledger.total)
+					.ok_or(ArithmeticError::Overflow)?,
+			)
+		};
+
+		ledger.total = ledger.total.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
+		ledger.active = ledger.active.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
+		// last check: the new active amount of ledger must be more than ED.
+		ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientBond);
+
+		// NOTE: ledger must be updated prior to calling `Self::weight_of`.
+		ledger.update()?;
+		// update this staker in the sorted list, if they exist in it.
+		if T::VoterList::contains(stash) {
+			let _ = T::VoterList::on_update(&stash, Self::weight_of(stash)).defensive();
+		}
+
+		Self::deposit_event(Event::<T>::Bonded { stash: stash.clone(), amount: extra });
+
+		Ok(())
+	}
+
 	pub(super) fn do_withdraw_unbonded(
 		controller: &T::AccountId,
 		num_slashing_spans: u32,
@@ -1132,6 +1168,11 @@ impl<T: Config> Pallet<T> {
 	) -> Exposure<T::AccountId, BalanceOf<T>> {
 		EraInfo::<T>::get_full_exposure(era, account)
 	}
+
+	/// Whether `who` is a virtual staker whose funds are managed by another pallet.
+	pub(crate) fn is_virtual_staker(who: &T::AccountId) -> bool {
+		VirtualStakers::<T>::contains_key(who)
+	}
 }
 
 impl<T: Config> Pallet<T> {
@@ -1748,6 +1789,23 @@ impl<T: Config> StakingInterface for Pallet<T> {
 			.map(|_| ())
 	}
 
+	fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult {
+		// Since virtual stakers are not allowed to compound their rewards as this pallet does not
+		// manage their locks, we do not allow reward account to be set same as stash. For
+		// external pallets that manage the virtual bond, they can claim rewards and re-bond them.
+		ensure!(
+			!Self::is_virtual_staker(stash) || stash != reward_acc,
+			Error::<T>::RewardDestinationRestricted
+		);
+
+		// since controller is deprecated and this function is never used for old ledgers with
+		// distinct controllers, we can safely assume that stash is the controller.
+		Self::set_payee(
+			RawOrigin::Signed(stash.clone()).into(),
+			RewardDestination::Account(reward_acc.clone()),
+		)
+	}
+
 	fn chill(who: &Self::AccountId) -> DispatchResult {
 		// defensive-only: any account bonded via this interface has the stash set as the
 		// controller, but we have to be sure. Same comment anywhere else that we read this.
@@ -1832,6 +1890,10 @@ impl<T: Config> StakingInterface for Pallet<T> {
 		}
 	}
 
+	fn slash_reward_fraction() -> Perbill {
+		SlashRewardFraction::<T>::get()
+	}
+
 	sp_staking::runtime_benchmarks_enabled! {
 		fn nominations(who: &Self::AccountId) -> Option<Vec<T::AccountId>> {
 			Nominators::<T>::get(who).map(|n| n.targets.into_inner())
@@ -1860,6 +1922,55 @@ impl<T: Config> StakingInterface for Pallet<T> {
 	}
 }
 
+impl<T: Config> sp_staking::StakingUnchecked for Pallet<T> {
+	fn migrate_to_virtual_staker(who: &Self::AccountId) {
+		T::Currency::remove_lock(crate::STAKING_ID, who);
+		VirtualStakers::<T>::insert(who, ());
+	}
+
+	/// Virtually bonds `keyless_who` to `payee` with `value`.
+	///
+	/// The payee must not be the same as the `keyless_who`.
+	fn virtual_bond(
+		keyless_who: &Self::AccountId,
+		value: Self::Balance,
+		payee: &Self::AccountId,
+	) -> DispatchResult {
+		if StakingLedger::<T>::is_bonded(StakingAccount::Stash(keyless_who.clone())) {
+			return Err(Error::<T>::AlreadyBonded.into())
+		}
+
+		// check if payee not same as who.
+		ensure!(keyless_who != payee, Error::<T>::RewardDestinationRestricted);
+
+		// mark this pallet as consumer of `who`.
+		frame_system::Pallet::<T>::inc_consumers(&keyless_who).map_err(|_| Error::<T>::BadState)?;
+
+		// mark who as a virtual staker.
+		VirtualStakers::<T>::insert(keyless_who, ());
+
+		Self::deposit_event(Event::<T>::Bonded { stash: keyless_who.clone(), amount: value });
+		let ledger = StakingLedger::<T>::new(keyless_who.clone(), value);
+
+		ledger.bond(RewardDestination::Account(payee.clone()))?;
+
+		Ok(())
+	}
+
+	#[cfg(feature = "runtime-benchmarks")]
+	fn migrate_to_direct_staker(who: &Self::AccountId) {
+		assert!(VirtualStakers::<T>::contains_key(who));
+		let ledger = StakingLedger::<T>::get(Stash(who.clone())).unwrap();
+		T::Currency::set_lock(
+			crate::STAKING_ID,
+			who,
+			ledger.total,
+			frame_support::traits::WithdrawReasons::all(),
+		);
+		VirtualStakers::<T>::remove(who);
+	}
+}
+
 #[cfg(any(test, feature = "try-runtime"))]
 impl<T: Config> Pallet<T> {
 	pub(crate) fn do_try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
@@ -1980,16 +2091,44 @@ impl<T: Config> Pallet<T> {
 	/// Invariants:
 	/// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking).
 	/// * The ledger's controller and stash matches the associated `Bonded` tuple.
-	/// * Staking locked funds for every bonded stash should be the same as its ledger's total.
+	/// * Staking locked funds for every bonded stash (non virtual stakers) should be the same as
+	/// its ledger's total.
+	/// * For virtual stakers, locked funds should be zero and payee should be non-stash account.
 	/// * Staking ledger and bond are not corrupted.
 	fn check_ledgers() -> Result<(), TryRuntimeError> {
 		Bonded::<T>::iter()
 			.map(|(stash, ctrl)| {
 				// ensure locks consistency.
-				ensure!(
-					Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok),
-					"bond, ledger and/or staking lock inconsistent for a bonded stash."
-				);
+				if VirtualStakers::<T>::contains_key(stash.clone()) {
+					ensure!(
+						T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(),
+						"virtual stakers should not have any locked balance"
+					);
+					ensure!(
+						<Bonded<T>>::get(stash.clone()).unwrap() == stash.clone(),
+						"stash and controller should be same"
+					);
+					ensure!(
+						Ledger::<T>::get(stash.clone()).unwrap().stash == stash,
+						"ledger corrupted for virtual staker"
+					);
+					let reward_destination = <Payee<T>>::get(stash.clone()).unwrap();
+					if let RewardDestination::Account(payee) = reward_destination {
+						ensure!(
+							payee != stash.clone(),
+							"reward destination should not be same as stash for virtual staker"
+						);
+					} else {
+						return Err(DispatchError::Other(
+							"reward destination must be of account variant for virtual staker",
+						));
+					}
+				} else {
+					ensure!(
+						Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok),
+						"bond, ledger and/or staking lock inconsistent for a bonded stash."
+					);
+				}
 
 				// ensure ledger consistency.
 				Self::ensure_ledger_consistent(ctrl)
diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs
index 2e5b3aa7b873e2c123b3176a4e80d6e0cd1b6177..76ddad6f1359a57e2fc361a010816724799f238b 100644
--- a/substrate/frame/staking/src/pallet/mod.rs
+++ b/substrate/frame/staking/src/pallet/mod.rs
@@ -32,7 +32,7 @@ use frame_support::{
 };
 use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
 use sp_runtime::{
-	traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero},
+	traits::{SaturatedConversion, StaticLookup, Zero},
 	ArithmeticError, Perbill, Percent,
 };
 
@@ -379,6 +379,15 @@ pub mod pallet {
 	pub type Nominators<T: Config> =
 		CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations<T>>;
 
+	/// Stakers whose funds are managed by other pallets.
+	///
+	/// This pallet does not apply any locks on them, therefore they are only virtually bonded. They
+	/// are expected to be keyless accounts and hence should not be allowed to mutate their ledger
+	/// directly via this pallet. Instead, these accounts are managed by other pallets and accessed
+	/// via low level apis. We keep track of them to do minimal integrity checks.
+	#[pallet::storage]
+	pub type VirtualStakers<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>;
+
 	/// The maximum nominator count before we stop allowing new validators to join.
 	///
 	/// When this value is not set, no limits are enforced.
@@ -858,6 +867,10 @@ pub mod pallet {
 		ControllerDeprecated,
 		/// Cannot reset a ledger.
 		CannotRestoreLedger,
+		/// Provided reward destination is not allowed.
+		RewardDestinationRestricted,
+		/// Not enough funds available to withdraw.
+		NotEnoughFunds,
 	}
 
 	#[pallet::hooks]
@@ -985,29 +998,7 @@ pub mod pallet {
 			#[pallet::compact] max_additional: BalanceOf<T>,
 		) -> DispatchResult {
 			let stash = ensure_signed(origin)?;
-			let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?;
-
-			let stash_balance = T::Currency::free_balance(&stash);
-			if let Some(extra) = stash_balance.checked_sub(&ledger.total) {
-				let extra = extra.min(max_additional);
-				ledger.total += extra;
-				ledger.active += extra;
-				// Last check: the new active amount of ledger must be more than ED.
-				ensure!(
-					ledger.active >= T::Currency::minimum_balance(),
-					Error::<T>::InsufficientBond
-				);
-
-				// NOTE: ledger must be updated prior to calling `Self::weight_of`.
-				ledger.update()?;
-				// update this staker in the sorted list, if they exist in it.
-				if T::VoterList::contains(&stash) {
-					let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive();
-				}
-
-				Self::deposit_event(Event::<T>::Bonded { stash, amount: extra });
-			}
-			Ok(())
+			Self::do_bond_extra(&stash, max_additional)
 		}
 
 		/// Schedule a portion of the stash to be unlocked ready for transfer out after the bond
diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs
index 709fd1441ec3af313220486aa8000c959e30fd07..2011e9eb830144960488e7fe40dc82ed1fcfa1b6 100644
--- a/substrate/frame/staking/src/slashing.rs
+++ b/substrate/frame/staking/src/slashing.rs
@@ -609,8 +609,13 @@ pub fn do_slash<T: Config>(
 		};
 
 	let value = ledger.slash(value, T::Currency::minimum_balance(), slash_era);
+	if value.is_zero() {
+		// nothing to do
+		return
+	}
 
-	if !value.is_zero() {
+	// Skip slashing for virtual stakers. The pallets managing them should handle the slashing.
+	if !Pallet::<T>::is_virtual_staker(stash) {
 		let (imbalance, missing) = T::Currency::slash(stash, value);
 		slashed_imbalance.subsume(imbalance);
 
@@ -618,17 +623,14 @@ pub fn do_slash<T: Config>(
 			// deduct overslash from the reward payout
 			*reward_payout = reward_payout.saturating_sub(missing);
 		}
+	}
 
-		let _ = ledger
-			.update()
-			.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
+	let _ = ledger
+		.update()
+		.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
 
-		// trigger the event
-		<Pallet<T>>::deposit_event(super::Event::<T>::Slashed {
-			staker: stash.clone(),
-			amount: value,
-		});
-	}
+	// trigger the event
+	<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
 }
 
 /// Apply a previously-unapplied slash.
diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs
index a5c9abe2f176233f1f66e9872fde0a6bde99bbcb..87f6fd424bd75b6879bee1ee996a58b283da7ee5 100644
--- a/substrate/frame/staking/src/tests.rs
+++ b/substrate/frame/staking/src/tests.rs
@@ -27,7 +27,7 @@ use frame_support::{
 	assert_noop, assert_ok, assert_storage_noop,
 	dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo},
 	pallet_prelude::*,
-	traits::{Currency, Get, ReservableCurrency},
+	traits::{Currency, Get, InspectLockableCurrency, ReservableCurrency},
 };
 
 use mock::*;
@@ -623,12 +623,8 @@ fn nominating_and_rewards_should_work() {
 			));
 			assert_ok!(Staking::nominate(RuntimeOrigin::signed(1), vec![11, 21, 31]));
 
-			assert_ok!(Staking::bond(
-				RuntimeOrigin::signed(3),
-				1000,
-				RewardDestination::Account(3)
-			));
-			assert_ok!(Staking::nominate(RuntimeOrigin::signed(3), vec![11, 21, 41]));
+			// the second nominator is virtual.
+			bond_virtual_nominator(3, 333, 1000, vec![11, 21, 41]);
 
 			// the total reward for era 0
 			let total_payout_0 = current_total_payout_for_duration(reward_time_per_era());
@@ -694,10 +690,12 @@ fn nominating_and_rewards_should_work() {
 			);
 			// Nominator 3: has [400/1800 ~ 2/9 from 10] + [600/2200 ~ 3/11 from 21]'s reward. ==>
 			// 2/9 + 3/11
+			assert_eq!(Balances::total_balance(&3), initial_balance);
+			// 333 is the reward destination for 3.
 			assert_eq_error_rate!(
-				Balances::total_balance(&3),
-				initial_balance + (2 * payout_for_11 / 9 + 3 * payout_for_21 / 11),
-				2,
+				Balances::total_balance(&333),
+				2 * payout_for_11 / 9 + 3 * payout_for_21 / 11,
+				2
 			);
 
 			// Validator 11: got 800 / 1800 external stake => 8/18 =? 4/9 => Validator's share = 5/9
@@ -1893,7 +1891,7 @@ fn reap_stash_works() {
 		.balance_factor(10)
 		.build_and_execute(|| {
 			// given
-			assert_eq!(Balances::free_balance(11), 10 * 1000);
+			assert_eq!(Balances::balance_locked(STAKING_ID, &11), 10 * 1000);
 			assert_eq!(Staking::bonded(&11), Some(11));
 
 			assert!(<Ledger<Test>>::contains_key(&11));
@@ -1919,6 +1917,8 @@ fn reap_stash_works() {
 			assert!(!<Bonded<Test>>::contains_key(&11));
 			assert!(!<Validators<Test>>::contains_key(&11));
 			assert!(!<Payee<Test>>::contains_key(&11));
+			// lock is removed.
+			assert_eq!(Balances::balance_locked(STAKING_ID, &11), 0);
 		});
 }
 
@@ -6849,6 +6849,226 @@ mod staking_interface {
 	}
 }
 
+mod staking_unchecked {
+	use sp_staking::{Stake, StakingInterface, StakingUnchecked};
+
+	use super::*;
+
+	#[test]
+	fn virtual_bond_does_not_lock() {
+		ExtBuilder::default().build_and_execute(|| {
+			mock::start_active_era(1);
+			assert_eq!(Balances::free_balance(10), 1);
+			// 10 can bond more than its balance amount since we do not require lock for virtual
+			// bonding.
+			assert_ok!(<Staking as StakingUnchecked>::virtual_bond(&10, 100, &15));
+			// nothing is locked on 10.
+			assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0);
+			// adding more balance does not lock anything as well.
+			assert_ok!(<Staking as StakingInterface>::bond_extra(&10, 1000));
+			// but ledger is updated correctly.
+			assert_eq!(
+				<Staking as StakingInterface>::stake(&10),
+				Ok(Stake { total: 1100, active: 1100 })
+			);
+
+			// lets try unbonding some amount.
+			assert_ok!(<Staking as StakingInterface>::unbond(&10, 200));
+			assert_eq!(
+				Staking::ledger(10.into()).unwrap(),
+				StakingLedgerInspect {
+					stash: 10,
+					total: 1100,
+					active: 1100 - 200,
+					unlocking: bounded_vec![UnlockChunk { value: 200, era: 1 + 3 }],
+					legacy_claimed_rewards: bounded_vec![],
+				}
+			);
+
+			assert_eq!(
+				<Staking as StakingInterface>::stake(&10),
+				Ok(Stake { total: 1100, active: 900 })
+			);
+			// still no locks.
+			assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0);
+
+			mock::start_active_era(2);
+			// cannot withdraw without waiting for unbonding period.
+			assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
+			assert_eq!(
+				<Staking as StakingInterface>::stake(&10),
+				Ok(Stake { total: 1100, active: 900 })
+			);
+
+			// in era 4, 10 can withdraw unlocking amount.
+			mock::start_active_era(4);
+			assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
+			assert_eq!(
+				<Staking as StakingInterface>::stake(&10),
+				Ok(Stake { total: 900, active: 900 })
+			);
+
+			// unbond all.
+			assert_ok!(<Staking as StakingInterface>::unbond(&10, 900));
+			assert_eq!(
+				<Staking as StakingInterface>::stake(&10),
+				Ok(Stake { total: 900, active: 0 })
+			);
+			mock::start_active_era(7);
+			assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
+
+			// ensure withdrawing all amount cleans up storage.
+			assert_eq!(Staking::ledger(10.into()), Err(Error::<Test>::NotStash));
+			assert_eq!(VirtualStakers::<Test>::contains_key(10), false);
+		})
+	}
+
+	#[test]
+	fn virtual_staker_cannot_pay_reward_to_self_account() {
+		ExtBuilder::default().build_and_execute(|| {
+			// cannot set payee to self
+			assert_noop!(
+				<Staking as StakingUnchecked>::virtual_bond(&10, 100, &10),
+				Error::<Test>::RewardDestinationRestricted
+			);
+
+			// to another account works
+			assert_ok!(<Staking as StakingUnchecked>::virtual_bond(&10, 100, &11));
+
+			// cannot set via set_payee as well.
+			assert_noop!(
+				<Staking as StakingInterface>::update_payee(&10, &10),
+				Error::<Test>::RewardDestinationRestricted
+			);
+		});
+	}
+
+	#[test]
+	fn virtual_staker_cannot_bond_again() {
+		ExtBuilder::default().build_and_execute(|| {
+			// 200 virtual bonds
+			bond_virtual_nominator(200, 201, 500, vec![11, 21]);
+
+			// Tries bonding again
+			assert_noop!(
+				<Staking as StakingUnchecked>::virtual_bond(&200, 200, &201),
+				Error::<Test>::AlreadyBonded
+			);
+
+			// And again with a different reward destination.
+			assert_noop!(
+				<Staking as StakingUnchecked>::virtual_bond(&200, 200, &202),
+				Error::<Test>::AlreadyBonded
+			);
+
+			// Direct bond is not allowed as well.
+			assert_noop!(
+				<Staking as StakingInterface>::bond(&200, 200, &202),
+				Error::<Test>::AlreadyBonded
+			);
+		});
+	}
+
+	#[test]
+	fn normal_staker_cannot_virtual_bond() {
+		ExtBuilder::default().build_and_execute(|| {
+			// 101 is a nominator trying to virtual bond
+			assert_noop!(
+				<Staking as StakingUnchecked>::virtual_bond(&101, 200, &102),
+				Error::<Test>::AlreadyBonded
+			);
+
+			// validator 21 tries to virtual bond
+			assert_noop!(
+				<Staking as StakingUnchecked>::virtual_bond(&21, 200, &22),
+				Error::<Test>::AlreadyBonded
+			);
+		});
+	}
+
+	#[test]
+	fn migrate_virtual_staker() {
+		ExtBuilder::default().build_and_execute(|| {
+			// give some balance to 200
+			Balances::make_free_balance_be(&200, 2000);
+
+			// stake
+			assert_ok!(Staking::bond(RuntimeOrigin::signed(200), 1000, RewardDestination::Staked));
+			assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 1000);
+
+			// migrate them to virtual staker
+			<Staking as StakingUnchecked>::migrate_to_virtual_staker(&200);
+			// payee needs to be updated to a non-stash account.
+			assert_ok!(<Staking as StakingInterface>::update_payee(&200, &201));
+
+			// ensure the balance is not locked anymore
+			assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 0);
+
+			// and they are marked as virtual stakers
+			assert_eq!(Pallet::<Test>::is_virtual_staker(&200), true);
+		});
+	}
+
+	#[test]
+	fn virtual_nominators_are_lazily_slashed() {
+		ExtBuilder::default().build_and_execute(|| {
+			mock::start_active_era(1);
+			let slash_percent = Perbill::from_percent(5);
+			let initial_exposure = Staking::eras_stakers(active_era(), &11);
+			// 101 is a nominator for 11
+			assert_eq!(initial_exposure.others.first().unwrap().who, 101);
+			// make 101 a virtual nominator
+			<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
+			// set payee different to self.
+			assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
+
+			// cache values
+			let nominator_stake = Staking::ledger(101.into()).unwrap().active;
+			let nominator_balance = balances(&101).0;
+			let validator_stake = Staking::ledger(11.into()).unwrap().active;
+			let validator_balance = balances(&11).0;
+			let exposed_stake = initial_exposure.total;
+			let exposed_validator = initial_exposure.own;
+			let exposed_nominator = initial_exposure.others.first().unwrap().value;
+
+			// 11 goes offline
+			on_offence_now(
+				&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
+				&[slash_percent],
+			);
+
+			let slash_amount = slash_percent * exposed_stake;
+			let validator_share =
+				Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
+			let nominator_share =
+				Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
+
+			// both slash amounts need to be positive for the test to make sense.
+			assert!(validator_share > 0);
+			assert!(nominator_share > 0);
+
+			// both stakes must have been decreased pro-rata.
+			assert_eq!(
+				Staking::ledger(101.into()).unwrap().active,
+				nominator_stake - nominator_share
+			);
+			assert_eq!(
+				Staking::ledger(11.into()).unwrap().active,
+				validator_stake - validator_share
+			);
+
+			// validator balance is slashed as usual
+			assert_eq!(balances(&11).0, validator_balance - validator_share);
+			// Because slashing happened.
+			assert!(is_disabled(11));
+
+			// but virtual nominator's balance is not slashed.
+			assert_eq!(Balances::free_balance(&101), nominator_balance);
+			// but slash is broadcasted to slash observers.
+			assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
+		})
+	}
+}
 mod ledger {
 	use super::*;
 
@@ -7327,7 +7547,6 @@ mod ledger {
 
 mod ledger_recovery {
 	use super::*;
-	use frame_support::traits::InspectLockableCurrency;
 
 	#[test]
 	fn inspect_recovery_ledger_simple_works() {
diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs
index 11b7ef41b9a7b06e1b67d43abb58e9aacc33d8ab..ad6cc6e2f4ffdd8241d951cdd8ec2a9faacc8254 100644
--- a/substrate/primitives/staking/src/lib.rs
+++ b/substrate/primitives/staking/src/lib.rs
@@ -29,7 +29,7 @@ use core::ops::Sub;
 use scale_info::TypeInfo;
 use sp_runtime::{
 	traits::{AtLeast32BitUnsigned, Zero},
-	DispatchError, DispatchResult, RuntimeDebug, Saturating,
+	DispatchError, DispatchResult, Perbill, RuntimeDebug, Saturating,
 };
 
 pub mod offence;
@@ -254,6 +254,9 @@ pub trait StakingInterface {
 	/// schedules have reached their unlocking era should allow more calls to this function.
 	fn unbond(stash: &Self::AccountId, value: Self::Balance) -> DispatchResult;
 
+	/// Update the reward destination for the ledger associated with the stash.
+	fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult;
+
 	/// Unlock any funds schedule to unlock before or at the current era.
 	///
 	/// Returns whether the stash was killed because of this withdraw or not.
@@ -274,7 +277,7 @@ pub trait StakingInterface {
 	/// Checks whether an account `staker` has been exposed in an era.
 	fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool;
 
-	/// Return the status of the given staker, `None` if not staked at all.
+	/// Return the status of the given staker, `Err` if not staked at all.
 	fn status(who: &Self::AccountId) -> Result<StakerStatus<Self::AccountId>, DispatchError>;
 
 	/// Checks whether or not this is a validator account.
@@ -290,6 +293,9 @@ pub trait StakingInterface {
 		}
 	}
 
+	/// Returns the fraction of the slash to be rewarded to reporter.
+	fn slash_reward_fraction() -> Perbill;
+
 	#[cfg(feature = "runtime-benchmarks")]
 	fn max_exposure_page_size() -> Page;
 
@@ -304,6 +310,34 @@ pub trait StakingInterface {
 	fn set_current_era(era: EraIndex);
 }
 
+/// Set of low level apis to manipulate staking ledger.
+///
+/// These apis bypass some or all safety checks and should only be used if you know what you are
+/// doing.
+pub trait StakingUnchecked: StakingInterface {
+	/// Migrate an existing staker to a virtual staker.
+	///
+	/// It would release all funds held by the implementation pallet.
+	fn migrate_to_virtual_staker(who: &Self::AccountId);
+
+	/// Book-keep a new bond for `keyless_who` without applying any locks (hence virtual).
+	///
+	/// It is important that `keyless_who` is a keyless account and therefore cannot interact with
+	/// staking pallet directly. Caller is responsible for ensuring the passed amount is locked and
+	/// valid.
+	fn virtual_bond(
+		keyless_who: &Self::AccountId,
+		value: Self::Balance,
+		payee: &Self::AccountId,
+	) -> DispatchResult;
+
+	/// Migrate a virtual staker to a direct staker.
+	///
+	/// Only used for testing.
+	#[cfg(feature = "runtime-benchmarks")]
+	fn migrate_to_direct_staker(who: &Self::AccountId);
+}
+
 /// The amount of exposure for an era that an individual nominator has (susceptible to slashing).
 #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
 pub struct IndividualExposure<AccountId, Balance: HasCompact> {