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, DispatchResult, Perbill, RuntimeDebug, 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 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(agent));
414 let balance_remaining = Self::held_balance_of(proxy_delegator.clone());
415 ensure!(balance_remaining >= amount, Error::<T>::NotEnoughFunds);
416
417 Self::do_migrate_delegation(proxy_delegator, Delegator::from(delegator), amount)
418 }
419
420 pub fn delegate_to_agent(
430 origin: OriginFor<T>,
431 agent: T::AccountId,
432 amount: BalanceOf<T>,
433 ) -> DispatchResult {
434 let delegator = ensure_signed(origin)?;
435
436 ensure!(
438 Delegation::<T>::can_delegate(&delegator, &agent),
439 Error::<T>::InvalidDelegation
440 );
441
442 ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
444
445 Self::do_delegate(Delegator::from(delegator), Agent::from(agent.clone()), amount)?;
447
448 Self::do_bond(Agent::from(agent), amount)
450 }
451 }
452
453 #[pallet::hooks]
454 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
455 #[cfg(feature = "try-runtime")]
456 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
457 Self::do_try_state()
458 }
459 }
460}
461
462impl<T: Config> Pallet<T> {
463 pub fn generate_proxy_delegator(agent: Agent<T::AccountId>) -> Delegator<T::AccountId> {
466 Delegator::from(Self::sub_account(AccountType::ProxyDelegator, agent.get()))
467 }
468
469 fn sub_account(account_type: AccountType, acc: T::AccountId) -> T::AccountId {
471 let entropy = (T::PalletId::get(), acc, account_type).using_encoded(blake2_256);
472 Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
473 .expect("infinite length input; no invalid inputs for type; qed")
474 }
475
476 pub(crate) fn held_balance_of(who: Delegator<T::AccountId>) -> BalanceOf<T> {
478 T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), &who.get())
479 }
480
481 fn is_agent(who: &T::AccountId) -> bool {
483 <Agents<T>>::contains_key(who)
484 }
485
486 fn is_delegator(who: &T::AccountId) -> bool {
488 <Delegators<T>>::contains_key(who)
489 }
490
491 fn is_direct_staker(who: &T::AccountId) -> bool {
493 T::CoreStaking::status(who).is_ok()
494 }
495
496 fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) {
498 AgentLedger::<T>::new(reward_account).update(who);
500 }
501
502 fn do_migrate_to_agent(who: &T::AccountId, reward_account: &T::AccountId) -> DispatchResult {
504 Self::do_register_agent(who, reward_account);
505
506 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(who.clone()));
509
510 let stake = T::CoreStaking::stake(who)?;
512
513 T::CoreStaking::migrate_to_virtual_staker(who)?;
515
516 let amount_to_transfer =
518 T::Currency::reducible_balance(who, Preservation::Expendable, Fortitude::Polite);
519
520 T::Currency::transfer(
522 who,
523 &proxy_delegator.clone().get(),
524 amount_to_transfer,
525 Preservation::Expendable,
526 )?;
527
528 T::CoreStaking::set_payee(who, reward_account)?;
529 Self::do_delegate(proxy_delegator, Agent::from(who.clone()), amount_to_transfer)?;
531 let unclaimed_withdraws = amount_to_transfer
534 .checked_sub(&stake.total)
535 .defensive_ok_or(ArithmeticError::Underflow)?;
536
537 if !unclaimed_withdraws.is_zero() {
538 let mut ledger = AgentLedger::<T>::get(who).ok_or(Error::<T>::NotAgent)?;
539 ledger.unclaimed_withdrawals = ledger
540 .unclaimed_withdrawals
541 .checked_add(&unclaimed_withdraws)
542 .defensive_ok_or(ArithmeticError::Overflow)?;
543 ledger.update(who);
544 }
545
546 Ok(())
547 }
548
549 fn do_bond(agent_acc: Agent<T::AccountId>, amount: BalanceOf<T>) -> DispatchResult {
551 let agent_ledger = AgentLedgerOuter::<T>::get(&agent_acc.get())?;
552
553 let available_to_bond = agent_ledger.available_to_bond();
554 defensive_assert!(amount == available_to_bond, "not expected value to bond");
555
556 if agent_ledger.is_bonded() {
557 T::CoreStaking::bond_extra(&agent_ledger.key, amount)
558 } else {
559 T::CoreStaking::virtual_bond(&agent_ledger.key, amount, agent_ledger.reward_account())
560 }
561 }
562
563 fn do_delegate(
565 delegator: Delegator<T::AccountId>,
566 agent: Agent<T::AccountId>,
567 amount: BalanceOf<T>,
568 ) -> DispatchResult {
569 let agent = agent.get();
571 let delegator = delegator.get();
572
573 let mut ledger = AgentLedger::<T>::get(&agent).ok_or(Error::<T>::NotAgent)?;
574
575 if let Some(mut existing_delegation) = Delegation::<T>::get(&delegator) {
576 ensure!(existing_delegation.agent == agent, Error::<T>::InvalidDelegation);
577 existing_delegation.amount = existing_delegation
579 .amount
580 .checked_add(&amount)
581 .ok_or(ArithmeticError::Overflow)?;
582 existing_delegation
583 } else {
584 Delegation::<T>::new(&agent, amount)
585 }
586 .update(&delegator);
587
588 T::Currency::hold(&HoldReason::StakingDelegation.into(), &delegator, amount)?;
590
591 ledger.total_delegated =
592 ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
593 ledger.update(&agent);
594
595 Self::deposit_event(Event::<T>::Delegated { agent, delegator, amount });
596
597 Ok(())
598 }
599
600 fn do_release(
602 who: Agent<T::AccountId>,
603 delegator: Delegator<T::AccountId>,
604 amount: BalanceOf<T>,
605 num_slashing_spans: u32,
606 ) -> DispatchResult {
607 let agent = who.get();
609 let delegator = delegator.get();
610
611 let mut agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
612 let mut delegation = Delegation::<T>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
613
614 ensure!(delegation.agent == agent, Error::<T>::NotAgent);
616 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
617
618 if agent_ledger.ledger.unclaimed_withdrawals < amount {
620 T::CoreStaking::withdraw_unbonded(agent.clone(), num_slashing_spans)
622 .map_err(|_| Error::<T>::WithdrawFailed)?;
623 agent_ledger = agent_ledger.reload()?;
625 }
626
627 ensure!(agent_ledger.ledger.unclaimed_withdrawals >= amount, Error::<T>::NotEnoughFunds);
629 agent_ledger.remove_unclaimed_withdraw(amount)?.update();
630
631 delegation.amount = delegation
632 .amount
633 .checked_sub(&amount)
634 .defensive_ok_or(ArithmeticError::Overflow)?;
635
636 let released = T::Currency::release(
637 &HoldReason::StakingDelegation.into(),
638 &delegator,
639 amount,
640 Precision::BestEffort,
641 )?;
642
643 defensive_assert!(released == amount, "hold should have been released fully");
644
645 delegation.update(&delegator);
647
648 Self::deposit_event(Event::<T>::Released { agent, delegator, amount });
649
650 Ok(())
651 }
652
653 fn do_migrate_delegation(
655 source_delegator: Delegator<T::AccountId>,
656 destination_delegator: Delegator<T::AccountId>,
657 amount: BalanceOf<T>,
658 ) -> DispatchResult {
659 let source_delegator = source_delegator.get();
661 let destination_delegator = destination_delegator.get();
662
663 let mut source_delegation =
664 Delegators::<T>::get(&source_delegator).defensive_ok_or(Error::<T>::BadState)?;
665
666 ensure!(source_delegation.amount >= amount, Error::<T>::NotEnoughFunds);
668 debug_assert!(
669 !Self::is_delegator(&destination_delegator) && !Self::is_agent(&destination_delegator)
670 );
671
672 let agent = source_delegation.agent.clone();
673 Delegation::<T>::new(&agent, amount).update(&destination_delegator);
675
676 source_delegation.amount = source_delegation
677 .amount
678 .checked_sub(&amount)
679 .defensive_ok_or(Error::<T>::BadState)?;
680
681 T::Currency::transfer_on_hold(
683 &HoldReason::StakingDelegation.into(),
684 &source_delegator,
685 &destination_delegator,
686 amount,
687 Precision::Exact,
688 Restriction::OnHold,
689 Fortitude::Polite,
690 )?;
691
692 source_delegation.update(&source_delegator);
694
695 Self::deposit_event(Event::<T>::MigratedDelegation {
696 agent,
697 delegator: destination_delegator,
698 amount,
699 });
700
701 Ok(())
702 }
703
704 pub fn do_slash(
706 agent: Agent<T::AccountId>,
707 delegator: Delegator<T::AccountId>,
708 amount: BalanceOf<T>,
709 maybe_reporter: Option<T::AccountId>,
710 ) -> DispatchResult {
711 let agent = agent.get();
713 let delegator = delegator.get();
714
715 let agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
716 ensure!(agent_ledger.ledger.pending_slash > Zero::zero(), Error::<T>::NothingToSlash);
718
719 let mut delegation = <Delegators<T>>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
720 ensure!(delegation.agent == agent.clone(), Error::<T>::NotAgent);
721 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
722
723 let (mut credit, missing) =
725 T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount);
726
727 defensive_assert!(missing.is_zero(), "slash should have been fully applied");
728
729 let actual_slash = credit.peek();
730
731 agent_ledger.remove_slash(actual_slash).save();
733 delegation.amount =
734 delegation.amount.checked_sub(&actual_slash).ok_or(ArithmeticError::Overflow)?;
735 delegation.update(&delegator);
736
737 if let Some(reporter) = maybe_reporter {
738 let reward_payout: BalanceOf<T> = T::SlashRewardFraction::get() * actual_slash;
739 let (reporter_reward, rest) = credit.split(reward_payout);
740
741 credit = rest;
743
744 let _ = T::Currency::resolve(&reporter, reporter_reward);
746 }
747
748 T::OnSlash::on_unbalanced(credit);
749
750 Self::deposit_event(Event::<T>::Slashed { agent, delegator, amount });
751
752 Ok(())
753 }
754
755 #[cfg(test)]
757 pub(crate) fn stakeable_balance(who: Agent<T::AccountId>) -> BalanceOf<T> {
758 AgentLedgerOuter::<T>::get(&who.get())
759 .map(|agent| agent.ledger.stakeable_balance())
760 .unwrap_or_default()
761 }
762}
763
764#[cfg(any(test, feature = "try-runtime"))]
765use alloc::collections::btree_map::BTreeMap;
766
767#[cfg(any(test, feature = "try-runtime"))]
768impl<T: Config> Pallet<T> {
769 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
770 let delegation_map = Delegators::<T>::iter().collect::<BTreeMap<_, _>>();
772 let ledger_map = Agents::<T>::iter().collect::<BTreeMap<_, _>>();
773
774 Self::check_delegates(ledger_map.clone())?;
775 Self::check_delegators(delegation_map, ledger_map)?;
776
777 Ok(())
778 }
779
780 fn check_delegates(
781 ledgers: BTreeMap<T::AccountId, AgentLedger<T>>,
782 ) -> Result<(), sp_runtime::TryRuntimeError> {
783 for (agent, ledger) in ledgers {
784 let staked_value = ledger.stakeable_balance();
785
786 if !staked_value.is_zero() {
787 ensure!(
788 matches!(
789 T::CoreStaking::status(&agent).expect("agent should be bonded"),
790 sp_staking::StakerStatus::Nominator(_) | sp_staking::StakerStatus::Idle
791 ),
792 "agent should be bonded and not validator"
793 );
794 }
795
796 ensure!(
797 ledger.stakeable_balance() >=
798 T::CoreStaking::total_stake(&agent).unwrap_or_default(),
799 "Cannot stake more than balance"
800 );
801 }
802
803 Ok(())
804 }
805
806 fn check_delegators(
807 delegations: BTreeMap<T::AccountId, Delegation<T>>,
808 ledger: BTreeMap<T::AccountId, AgentLedger<T>>,
809 ) -> Result<(), sp_runtime::TryRuntimeError> {
810 let mut delegation_aggregation = BTreeMap::<T::AccountId, BalanceOf<T>>::new();
811 for (delegator, delegation) in delegations.iter() {
812 ensure!(!Self::is_agent(delegator), "delegator cannot be an agent");
813
814 delegation_aggregation
815 .entry(delegation.agent.clone())
816 .and_modify(|e| *e += delegation.amount)
817 .or_insert(delegation.amount);
818 }
819
820 for (agent, total_delegated) in delegation_aggregation {
821 ensure!(!Self::is_delegator(&agent), "agent cannot be delegator");
822
823 let ledger = ledger.get(&agent).expect("ledger should exist");
824 ensure!(
825 ledger.total_delegated == total_delegated,
826 "ledger total delegated should match delegations"
827 );
828 }
829
830 Ok(())
831 }
832}