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 pezpallet::*;
139
140use types::*;
141
142use core::convert::TryInto;
143use pezframe_support::{
144 pezpallet_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 pezsp_io::hashing::blake2_256;
157use pezsp_runtime::{
158 traits::{CheckedAdd, CheckedSub, TrailingZeroInput, Zero},
159 ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating,
160};
161use pezsp_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), <pezframe_system::Pezpallet<T>>::block_number() $(, $values)*
172 )
173 };
174}
175pub type BalanceOf<T> =
176 <<T as Config>::Currency as FunInspect<<T as pezframe_system::Config>::AccountId>>::Balance;
177
178use pezframe_system::{ensure_signed, pezpallet_prelude::*, RawOrigin};
179
180#[pezframe_support::pezpallet]
181pub mod pezpallet {
182 use super::*;
183
184 const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
186 #[pezpallet::pezpallet]
187 #[pezpallet::storage_version(STORAGE_VERSION)]
188 pub struct Pezpallet<T>(PhantomData<T>);
189
190 #[pezpallet::config]
191 pub trait Config: pezframe_system::Config {
192 #[allow(deprecated)]
194 type RuntimeEvent: From<Event<Self>>
195 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
196
197 #[pezpallet::constant]
199 type PalletId: Get<pezframe_support::PalletId>;
200
201 type Currency: FunHoldMutate<Self::AccountId, Reason = Self::RuntimeHoldReason>
203 + FunMutate<Self::AccountId>
204 + FunHoldBalanced<Self::AccountId>;
205
206 type OnSlash: OnUnbalanced<Credit<Self::AccountId, Self::Currency>>;
208
209 #[pezpallet::constant]
211 type SlashRewardFraction: Get<Perbill>;
212
213 type RuntimeHoldReason: From<HoldReason>;
215
216 type CoreStaking: StakingUnchecked<Balance = BalanceOf<Self>, AccountId = Self::AccountId>;
218 }
219
220 #[pezpallet::error]
221 pub enum Error<T> {
222 NotAllowed,
224 AlreadyStaking,
226 InvalidRewardDestination,
228 InvalidDelegation,
234 NotEnoughFunds,
236 NotAgent,
238 NotDelegator,
240 BadState,
242 UnappliedSlash,
244 NothingToSlash,
246 WithdrawFailed,
248 NotSupported,
250 }
251
252 #[pezpallet::composite_enum]
254 pub enum HoldReason {
255 #[codec(index = 0)]
257 StakingDelegation,
258 }
259
260 #[pezpallet::event]
261 #[pezpallet::generate_deposit(pub (super) fn deposit_event)]
262 pub enum Event<T: Config> {
263 Delegated { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
265 Released { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
267 Slashed { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
269 MigratedDelegation { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
271 }
272
273 #[pezpallet::storage]
278 pub type Delegators<T: Config> =
279 CountedStorageMap<_, Twox64Concat, T::AccountId, Delegation<T>, OptionQuery>;
280
281 #[pezpallet::storage]
283 pub type Agents<T: Config> =
284 CountedStorageMap<_, Twox64Concat, T::AccountId, AgentLedger<T>, OptionQuery>;
285
286 impl<T: Config> Pezpallet<T> {
290 pub fn register_agent(
305 origin: OriginFor<T>,
306 reward_account: T::AccountId,
307 ) -> DispatchResult {
308 let who = ensure_signed(origin)?;
309
310 ensure!(!Self::is_agent(&who) && !Self::is_delegator(&who), Error::<T>::NotAllowed);
312
313 ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
315
316 Self::do_register_agent(&who, &reward_account);
317 Ok(())
318 }
319
320 pub fn remove_agent(origin: OriginFor<T>) -> DispatchResult {
325 let who = ensure_signed(origin)?;
326 let ledger = AgentLedger::<T>::get(&who).ok_or(Error::<T>::NotAgent)?;
327
328 ensure!(
329 ledger.total_delegated == Zero::zero()
330 && ledger.pending_slash == Zero::zero()
331 && ledger.unclaimed_withdrawals == Zero::zero(),
332 Error::<T>::NotAllowed
333 );
334
335 AgentLedger::<T>::remove(&who);
336 Ok(())
337 }
338
339 pub fn migrate_to_agent(
353 origin: OriginFor<T>,
354 reward_account: T::AccountId,
355 ) -> DispatchResult {
356 let who = ensure_signed(origin)?;
357 ensure!(
359 Self::is_direct_staker(&who) && !Self::is_agent(&who) && !Self::is_delegator(&who),
360 Error::<T>::NotAllowed
361 );
362
363 ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
365
366 Self::do_migrate_to_agent(&who, &reward_account)
367 }
368
369 pub fn release_delegation(
376 origin: OriginFor<T>,
377 delegator: T::AccountId,
378 amount: BalanceOf<T>,
379 num_slashing_spans: u32,
380 ) -> DispatchResult {
381 let who = ensure_signed(origin)?;
382 Self::do_release(
383 Agent::from(who),
384 Delegator::from(delegator),
385 amount,
386 num_slashing_spans,
387 )
388 }
389
390 pub fn migrate_delegation(
400 origin: OriginFor<T>,
401 delegator: T::AccountId,
402 amount: BalanceOf<T>,
403 ) -> DispatchResult {
404 let agent = ensure_signed(origin)?;
405
406 ensure!(!Self::is_agent(&delegator), Error::<T>::NotAllowed);
408 ensure!(!Self::is_delegator(&delegator), Error::<T>::NotAllowed);
409
410 ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
412
413 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(agent));
415 let balance_remaining = Self::held_balance_of(proxy_delegator.clone());
416 ensure!(balance_remaining >= amount, Error::<T>::NotEnoughFunds);
417
418 Self::do_migrate_delegation(proxy_delegator, Delegator::from(delegator), amount)
419 }
420
421 pub fn delegate_to_agent(
431 origin: OriginFor<T>,
432 agent: T::AccountId,
433 amount: BalanceOf<T>,
434 ) -> DispatchResult {
435 let delegator = ensure_signed(origin)?;
436
437 ensure!(
439 Delegation::<T>::can_delegate(&delegator, &agent),
440 Error::<T>::InvalidDelegation
441 );
442
443 ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
445
446 Self::do_delegate(Delegator::from(delegator), Agent::from(agent.clone()), amount)?;
448
449 Self::do_bond(Agent::from(agent), amount)
451 }
452 }
453
454 #[pezpallet::hooks]
455 impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
456 #[cfg(feature = "try-runtime")]
457 fn try_state(_n: BlockNumberFor<T>) -> Result<(), pezsp_runtime::TryRuntimeError> {
458 Self::do_try_state()
459 }
460 }
461}
462
463impl<T: Config> Pezpallet<T> {
464 pub fn generate_proxy_delegator(agent: Agent<T::AccountId>) -> Delegator<T::AccountId> {
467 Delegator::from(Self::sub_account(AccountType::ProxyDelegator, agent.get()))
468 }
469
470 fn sub_account(account_type: AccountType, acc: T::AccountId) -> T::AccountId {
472 let entropy = (T::PalletId::get(), acc, account_type).using_encoded(blake2_256);
473 Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
474 .expect("infinite length input; no invalid inputs for type; qed")
475 }
476
477 pub(crate) fn held_balance_of(who: Delegator<T::AccountId>) -> BalanceOf<T> {
479 T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), &who.get())
480 }
481
482 fn is_agent(who: &T::AccountId) -> bool {
484 <Agents<T>>::contains_key(who)
485 }
486
487 fn is_delegator(who: &T::AccountId) -> bool {
489 <Delegators<T>>::contains_key(who)
490 }
491
492 fn is_direct_staker(who: &T::AccountId) -> bool {
494 T::CoreStaking::status(who).is_ok()
495 }
496
497 fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) {
499 AgentLedger::<T>::new(reward_account).update(who);
501 }
502
503 fn do_migrate_to_agent(who: &T::AccountId, reward_account: &T::AccountId) -> DispatchResult {
505 Self::do_register_agent(who, reward_account);
506
507 let proxy_delegator = Self::generate_proxy_delegator(Agent::from(who.clone()));
510
511 let stake = T::CoreStaking::stake(who)?;
513
514 T::CoreStaking::migrate_to_virtual_staker(who)?;
516
517 let amount_to_transfer =
519 T::Currency::reducible_balance(who, Preservation::Expendable, Fortitude::Polite);
520
521 T::Currency::transfer(
523 who,
524 &proxy_delegator.clone().get(),
525 amount_to_transfer,
526 Preservation::Expendable,
527 )?;
528
529 T::CoreStaking::set_payee(who, reward_account)?;
530 Self::do_delegate(proxy_delegator, Agent::from(who.clone()), amount_to_transfer)?;
532 let unclaimed_withdraws = amount_to_transfer
535 .checked_sub(&stake.total)
536 .defensive_ok_or(ArithmeticError::Underflow)?;
537
538 if !unclaimed_withdraws.is_zero() {
539 let mut ledger = AgentLedger::<T>::get(who).ok_or(Error::<T>::NotAgent)?;
540 ledger.unclaimed_withdrawals = ledger
541 .unclaimed_withdrawals
542 .checked_add(&unclaimed_withdraws)
543 .defensive_ok_or(ArithmeticError::Overflow)?;
544 ledger.update(who);
545 }
546
547 Ok(())
548 }
549
550 fn do_bond(agent_acc: Agent<T::AccountId>, amount: BalanceOf<T>) -> DispatchResult {
552 let agent_ledger = AgentLedgerOuter::<T>::get(&agent_acc.get())?;
553
554 let available_to_bond = agent_ledger.available_to_bond();
555 defensive_assert!(amount == available_to_bond, "not expected value to bond");
556
557 if agent_ledger.is_bonded() {
558 T::CoreStaking::bond_extra(&agent_ledger.key, amount)
559 } else {
560 T::CoreStaking::virtual_bond(&agent_ledger.key, amount, agent_ledger.reward_account())
561 }
562 }
563
564 fn do_delegate(
566 delegator: Delegator<T::AccountId>,
567 agent: Agent<T::AccountId>,
568 amount: BalanceOf<T>,
569 ) -> DispatchResult {
570 let agent = agent.get();
572 let delegator = delegator.get();
573
574 let mut ledger = AgentLedger::<T>::get(&agent).ok_or(Error::<T>::NotAgent)?;
575
576 if let Some(mut existing_delegation) = Delegation::<T>::get(&delegator) {
577 ensure!(existing_delegation.agent == agent, Error::<T>::InvalidDelegation);
578 existing_delegation.amount = existing_delegation
580 .amount
581 .checked_add(&amount)
582 .ok_or(ArithmeticError::Overflow)?;
583 existing_delegation
584 } else {
585 Delegation::<T>::new(&agent, amount)
586 }
587 .update(&delegator);
588
589 T::Currency::hold(&HoldReason::StakingDelegation.into(), &delegator, amount)?;
591
592 ledger.total_delegated =
593 ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
594 ledger.update(&agent);
595
596 Self::deposit_event(Event::<T>::Delegated { agent, delegator, amount });
597
598 Ok(())
599 }
600
601 fn do_release(
603 who: Agent<T::AccountId>,
604 delegator: Delegator<T::AccountId>,
605 amount: BalanceOf<T>,
606 num_slashing_spans: u32,
607 ) -> DispatchResult {
608 let agent = who.get();
610 let delegator = delegator.get();
611
612 let mut agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
613 let mut delegation = Delegation::<T>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
614
615 ensure!(delegation.agent == agent, Error::<T>::NotAgent);
617 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
618
619 if agent_ledger.ledger.unclaimed_withdrawals < amount {
621 T::CoreStaking::withdraw_unbonded(agent.clone(), num_slashing_spans)
623 .map_err(|_| Error::<T>::WithdrawFailed)?;
624 agent_ledger = agent_ledger.reload()?;
626 }
627
628 ensure!(agent_ledger.ledger.unclaimed_withdrawals >= amount, Error::<T>::NotEnoughFunds);
630 agent_ledger.remove_unclaimed_withdraw(amount)?.update();
631
632 delegation.amount = delegation
633 .amount
634 .checked_sub(&amount)
635 .defensive_ok_or(ArithmeticError::Overflow)?;
636
637 let released = T::Currency::release(
638 &HoldReason::StakingDelegation.into(),
639 &delegator,
640 amount,
641 Precision::BestEffort,
642 )?;
643
644 defensive_assert!(released == amount, "hold should have been released fully");
645
646 delegation.update(&delegator);
648
649 Self::deposit_event(Event::<T>::Released { agent, delegator, amount });
650
651 Ok(())
652 }
653
654 fn do_migrate_delegation(
656 source_delegator: Delegator<T::AccountId>,
657 destination_delegator: Delegator<T::AccountId>,
658 amount: BalanceOf<T>,
659 ) -> DispatchResult {
660 let source_delegator = source_delegator.get();
662 let destination_delegator = destination_delegator.get();
663
664 let mut source_delegation =
665 Delegators::<T>::get(&source_delegator).defensive_ok_or(Error::<T>::BadState)?;
666
667 ensure!(source_delegation.amount >= amount, Error::<T>::NotEnoughFunds);
669 debug_assert!(
670 !Self::is_delegator(&destination_delegator) && !Self::is_agent(&destination_delegator)
671 );
672
673 let agent = source_delegation.agent.clone();
674 Delegation::<T>::new(&agent, amount).update(&destination_delegator);
676
677 source_delegation.amount = source_delegation
678 .amount
679 .checked_sub(&amount)
680 .defensive_ok_or(Error::<T>::BadState)?;
681
682 T::Currency::transfer_on_hold(
684 &HoldReason::StakingDelegation.into(),
685 &source_delegator,
686 &destination_delegator,
687 amount,
688 Precision::Exact,
689 Restriction::OnHold,
690 Fortitude::Polite,
691 )?;
692
693 source_delegation.update(&source_delegator);
695
696 Self::deposit_event(Event::<T>::MigratedDelegation {
697 agent,
698 delegator: destination_delegator,
699 amount,
700 });
701
702 Ok(())
703 }
704
705 pub fn do_slash(
707 agent: Agent<T::AccountId>,
708 delegator: Delegator<T::AccountId>,
709 amount: BalanceOf<T>,
710 maybe_reporter: Option<T::AccountId>,
711 ) -> DispatchResult {
712 let agent = agent.get();
714 let delegator = delegator.get();
715
716 let agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
717 ensure!(agent_ledger.ledger.pending_slash > Zero::zero(), Error::<T>::NothingToSlash);
719
720 let mut delegation = <Delegators<T>>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
721 ensure!(delegation.agent == agent.clone(), Error::<T>::NotAgent);
722 ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
723
724 let (mut credit, missing) =
726 T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount);
727
728 defensive_assert!(missing.is_zero(), "slash should have been fully applied");
729
730 let actual_slash = credit.peek();
731
732 agent_ledger.remove_slash(actual_slash).save();
734 delegation.amount =
735 delegation.amount.checked_sub(&actual_slash).ok_or(ArithmeticError::Overflow)?;
736 delegation.update(&delegator);
737
738 if let Some(reporter) = maybe_reporter {
739 let reward_payout: BalanceOf<T> = T::SlashRewardFraction::get() * actual_slash;
740 let (reporter_reward, rest) = credit.split(reward_payout);
741
742 credit = rest;
744
745 let _ = T::Currency::resolve(&reporter, reporter_reward);
747 }
748
749 T::OnSlash::on_unbalanced(credit);
750
751 Self::deposit_event(Event::<T>::Slashed { agent, delegator, amount });
752
753 Ok(())
754 }
755
756 #[cfg(test)]
758 pub(crate) fn stakeable_balance(who: Agent<T::AccountId>) -> BalanceOf<T> {
759 AgentLedgerOuter::<T>::get(&who.get())
760 .map(|agent| agent.ledger.stakeable_balance())
761 .unwrap_or_default()
762 }
763}
764
765#[cfg(any(test, feature = "try-runtime"))]
766use alloc::collections::btree_map::BTreeMap;
767
768#[cfg(any(test, feature = "try-runtime"))]
769impl<T: Config> Pezpallet<T> {
770 pub(crate) fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
771 let delegation_map = Delegators::<T>::iter().collect::<BTreeMap<_, _>>();
773 let ledger_map = Agents::<T>::iter().collect::<BTreeMap<_, _>>();
774
775 Self::check_delegates(ledger_map.clone())?;
776 Self::check_delegators(delegation_map, ledger_map)?;
777
778 Ok(())
779 }
780
781 fn check_delegates(
782 ledgers: BTreeMap<T::AccountId, AgentLedger<T>>,
783 ) -> Result<(), pezsp_runtime::TryRuntimeError> {
784 for (agent, ledger) in ledgers {
785 let staked_value = ledger.stakeable_balance();
786
787 if !staked_value.is_zero() {
788 ensure!(
789 matches!(
790 T::CoreStaking::status(&agent).expect("agent should be bonded"),
791 pezsp_staking::StakerStatus::Nominator(_)
792 | pezsp_staking::StakerStatus::Idle
793 ),
794 "agent should be bonded and not validator"
795 );
796 }
797
798 ensure!(
799 ledger.stakeable_balance()
800 >= T::CoreStaking::total_stake(&agent).unwrap_or_default(),
801 "Cannot stake more than balance"
802 );
803 }
804
805 Ok(())
806 }
807
808 fn check_delegators(
809 delegations: BTreeMap<T::AccountId, Delegation<T>>,
810 ledger: BTreeMap<T::AccountId, AgentLedger<T>>,
811 ) -> Result<(), pezsp_runtime::TryRuntimeError> {
812 let mut delegation_aggregation = BTreeMap::<T::AccountId, BalanceOf<T>>::new();
813 for (delegator, delegation) in delegations.iter() {
814 ensure!(!Self::is_agent(delegator), "delegator cannot be an agent");
815
816 delegation_aggregation
817 .entry(delegation.agent.clone())
818 .and_modify(|e| *e += delegation.amount)
819 .or_insert(delegation.amount);
820 }
821
822 for (agent, total_delegated) in delegation_aggregation {
823 ensure!(!Self::is_delegator(&agent), "agent cannot be delegator");
824
825 let ledger = ledger.get(&agent).expect("ledger should exist");
826 ensure!(
827 ledger.total_delegated == total_delegated,
828 "ledger total delegated should match delegations"
829 );
830 }
831
832 Ok(())
833 }
834}