1#![cfg_attr(not(feature = "std"), no_std)]
126#![deny(rustdoc::broken_intra_doc_links)]
127
128mod impls;
129pub mod migration;
130#[cfg(test)]
131mod mock;
132#[cfg(test)]
133mod tests;
134pub mod types;
135
136extern crate alloc;
137
138pub use pallet::*;
139
140use types::*;
141
142use core::convert::TryInto;
143use frame_support::{
144 pallet_prelude::*,
145 traits::{
146 fungible::{
147 hold::{
148 Balanced as FunHoldBalanced, Inspect as FunHoldInspect, Mutate as FunHoldMutate,
149 },
150 Balanced, Inspect as FunInspect, Mutate as FunMutate,
151 },
152 tokens::{fungible::Credit, Fortitude, Precision, Preservation, Restriction},
153 Defensive, DefensiveOption, Imbalance, OnUnbalanced,
154 },
155};
156use sp_io::hashing::blake2_256;
157use sp_runtime::{
158 traits::{CheckedAdd, CheckedSub, TrailingZeroInput, Zero},
159 ArithmeticError, Debug, DispatchResult, Perbill, Saturating,
160};
161use sp_staking::{Agent, Delegator, EraIndex, StakingInterface, StakingUnchecked};
162
163pub const LOG_TARGET: &str = "runtime::delegated-staking";
165#[macro_export]
167macro_rules! log {
168 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
169 log::$level!(
170 target: $crate::LOG_TARGET,
171 concat!("[{:?}] ๐โโ๏ธ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
172 )
173 };
174}
175pub type BalanceOf<T> =
176 <<T as Config>::Currency as FunInspect<<T as frame_system::Config>::AccountId>>::Balance;
177
178use frame_system::{ensure_signed, pallet_prelude::*, RawOrigin};
179
180#[frame_support::pallet]
181pub mod pallet {
182 use super::*;
183
184 const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
186 #[pallet::pallet]
187 #[pallet::storage_version(STORAGE_VERSION)]
188 pub struct Pallet<T>(PhantomData<T>);
189
190 #[pallet::config]
191 pub trait Config: frame_system::Config {
192 #[allow(deprecated)]
194 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
195
196 #[pallet::constant]
198 type PalletId: Get<frame_support::PalletId>;
199
200 type Currency: FunHoldMutate<Self::AccountId, Reason = Self::RuntimeHoldReason>
202 + FunMutate<Self::AccountId>
203 + FunHoldBalanced<Self::AccountId>;
204
205 type OnSlash: OnUnbalanced<Credit<Self::AccountId, Self::Currency>>;
207
208 #[pallet::constant]
210 type SlashRewardFraction: Get<Perbill>;
211
212 type RuntimeHoldReason: From<HoldReason>;
214
215 type CoreStaking: StakingUnchecked<Balance = BalanceOf<Self>, AccountId = Self::AccountId>;
217 }
218
219 #[pallet::error]
220 pub enum Error<T> {
221 NotAllowed,
223 AlreadyStaking,
225 InvalidRewardDestination,
227 InvalidDelegation,
233 NotEnoughFunds,
235 NotAgent,
237 NotDelegator,
239 BadState,
241 UnappliedSlash,
243 NothingToSlash,
245 WithdrawFailed,
247 NotSupported,
249 }
250
251 #[pallet::composite_enum]
253 pub enum HoldReason {
254 #[codec(index = 0)]
256 StakingDelegation,
257 }
258
259 #[pallet::event]
260 #[pallet::generate_deposit(pub (super) fn deposit_event)]
261 pub enum Event<T: Config> {
262 Delegated { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
264 Released { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
266 Slashed { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
268 MigratedDelegation { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
270 }
271
272 #[pallet::storage]
277 pub type Delegators<T: Config> =
278 CountedStorageMap<_, Twox64Concat, T::AccountId, Delegation<T>, OptionQuery>;
279
280 #[pallet::storage]
282 pub type Agents<T: Config> =
283 CountedStorageMap<_, Twox64Concat, T::AccountId, AgentLedger<T>, OptionQuery>;
284
285 impl<T: Config> Pallet<T> {
289 pub fn register_agent(
304 origin: OriginFor<T>,
305 reward_account: T::AccountId,
306 ) -> DispatchResult {
307 let who = ensure_signed(origin)?;
308
309 ensure!(!Self::is_agent(&who) && !Self::is_delegator(&who), Error::<T>::NotAllowed);
311
312 ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
314
315 Self::do_register_agent(&who, &reward_account);
316 Ok(())
317 }
318
319 pub fn remove_agent(origin: OriginFor<T>) -> DispatchResult {
324 let who = ensure_signed(origin)?;
325 let ledger = AgentLedger::<T>::get(&who).ok_or(Error::<T>::NotAgent)?;
326
327 ensure!(
328 ledger.total_delegated == Zero::zero() &&
329 ledger.pending_slash == Zero::zero() &&
330 ledger.unclaimed_withdrawals == Zero::zero(),
331 Error::<T>::NotAllowed
332 );
333
334 AgentLedger::<T>::remove(&who);
335 Ok(())
336 }
337
338 pub fn migrate_to_agent(
352 origin: OriginFor<T>,
353 reward_account: T::AccountId,
354 ) -> DispatchResult {
355 let who = ensure_signed(origin)?;
356 ensure!(
358 Self::is_direct_staker(&who) && !Self::is_agent(&who) && !Self::is_delegator(&who),
359 Error::<T>::NotAllowed
360 );
361
362 ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
364
365 Self::do_migrate_to_agent(&who, &reward_account)
366 }
367
368 pub fn release_delegation(
375 origin: OriginFor<T>,
376 delegator: T::AccountId,
377 amount: BalanceOf<T>,
378 num_slashing_spans: u32,
379 ) -> DispatchResult {
380 let who = ensure_signed(origin)?;
381 Self::do_release(
382 Agent::from(who),
383 Delegator::from(delegator),
384 amount,
385 num_slashing_spans,
386 )
387 }
388
389 pub fn migrate_delegation(
399 origin: OriginFor<T>,
400 delegator: T::AccountId,
401 amount: BalanceOf<T>,
402 ) -> DispatchResult {
403 let agent = ensure_signed(origin)?;
404
405 ensure!(!Self::is_agent(&delegator), Error::<T>::NotAllowed);
407 ensure!(!Self::is_delegator(&delegator), Error::<T>::NotAllowed);
408
409 ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
411
412 if amount.is_zero() {
414 return Ok(());
415 }
416
417 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(agent));
419 let balance_remaining = Self::held_balance_of(proxy_delegator.clone());
420 ensure!(balance_remaining >= amount, Error::<T>::NotEnoughFunds);
421
422 Self::do_migrate_delegation(proxy_delegator, Delegator::from(delegator), amount)
423 }
424
425 pub fn delegate_to_agent(
435 origin: OriginFor<T>,
436 agent: T::AccountId,
437 amount: BalanceOf<T>,
438 ) -> DispatchResult {
439 let delegator = ensure_signed(origin)?;
440
441 ensure!(
443 Delegation::<T>::can_delegate(&delegator, &agent),
444 Error::<T>::InvalidDelegation
445 );
446
447 ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
449
450 if amount.is_zero() {
452 return Ok(());
453 }
454
455 Self::do_delegate(Delegator::from(delegator), Agent::from(agent.clone()), amount)?;
457
458 Self::do_bond(Agent::from(agent), amount)
460 }
461 }
462
463 #[pallet::hooks]
464 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
465 #[cfg(feature = "try-runtime")]
466 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
467 Self::do_try_state()
468 }
469 }
470}
471
472impl<T: Config> Pallet<T> {
473 pub fn generate_proxy_delegator(agent: Agent<T::AccountId>) -> Delegator<T::AccountId> {
476 Delegator::from(Self::sub_account(AccountType::ProxyDelegator, agent.get()))
477 }
478
479 fn sub_account(account_type: AccountType, acc: T::AccountId) -> T::AccountId {
481 let entropy = (T::PalletId::get(), acc, account_type).using_encoded(blake2_256);
482 Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
483 .expect("infinite length input; no invalid inputs for type; qed")
484 }
485
486 pub(crate) fn held_balance_of(who: Delegator<T::AccountId>) -> BalanceOf<T> {
488 T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), &who.get())
489 }
490
491 fn is_agent(who: &T::AccountId) -> bool {
493 <Agents<T>>::contains_key(who)
494 }
495
496 fn is_delegator(who: &T::AccountId) -> bool {
498 <Delegators<T>>::contains_key(who)
499 }
500
501 fn is_direct_staker(who: &T::AccountId) -> bool {
503 T::CoreStaking::status(who).is_ok()
504 }
505
506 fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) {
508 AgentLedger::<T>::new(reward_account).update(who);
510 }
511
512 fn do_migrate_to_agent(who: &T::AccountId, reward_account: &T::AccountId) -> DispatchResult {
514 Self::do_register_agent(who, reward_account);
515
516 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(who.clone()));
519
520 let stake = T::CoreStaking::stake(who)?;
522
523 T::CoreStaking::migrate_to_virtual_staker(who)?;
525
526 let amount_to_transfer =
528 T::Currency::reducible_balance(who, Preservation::Expendable, Fortitude::Polite);
529
530 T::Currency::transfer(
532 who,
533 &proxy_delegator.clone().get(),
534 amount_to_transfer,
535 Preservation::Expendable,
536 )?;
537
538 T::CoreStaking::set_payee(who, reward_account)?;
539 Self::do_delegate(proxy_delegator, Agent::from(who.clone()), amount_to_transfer)?;
541 let unclaimed_withdraws = amount_to_transfer
544 .checked_sub(&stake.total)
545 .defensive_ok_or(ArithmeticError::Underflow)?;
546
547 if !unclaimed_withdraws.is_zero() {
548 let mut ledger = AgentLedger::<T>::get(who).ok_or(Error::<T>::NotAgent)?;
549 ledger.unclaimed_withdrawals = ledger
550 .unclaimed_withdrawals
551 .checked_add(&unclaimed_withdraws)
552 .defensive_ok_or(ArithmeticError::Overflow)?;
553 ledger.update(who);
554 }
555
556 Ok(())
557 }
558
559 fn do_bond(agent_acc: Agent<T::AccountId>, amount: BalanceOf<T>) -> DispatchResult {
561 let agent_ledger = AgentLedgerOuter::<T>::get(&agent_acc.get())?;
562
563 let available_to_bond = agent_ledger.available_to_bond();
564 defensive_assert!(amount == available_to_bond, "not expected value to bond");
565
566 if agent_ledger.is_bonded() {
567 T::CoreStaking::bond_extra(&agent_ledger.key, amount)
568 } else {
569 T::CoreStaking::virtual_bond(&agent_ledger.key, amount, agent_ledger.reward_account())
570 }
571 }
572
573 fn do_delegate(
575 delegator: Delegator<T::AccountId>,
576 agent: Agent<T::AccountId>,
577 amount: BalanceOf<T>,
578 ) -> DispatchResult {
579 let agent = agent.get();
581 let delegator = delegator.get();
582
583 let mut ledger = AgentLedger::<T>::get(&agent).ok_or(Error::<T>::NotAgent)?;
584
585 if let Some(mut existing_delegation) = Delegation::<T>::get(&delegator) {
586 ensure!(existing_delegation.agent == agent, Error::<T>::InvalidDelegation);
587 existing_delegation.amount = existing_delegation
589 .amount
590 .checked_add(&amount)
591 .ok_or(ArithmeticError::Overflow)?;
592 existing_delegation
593 } else {
594 Delegation::<T>::new(&agent, amount)
595 }
596 .update(&delegator);
597
598 T::Currency::hold(&HoldReason::StakingDelegation.into(), &delegator, amount)?;
600
601 ledger.total_delegated =
602 ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
603 ledger.update(&agent);
604
605 Self::deposit_event(Event::<T>::Delegated { agent, delegator, amount });
606
607 Ok(())
608 }
609
610 fn do_release(
612 who: Agent<T::AccountId>,
613 delegator: Delegator<T::AccountId>,
614 amount: BalanceOf<T>,
615 num_slashing_spans: u32,
616 ) -> DispatchResult {
617 let agent = who.get();
619 let delegator = delegator.get();
620
621 let mut agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
622 let mut delegation = Delegation::<T>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
623
624 ensure!(delegation.agent == agent, Error::<T>::NotAgent);
626 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
627
628 if agent_ledger.ledger.unclaimed_withdrawals < amount {
630 T::CoreStaking::withdraw_unbonded(agent.clone(), num_slashing_spans)
632 .map_err(|_| Error::<T>::WithdrawFailed)?;
633 agent_ledger = agent_ledger.reload()?;
635 }
636
637 ensure!(agent_ledger.ledger.unclaimed_withdrawals >= amount, Error::<T>::NotEnoughFunds);
639 agent_ledger.remove_unclaimed_withdraw(amount)?.update();
640
641 delegation.amount = delegation
642 .amount
643 .checked_sub(&amount)
644 .defensive_ok_or(ArithmeticError::Overflow)?;
645
646 let released = T::Currency::release(
647 &HoldReason::StakingDelegation.into(),
648 &delegator,
649 amount,
650 Precision::BestEffort,
651 )?;
652
653 defensive_assert!(released == amount, "hold should have been released fully");
654
655 delegation.update(&delegator);
657
658 Self::deposit_event(Event::<T>::Released { agent, delegator, amount });
659
660 Ok(())
661 }
662
663 fn do_migrate_delegation(
665 source_delegator: Delegator<T::AccountId>,
666 destination_delegator: Delegator<T::AccountId>,
667 amount: BalanceOf<T>,
668 ) -> DispatchResult {
669 let source_delegator = source_delegator.get();
671 let destination_delegator = destination_delegator.get();
672
673 let mut source_delegation =
674 Delegators::<T>::get(&source_delegator).defensive_ok_or(Error::<T>::BadState)?;
675
676 ensure!(source_delegation.amount >= amount, Error::<T>::NotEnoughFunds);
678 debug_assert!(
679 !Self::is_delegator(&destination_delegator) && !Self::is_agent(&destination_delegator)
680 );
681
682 let agent = source_delegation.agent.clone();
683 Delegation::<T>::new(&agent, amount).update(&destination_delegator);
685
686 source_delegation.amount = source_delegation
687 .amount
688 .checked_sub(&amount)
689 .defensive_ok_or(Error::<T>::BadState)?;
690
691 T::Currency::transfer_on_hold(
693 &HoldReason::StakingDelegation.into(),
694 &source_delegator,
695 &destination_delegator,
696 amount,
697 Precision::Exact,
698 Restriction::OnHold,
699 Fortitude::Polite,
700 )?;
701
702 source_delegation.update(&source_delegator);
704
705 Self::deposit_event(Event::<T>::MigratedDelegation {
706 agent,
707 delegator: destination_delegator,
708 amount,
709 });
710
711 Ok(())
712 }
713
714 pub fn do_slash(
716 agent: Agent<T::AccountId>,
717 delegator: Delegator<T::AccountId>,
718 amount: BalanceOf<T>,
719 maybe_reporter: Option<T::AccountId>,
720 ) -> DispatchResult {
721 let agent = agent.get();
723 let delegator = delegator.get();
724
725 let agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
726 ensure!(agent_ledger.ledger.pending_slash > Zero::zero(), Error::<T>::NothingToSlash);
728
729 let mut delegation = <Delegators<T>>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
730 ensure!(delegation.agent == agent.clone(), Error::<T>::NotAgent);
731 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
732
733 let (mut credit, missing) =
735 T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount);
736
737 defensive_assert!(missing.is_zero(), "slash should have been fully applied");
738
739 let actual_slash = credit.peek();
740
741 agent_ledger.remove_slash(actual_slash).save();
743 delegation.amount =
744 delegation.amount.checked_sub(&actual_slash).ok_or(ArithmeticError::Overflow)?;
745 delegation.update(&delegator);
746
747 if let Some(reporter) = maybe_reporter {
748 let reward_payout: BalanceOf<T> = T::SlashRewardFraction::get() * actual_slash;
749 let (reporter_reward, rest) = credit.split(reward_payout);
750
751 credit = rest;
753
754 let _ = T::Currency::resolve(&reporter, reporter_reward);
756 }
757
758 T::OnSlash::on_unbalanced(credit);
759
760 Self::deposit_event(Event::<T>::Slashed { agent, delegator, amount });
761
762 Ok(())
763 }
764
765 #[cfg(test)]
767 pub(crate) fn stakeable_balance(who: Agent<T::AccountId>) -> BalanceOf<T> {
768 AgentLedgerOuter::<T>::get(&who.get())
769 .map(|agent| agent.ledger.stakeable_balance())
770 .unwrap_or_default()
771 }
772}
773
774#[cfg(any(test, feature = "try-runtime"))]
775use alloc::collections::btree_map::BTreeMap;
776
777#[cfg(any(test, feature = "try-runtime"))]
778impl<T: Config> Pallet<T> {
779 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
780 let delegation_map = Delegators::<T>::iter().collect::<BTreeMap<_, _>>();
782 let ledger_map = Agents::<T>::iter().collect::<BTreeMap<_, _>>();
783
784 Self::check_delegates(ledger_map.clone())?;
785 Self::check_delegators(delegation_map, ledger_map)?;
786
787 Ok(())
788 }
789
790 fn check_delegates(
791 ledgers: BTreeMap<T::AccountId, AgentLedger<T>>,
792 ) -> Result<(), sp_runtime::TryRuntimeError> {
793 for (agent, ledger) in ledgers {
794 let staked_value = ledger.stakeable_balance();
795
796 if !staked_value.is_zero() {
797 ensure!(
798 matches!(
799 T::CoreStaking::status(&agent).expect("agent should be bonded"),
800 sp_staking::StakerStatus::Nominator(_) | sp_staking::StakerStatus::Idle
801 ),
802 "agent should be bonded and not validator"
803 );
804 }
805
806 ensure!(
807 ledger.stakeable_balance() >=
808 T::CoreStaking::total_stake(&agent).unwrap_or_default(),
809 "Cannot stake more than balance"
810 );
811 }
812
813 Ok(())
814 }
815
816 fn check_delegators(
817 delegations: BTreeMap<T::AccountId, Delegation<T>>,
818 ledger: BTreeMap<T::AccountId, AgentLedger<T>>,
819 ) -> Result<(), sp_runtime::TryRuntimeError> {
820 let mut delegation_aggregation = BTreeMap::<T::AccountId, BalanceOf<T>>::new();
821 for (delegator, delegation) in delegations.iter() {
822 ensure!(!Self::is_agent(delegator), "delegator cannot be an agent");
823 ensure!(!delegation.amount.is_zero(), "delegation amount must be non-zero");
824
825 delegation_aggregation
826 .entry(delegation.agent.clone())
827 .and_modify(|e| *e += delegation.amount)
828 .or_insert(delegation.amount);
829 }
830
831 for (agent, total_delegated) in delegation_aggregation {
832 ensure!(!Self::is_delegator(&agent), "agent cannot be delegator");
833
834 let ledger = ledger.get(&agent).expect("ledger should exist");
835 ensure!(
836 ledger.total_delegated == total_delegated,
837 "ledger total delegated should match delegations"
838 );
839 }
840
841 Ok(())
842 }
843}