1#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6use crate::block_tree::invalid_bundle_authors_for_receipt;
7use crate::bundle_storage_fund::{self, deposit_reserve_for_storage_fund};
8use crate::pallet::{
9 Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber, NextOperatorId,
10 OperatorIdOwner, Operators, PendingSlashes, PendingStakingOperationCount, Withdrawals,
11};
12use crate::staking_epoch::{mint_funds, mint_into_treasury};
13use crate::{
14 BalanceOf, Config, DeactivatedOperators, DepositOnHold, DeregisteredOperators,
15 DomainBlockNumberFor, DomainHashingFor, Event, ExecutionReceiptOf, HoldIdentifier,
16 InvalidBundleAuthors, NominatorId, OperatorEpochSharePrice, OperatorHighestSlot, Pallet,
17 ReceiptHashFor, SlashedReason, WeightInfo,
18};
19use frame_support::traits::fungible::{Inspect, MutateHold};
20use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
21use frame_support::{PalletError, StorageDoubleMap, ensure};
22use frame_system::pallet_prelude::BlockNumberFor;
23use parity_scale_codec::{Decode, Encode};
24use scale_info::TypeInfo;
25use sp_core::{Get, sr25519};
26use sp_domains::{DomainId, EpochIndex, OperatorId, OperatorPublicKey, OperatorRewardSource};
27use sp_runtime::traits::{CheckedAdd, CheckedSub, Zero};
28use sp_runtime::{PerThing, Percent, Perquintill, Saturating, Weight};
29use sp_std::collections::btree_map::BTreeMap;
30use sp_std::collections::btree_set::BTreeSet;
31use sp_std::collections::vec_deque::VecDeque;
32use sp_std::vec::IntoIter;
33
34#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
36pub(crate) struct Deposit<Share: Copy, Balance: Copy> {
37 pub(crate) known: KnownDeposit<Share, Balance>,
38 pub(crate) pending: Option<PendingDeposit<Balance>>,
39}
40
41#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
44pub struct SharePrice(pub Perquintill);
45
46impl SharePrice {
47 pub(crate) fn new<T: Config>(
50 total_shares: T::Share,
51 total_stake: BalanceOf<T>,
52 ) -> Result<Self, Error> {
53 if total_shares > total_stake.into() {
54 Err(Error::ShareOverflow)
56 } else if total_stake.is_zero() || total_shares.is_zero() {
57 Err(Error::ZeroSharePrice)
59 } else {
60 Ok(SharePrice(Perquintill::from_rational(
61 total_shares.into(),
62 total_stake,
63 )))
64 }
65 }
66
67 pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
70 self.0.mul_floor(stake).into()
71 }
72
73 pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
76 self.0
81 .plus_epsilon()
84 .saturating_reciprocal_mul_floor(shares.into())
85 }
86
87 pub(crate) fn one() -> Self {
89 Self(Perquintill::one())
90 }
91}
92
93#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
95pub struct DomainEpoch(DomainId, EpochIndex);
96
97impl DomainEpoch {
98 pub(crate) fn deconstruct(self) -> (DomainId, EpochIndex) {
99 (self.0, self.1)
100 }
101}
102
103impl From<(DomainId, EpochIndex)> for DomainEpoch {
104 fn from((domain_id, epoch_idx): (DomainId, EpochIndex)) -> Self {
105 Self(domain_id, epoch_idx)
106 }
107}
108
109pub struct NewDeposit<Balance> {
110 pub(crate) staking: Balance,
111 pub(crate) storage_fee_deposit: Balance,
112}
113
114#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq, Default)]
116pub(crate) struct KnownDeposit<Share: Copy, Balance: Copy> {
117 pub(crate) shares: Share,
118 pub(crate) storage_fee_deposit: Balance,
119}
120
121#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
123pub(crate) struct PendingDeposit<Balance: Copy> {
124 pub(crate) effective_domain_epoch: DomainEpoch,
125 pub(crate) amount: Balance,
126 pub(crate) storage_fee_deposit: Balance,
127}
128
129impl<Balance: Copy + CheckedAdd> PendingDeposit<Balance> {
130 fn total(&self) -> Result<Balance, Error> {
131 self.amount
132 .checked_add(&self.storage_fee_deposit)
133 .ok_or(Error::BalanceOverflow)
134 }
135}
136
137#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
139pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
140 pub(crate) total_withdrawal_amount: Balance,
143 pub(crate) total_storage_fee_withdrawal: Balance,
145 pub(crate) withdrawals: VecDeque<WithdrawalInBalance<DomainBlockNumber, Balance>>,
147 pub(crate) withdrawal_in_shares: Option<WithdrawalInShares<DomainBlockNumber, Share, Balance>>,
150}
151
152#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
153pub(crate) struct WithdrawalInBalance<DomainBlockNumber, Balance> {
154 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
155 pub(crate) amount_to_unlock: Balance,
156 pub(crate) storage_fee_refund: Balance,
157}
158
159#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
160pub(crate) struct WithdrawalInShares<DomainBlockNumber, Share, Balance> {
161 pub(crate) domain_epoch: DomainEpoch,
162 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
163 pub(crate) shares: Share,
164 pub(crate) storage_fee_refund: Balance,
165}
166
167#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
168pub struct OperatorDeregisteredInfo<DomainBlockNumber> {
169 pub domain_epoch: DomainEpoch,
170 pub unlock_at_confirmed_domain_block_number: DomainBlockNumber,
171}
172
173impl<DomainBlockNumber> From<(DomainId, EpochIndex, DomainBlockNumber)>
174 for OperatorDeregisteredInfo<DomainBlockNumber>
175{
176 fn from(value: (DomainId, EpochIndex, DomainBlockNumber)) -> Self {
177 OperatorDeregisteredInfo {
178 domain_epoch: (value.0, value.1).into(),
179 unlock_at_confirmed_domain_block_number: value.2,
180 }
181 }
182}
183
184#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
186pub enum OperatorStatus<DomainBlockNumber, ReceiptHash> {
187 #[codec(index = 0)]
188 Registered,
189 #[codec(index = 1)]
191 Deregistered(OperatorDeregisteredInfo<DomainBlockNumber>),
192 #[codec(index = 2)]
193 Slashed,
194 #[codec(index = 3)]
195 PendingSlash,
196 #[codec(index = 4)]
197 InvalidBundle(ReceiptHash),
198 #[codec(index = 5)]
201 Deactivated(EpochIndex),
202}
203
204#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
206pub struct Operator<Balance, Share, DomainBlockNumber, ReceiptHash> {
207 pub signing_key: OperatorPublicKey,
208 pub current_domain_id: DomainId,
209 pub next_domain_id: DomainId,
210 pub minimum_nominator_stake: Balance,
211 pub nomination_tax: Percent,
212 pub current_total_stake: Balance,
214 pub current_total_shares: Share,
216 partial_status: OperatorStatus<DomainBlockNumber, ReceiptHash>,
220 pub deposits_in_epoch: Balance,
222 pub withdrawals_in_epoch: Share,
224 pub total_storage_fee_deposit: Balance,
226}
227
228impl<Balance, Share, DomainBlockNumber, ReceiptHash>
229 Operator<Balance, Share, DomainBlockNumber, ReceiptHash>
230{
231 pub fn status<T: Config>(
232 &self,
233 operator_id: OperatorId,
234 ) -> &OperatorStatus<DomainBlockNumber, ReceiptHash> {
235 if matches!(self.partial_status, OperatorStatus::Slashed) {
236 &OperatorStatus::Slashed
237 } else if Pallet::<T>::is_operator_pending_to_slash(self.current_domain_id, operator_id) {
238 &OperatorStatus::PendingSlash
239 } else {
240 &self.partial_status
241 }
242 }
243
244 pub fn update_status(&mut self, new_status: OperatorStatus<DomainBlockNumber, ReceiptHash>) {
245 self.partial_status = new_status;
246 }
247
248 pub fn is_operator_registered_or_deactivated<T: Config>(
250 &self,
251 operator_id: OperatorId,
252 ) -> bool {
253 let status = self.status::<T>(operator_id);
254 matches!(
255 status,
256 OperatorStatus::Registered | OperatorStatus::Deactivated(_)
257 )
258 }
259
260 pub fn can_operator_submit_bundle<T: Config>(&self, operator_id: OperatorId) -> bool {
262 let status = self.status::<T>(operator_id);
263 !matches!(
265 status,
266 OperatorStatus::Slashed
267 | OperatorStatus::PendingSlash
268 | OperatorStatus::InvalidBundle(_)
269 | OperatorStatus::Deactivated(_)
270 )
271 }
272}
273
274#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
275pub struct StakingSummary<OperatorId, Balance> {
276 pub current_epoch_index: EpochIndex,
278 pub current_total_stake: Balance,
280 pub current_operators: BTreeMap<OperatorId, Balance>,
282 pub next_operators: BTreeSet<OperatorId>,
284 pub current_epoch_rewards: BTreeMap<OperatorId, Balance>,
286}
287
288#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
289pub struct OperatorConfig<Balance> {
290 pub signing_key: OperatorPublicKey,
291 pub minimum_nominator_stake: Balance,
292 pub nomination_tax: Percent,
293}
294
295#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
296pub enum Error {
297 MaximumOperatorId,
298 DomainNotInitialized,
299 PendingOperatorSwitch,
300 InsufficientBalance,
301 InsufficientShares,
302 ZeroWithdraw,
303 BalanceFreeze,
304 MinimumOperatorStake,
305 UnknownOperator,
306 MinimumNominatorStake,
307 BalanceOverflow,
308 BalanceUnderflow,
309 NotOperatorOwner,
310 OperatorNotRegistered,
311 OperatorNotDeactivated,
312 UnknownNominator,
313 MissingOperatorOwner,
314 MintBalance,
315 BlockNumberOverflow,
316 RemoveLock,
317 EpochOverflow,
318 ShareUnderflow,
319 ShareOverflow,
320 TooManyPendingStakingOperation,
321 OperatorNotAllowed,
322 InvalidOperatorSigningKey,
323 MissingOperatorEpochSharePrice,
324 MissingWithdrawal,
325 EpochNotComplete,
326 UnlockPeriodNotComplete,
327 OperatorNotDeregistered,
328 BundleStorageFund(bundle_storage_fund::Error),
329 UnconfirmedER,
330 TooManyWithdrawals,
331 ZeroDeposit,
332 ZeroSharePrice,
333 ReactivationDelayPeriodIncomplete,
334 OperatorNotRegisterdOrDeactivated,
335}
336
337fn note_pending_staking_operation<T: Config>(domain_id: DomainId) -> Result<(), Error> {
340 let pending_op_count = PendingStakingOperationCount::<T>::get(domain_id);
341
342 ensure!(
343 pending_op_count < T::MaxPendingStakingOperation::get(),
344 Error::TooManyPendingStakingOperation
345 );
346
347 PendingStakingOperationCount::<T>::set(domain_id, pending_op_count.saturating_add(1));
348
349 Ok(())
350}
351
352pub fn do_register_operator<T: Config>(
353 operator_owner: T::AccountId,
354 domain_id: DomainId,
355 amount: BalanceOf<T>,
356 config: OperatorConfig<BalanceOf<T>>,
357) -> Result<(OperatorId, EpochIndex), Error> {
358 note_pending_staking_operation::<T>(domain_id)?;
359
360 DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_domain_stake_summary| {
361 ensure!(
362 config.signing_key != OperatorPublicKey::from(sr25519::Public::default()),
363 Error::InvalidOperatorSigningKey
364 );
365
366 ensure!(
367 config.minimum_nominator_stake >= T::MinNominatorStake::get(),
368 Error::MinimumNominatorStake
369 );
370
371 let domain_obj = DomainRegistry::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
372 ensure!(
373 domain_obj
374 .domain_config
375 .operator_allow_list
376 .is_operator_allowed(&operator_owner),
377 Error::OperatorNotAllowed
378 );
379
380 let operator_id = NextOperatorId::<T>::get();
381 let next_operator_id = operator_id.checked_add(1).ok_or(Error::MaximumOperatorId)?;
382 NextOperatorId::<T>::set(next_operator_id);
383
384 OperatorIdOwner::<T>::insert(operator_id, operator_owner.clone());
385
386 ensure!(
388 amount >= T::MinOperatorStake::get(),
389 Error::MinimumOperatorStake
390 );
391
392 let new_deposit =
393 deposit_reserve_for_storage_fund::<T>(operator_id, &operator_owner, amount)
394 .map_err(Error::BundleStorageFund)?;
395
396 hold_deposit::<T>(&operator_owner, operator_id, new_deposit.staking)?;
397
398 let domain_stake_summary = maybe_domain_stake_summary
399 .as_mut()
400 .ok_or(Error::DomainNotInitialized)?;
401
402 let OperatorConfig {
403 signing_key,
404 minimum_nominator_stake,
405 nomination_tax,
406 } = config;
407
408 let first_share_price = SharePrice::one();
417 let operator = Operator {
418 signing_key: signing_key.clone(),
419 current_domain_id: domain_id,
420 next_domain_id: domain_id,
421 minimum_nominator_stake,
422 nomination_tax,
423 current_total_stake: new_deposit.staking,
424 current_total_shares: first_share_price.stake_to_shares::<T>(new_deposit.staking),
425 partial_status: OperatorStatus::Registered,
426 deposits_in_epoch: Zero::zero(),
428 withdrawals_in_epoch: Zero::zero(),
429 total_storage_fee_deposit: new_deposit.storage_fee_deposit,
430 };
431 Operators::<T>::insert(operator_id, operator);
432 OperatorEpochSharePrice::<T>::insert(
433 operator_id,
434 DomainEpoch::from((domain_id, domain_stake_summary.current_epoch_index)),
435 first_share_price,
436 );
437
438 domain_stake_summary.next_operators.insert(operator_id);
440 let current_domain_epoch = (domain_id, domain_stake_summary.current_epoch_index).into();
442 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
443 operator_id,
444 operator_owner,
445 current_domain_epoch,
446 new_deposit,
447 None,
448 )?;
449
450 Ok((operator_id, domain_stake_summary.current_epoch_index))
451 })
452}
453
454pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T: Config>(
459 operator_id: OperatorId,
460 nominator_id: NominatorId<T>,
461 current_domain_epoch: DomainEpoch,
462 new_deposit: NewDeposit<BalanceOf<T>>,
463 required_minimum_nominator_stake: Option<BalanceOf<T>>,
464) -> Result<(), Error> {
465 Deposits::<T>::try_mutate(operator_id, nominator_id, |maybe_deposit| {
466 let mut deposit = maybe_deposit.take().unwrap_or_default();
467 do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, current_domain_epoch.1)?;
468
469 let pending_deposit = match deposit.pending {
471 None => PendingDeposit {
472 effective_domain_epoch: current_domain_epoch,
473 amount: new_deposit.staking,
474 storage_fee_deposit: new_deposit.storage_fee_deposit,
475 },
476 Some(pending_deposit) => PendingDeposit {
477 effective_domain_epoch: current_domain_epoch,
478 amount: pending_deposit
479 .amount
480 .checked_add(&new_deposit.staking)
481 .ok_or(Error::BalanceOverflow)?,
482 storage_fee_deposit: pending_deposit
483 .storage_fee_deposit
484 .checked_add(&new_deposit.storage_fee_deposit)
485 .ok_or(Error::BalanceOverflow)?,
486 },
487 };
488
489 if deposit.known.shares.is_zero()
490 && let Some(minimum_nominator_stake) = required_minimum_nominator_stake
491 {
492 ensure!(
493 pending_deposit.total()? >= minimum_nominator_stake,
494 Error::MinimumNominatorStake
495 );
496 }
497
498 deposit.pending = Some(pending_deposit);
499 *maybe_deposit = Some(deposit);
500 Ok(())
501 })
502}
503
504pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
505 operator_id: OperatorId,
506 deposit: &mut Deposit<T::Share, BalanceOf<T>>,
507 current_domain_epoch_index: EpochIndex,
508) -> Result<(), Error> {
509 let epoch_share_price = match deposit.pending {
511 None => return Ok(()),
512 Some(pending_deposit) => {
513 match OperatorEpochSharePrice::<T>::get(
514 operator_id,
515 pending_deposit.effective_domain_epoch,
516 ) {
517 Some(p) => p,
518 None => {
519 ensure!(
520 pending_deposit.effective_domain_epoch.1 >= current_domain_epoch_index,
521 Error::MissingOperatorEpochSharePrice
522 );
523 return Ok(());
524 }
525 }
526 }
527 };
528
529 if let Some(PendingDeposit {
530 amount,
531 storage_fee_deposit,
532 ..
533 }) = deposit.pending.take()
534 {
535 let new_shares = epoch_share_price.stake_to_shares::<T>(amount);
536 deposit.known.shares = deposit
537 .known
538 .shares
539 .checked_add(&new_shares)
540 .ok_or(Error::ShareOverflow)?;
541 deposit.known.storage_fee_deposit = deposit
542 .known
543 .storage_fee_deposit
544 .checked_add(&storage_fee_deposit)
545 .ok_or(Error::BalanceOverflow)?;
546 }
547
548 Ok(())
549}
550
551pub(crate) fn do_convert_previous_epoch_withdrawal<T: Config>(
557 operator_id: OperatorId,
558 withdrawal: &mut Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
559 current_domain_epoch_index: EpochIndex,
560) -> Result<(), Error> {
561 let epoch_share_price = match withdrawal.withdrawal_in_shares.as_ref() {
562 None => return Ok(()),
563 Some(withdraw) => {
564 if withdraw.domain_epoch.1 >= current_domain_epoch_index {
566 return Ok(());
567 }
568
569 match OperatorEpochSharePrice::<T>::get(operator_id, withdraw.domain_epoch) {
570 Some(p) => p,
571 None => return Err(Error::MissingOperatorEpochSharePrice),
572 }
573 }
574 };
575
576 if let Some(WithdrawalInShares {
577 unlock_at_confirmed_domain_block_number,
578 shares,
579 storage_fee_refund,
580 domain_epoch: _,
581 }) = withdrawal.withdrawal_in_shares.take()
582 {
583 let withdrawal_amount = epoch_share_price.shares_to_stake::<T>(shares);
584 withdrawal.total_withdrawal_amount = withdrawal
585 .total_withdrawal_amount
586 .checked_add(&withdrawal_amount)
587 .ok_or(Error::BalanceOverflow)?;
588
589 let withdraw_in_balance = WithdrawalInBalance {
590 unlock_at_confirmed_domain_block_number,
591 amount_to_unlock: withdrawal_amount,
592 storage_fee_refund,
593 };
594 withdrawal.withdrawals.push_back(withdraw_in_balance);
595 }
596
597 Ok(())
598}
599
600pub(crate) fn do_nominate_operator<T: Config>(
601 operator_id: OperatorId,
602 nominator_id: T::AccountId,
603 amount: BalanceOf<T>,
604) -> Result<(), Error> {
605 ensure!(!amount.is_zero(), Error::ZeroDeposit);
606
607 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
608 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
609
610 ensure!(
611 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
612 Error::OperatorNotRegistered
613 );
614
615 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
617 note_pending_staking_operation::<T>(operator.current_domain_id)?;
618 }
619
620 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
621 .ok_or(Error::DomainNotInitialized)?;
622
623 let new_deposit = deposit_reserve_for_storage_fund::<T>(operator_id, &nominator_id, amount)
625 .map_err(Error::BundleStorageFund)?;
626
627 hold_deposit::<T>(&nominator_id, operator_id, new_deposit.staking)?;
628 Pallet::<T>::deposit_event(Event::OperatorNominated {
629 operator_id,
630 nominator_id: nominator_id.clone(),
631 amount: new_deposit.staking,
632 });
633
634 operator.deposits_in_epoch = operator
636 .deposits_in_epoch
637 .checked_add(&new_deposit.staking)
638 .ok_or(Error::BalanceOverflow)?;
639
640 operator.total_storage_fee_deposit = operator
642 .total_storage_fee_deposit
643 .checked_add(&new_deposit.storage_fee_deposit)
644 .ok_or(Error::BalanceOverflow)?;
645
646 let current_domain_epoch = (
647 operator.current_domain_id,
648 domain_stake_summary.current_epoch_index,
649 )
650 .into();
651
652 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
653 operator_id,
654 nominator_id,
655 current_domain_epoch,
656 new_deposit,
657 Some(operator.minimum_nominator_stake),
658 )?;
659
660 Ok(())
661 })
662}
663
664pub(crate) fn hold_deposit<T: Config>(
665 who: &T::AccountId,
666 operator_id: OperatorId,
667 amount: BalanceOf<T>,
668) -> Result<(), Error> {
669 ensure!(
671 T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite) >= amount,
672 Error::InsufficientBalance
673 );
674
675 DepositOnHold::<T>::try_mutate((operator_id, who), |deposit_on_hold| {
676 *deposit_on_hold = deposit_on_hold
677 .checked_add(&amount)
678 .ok_or(Error::BalanceOverflow)?;
679 Ok(())
680 })?;
681
682 let pending_deposit_hold_id = T::HoldIdentifier::staking_staked();
683 T::Currency::hold(&pending_deposit_hold_id, who, amount).map_err(|_| Error::BalanceFreeze)?;
684
685 Ok(())
686}
687
688pub(crate) fn do_deregister_operator<T: Config>(
691 operator_owner: T::AccountId,
692 operator_id: OperatorId,
693) -> Result<Weight, Error> {
694 ensure!(
695 OperatorIdOwner::<T>::get(operator_id) == Some(operator_owner),
696 Error::NotOperatorOwner
697 );
698
699 let mut weight = T::WeightInfo::deregister_operator();
700 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
701 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
702
703 ensure!(
704 operator.is_operator_registered_or_deactivated::<T>(operator_id),
705 Error::OperatorNotRegisterdOrDeactivated
706 );
707
708 DomainStakingSummary::<T>::try_mutate(
709 operator.current_domain_id,
710 |maybe_domain_stake_summary| {
711 let stake_summary = maybe_domain_stake_summary
712 .as_mut()
713 .ok_or(Error::DomainNotInitialized)?;
714
715 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
716 let unlock_operator_at_domain_block_number = head_domain_number
717 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
718 .ok_or(Error::BlockNumberOverflow)?;
719 let operator_deregister_info = (
720 operator.current_domain_id,
721 stake_summary.current_epoch_index,
722 unlock_operator_at_domain_block_number,
723 )
724 .into();
725
726 if matches!(operator.partial_status, OperatorStatus::Deactivated(_)) {
730 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
731 operators.remove(&operator_id);
732 });
733 weight = T::WeightInfo::deregister_deactivated_operator();
734 }
735
736 operator.update_status(OperatorStatus::Deregistered(operator_deregister_info));
737
738 stake_summary.next_operators.remove(&operator_id);
739
740 DeregisteredOperators::<T>::mutate(operator.current_domain_id, |operators| {
741 operators.insert(operator_id)
742 });
743 Ok(())
744 },
745 )
746 })?;
747
748 Ok(weight)
749}
750
751pub(crate) fn do_deactivate_operator<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
756 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
757 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
758
759 ensure!(
760 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
761 Error::OperatorNotRegistered
762 );
763
764 DomainStakingSummary::<T>::try_mutate(
765 operator.current_domain_id,
766 |maybe_domain_stake_summary| {
767 let stake_summary = maybe_domain_stake_summary
768 .as_mut()
769 .ok_or(Error::DomainNotInitialized)?;
770
771 let current_epoch = stake_summary.current_epoch_index;
772 let reactivation_delay = current_epoch
773 .checked_add(T::OperatorActivationDelayInEpochs::get())
774 .ok_or(Error::EpochOverflow)?;
775
776 operator.update_status(OperatorStatus::Deactivated(reactivation_delay));
777
778 if stake_summary
782 .current_operators
783 .remove(&operator_id)
784 .is_some()
785 {
786 stake_summary.current_total_stake = stake_summary
787 .current_total_stake
788 .checked_sub(&operator.current_total_stake)
789 .ok_or(Error::BalanceUnderflow)?;
790 }
791 stake_summary.next_operators.remove(&operator_id);
792
793 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
794 operators.insert(operator_id)
795 });
796 Pallet::<T>::deposit_event(Event::OperatorDeactivated {
797 domain_id: operator.current_domain_id,
798 operator_id,
799 reactivation_delay,
800 });
801
802 Ok(())
803 },
804 )
805 })
806}
807
808pub(crate) fn do_reactivate_operator<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
811 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
812 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
813 let operator_status = operator.status::<T>(operator_id);
814 let reactivation_delay =
815 if let OperatorStatus::Deactivated(reactivation_delay) = operator_status {
816 *reactivation_delay
817 } else {
818 return Err(Error::OperatorNotDeactivated);
819 };
820
821 DomainStakingSummary::<T>::try_mutate(
822 operator.current_domain_id,
823 |maybe_domain_stake_summary| {
824 let stake_summary = maybe_domain_stake_summary
825 .as_mut()
826 .ok_or(Error::DomainNotInitialized)?;
827
828 let current_epoch = stake_summary.current_epoch_index;
829 ensure!(
830 current_epoch >= reactivation_delay,
831 Error::ReactivationDelayPeriodIncomplete
832 );
833
834 operator.update_status(OperatorStatus::Registered);
835 stake_summary.next_operators.insert(operator_id);
836
837 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
841 operators.remove(&operator_id);
842 });
843
844 Pallet::<T>::deposit_event(Event::OperatorReactivated {
845 domain_id: operator.current_domain_id,
846 operator_id,
847 });
848
849 Ok(())
850 },
851 )
852 })
853}
854
855pub(crate) fn current_share_price<T: Config>(
858 operator_id: OperatorId,
859 operator: &Operator<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
860 domain_stake_summary: &StakingSummary<OperatorId, BalanceOf<T>>,
861) -> Result<SharePrice, Error> {
862 let total_stake = domain_stake_summary
864 .current_epoch_rewards
865 .get(&operator_id)
866 .and_then(|rewards| {
867 let operator_tax = operator.nomination_tax.mul_floor(*rewards);
868 operator
869 .current_total_stake
870 .checked_add(rewards)?
871 .checked_sub(&operator_tax)
873 })
874 .unwrap_or(operator.current_total_stake);
875
876 SharePrice::new::<T>(operator.current_total_shares, total_stake)
877}
878
879pub(crate) fn do_withdraw_stake<T: Config>(
888 operator_id: OperatorId,
889 nominator_id: NominatorId<T>,
890 to_withdraw: T::Share,
891) -> Result<Weight, Error> {
892 ensure!(!to_withdraw.is_zero(), Error::ZeroWithdraw);
895
896 let mut weight = T::WeightInfo::withdraw_stake();
897 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
898 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
899 ensure!(
900 operator.is_operator_registered_or_deactivated::<T>(operator_id),
901 Error::OperatorNotRegisterdOrDeactivated
902 );
903
904 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
906 note_pending_staking_operation::<T>(operator.current_domain_id)?;
907 }
908
909 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
911 .ok_or(Error::DomainNotInitialized)?;
912 let domain_current_epoch = (
913 operator.current_domain_id,
914 domain_stake_summary.current_epoch_index,
915 )
916 .into();
917
918 let known_shares =
919 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
920 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
921 do_convert_previous_epoch_deposits::<T>(
922 operator_id,
923 deposit,
924 domain_stake_summary.current_epoch_index,
925 )?;
926 Ok(deposit.known.shares)
927 })?;
928
929 Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
930 if let Some(withdrawal) = maybe_withdrawal {
931 do_convert_previous_epoch_withdrawal::<T>(
932 operator_id,
933 withdrawal,
934 domain_stake_summary.current_epoch_index,
935 )?;
936 if withdrawal.withdrawals.len() as u32 >= T::WithdrawalLimit::get() {
937 return Err(Error::TooManyWithdrawals);
938 }
939 }
940 Ok(())
941 })?;
942
943 if matches!(operator.partial_status, OperatorStatus::Deactivated(_)) {
946 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
947 operators.insert(operator_id)
948 });
949
950 weight = T::WeightInfo::withdraw_stake_from_deactivated_operator();
951 }
952
953 let operator_owner =
954 OperatorIdOwner::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
955
956 let is_operator_owner = operator_owner == nominator_id;
957
958 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
959 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
960
961 let (remaining_shares, shares_withdrew) = {
962 let remaining_shares = known_shares
963 .checked_sub(&to_withdraw)
964 .ok_or(Error::InsufficientShares)?;
965
966 if remaining_shares.is_zero() {
968 if is_operator_owner {
969 return Err(Error::MinimumOperatorStake);
970 }
971
972 (remaining_shares, to_withdraw)
973 } else {
974 let share_price =
975 current_share_price::<T>(operator_id, operator, &domain_stake_summary)?;
976
977 let remaining_storage_fee =
978 Perquintill::from_rational(remaining_shares.into(), known_shares.into())
979 .mul_floor(deposit.known.storage_fee_deposit);
980
981 let remaining_stake = share_price
982 .shares_to_stake::<T>(remaining_shares)
983 .checked_add(&remaining_storage_fee)
984 .ok_or(Error::BalanceOverflow)?;
985
986 if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
989 return Err(Error::MinimumOperatorStake);
990 }
991
992 if !is_operator_owner && remaining_stake.lt(&operator.minimum_nominator_stake) {
994 (T::Share::zero(), known_shares)
995 } else {
996 (remaining_shares, to_withdraw)
997 }
998 }
999 };
1000
1001 let storage_fee_to_withdraw =
1004 Perquintill::from_rational(shares_withdrew.into(), known_shares.into())
1005 .mul_floor(deposit.known.storage_fee_deposit);
1006
1007 let withdraw_storage_fee = {
1008 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1009 operator_id,
1010 operator.total_storage_fee_deposit,
1011 );
1012 bundle_storage_fund::withdraw_and_hold::<T>(
1013 operator_id,
1014 &nominator_id,
1015 storage_fund_redeem_price.redeem(storage_fee_to_withdraw),
1016 )
1017 .map_err(Error::BundleStorageFund)?
1018 };
1019
1020 deposit.known.storage_fee_deposit = deposit
1021 .known
1022 .storage_fee_deposit
1023 .checked_sub(&storage_fee_to_withdraw)
1024 .ok_or(Error::BalanceOverflow)?;
1025
1026 operator.total_storage_fee_deposit = operator
1027 .total_storage_fee_deposit
1028 .checked_sub(&storage_fee_to_withdraw)
1029 .ok_or(Error::BalanceOverflow)?;
1030
1031 operator.withdrawals_in_epoch = operator
1033 .withdrawals_in_epoch
1034 .checked_add(&shares_withdrew)
1035 .ok_or(Error::ShareOverflow)?;
1036
1037 deposit.known.shares = remaining_shares;
1038 if remaining_shares.is_zero()
1039 && let Some(pending_deposit) = deposit.pending
1040 {
1041 ensure!(
1044 pending_deposit.total()? >= operator.minimum_nominator_stake,
1045 Error::MinimumNominatorStake
1046 );
1047 }
1048
1049 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
1050 let unlock_at_confirmed_domain_block_number = head_domain_number
1051 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
1052 .ok_or(Error::BlockNumberOverflow)?;
1053
1054 Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
1055 let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
1056 let new_withdrawal_in_shares = match withdrawal.withdrawal_in_shares.take() {
1059 Some(WithdrawalInShares {
1060 shares,
1061 storage_fee_refund,
1062 ..
1063 }) => WithdrawalInShares {
1064 domain_epoch: domain_current_epoch,
1065 shares: shares
1066 .checked_add(&shares_withdrew)
1067 .ok_or(Error::ShareOverflow)?,
1068 unlock_at_confirmed_domain_block_number,
1069 storage_fee_refund: storage_fee_refund
1070 .checked_add(&withdraw_storage_fee)
1071 .ok_or(Error::BalanceOverflow)?,
1072 },
1073 None => WithdrawalInShares {
1074 domain_epoch: domain_current_epoch,
1075 unlock_at_confirmed_domain_block_number,
1076 shares: shares_withdrew,
1077 storage_fee_refund: withdraw_storage_fee,
1078 },
1079 };
1080 withdrawal.withdrawal_in_shares = Some(new_withdrawal_in_shares);
1081 withdrawal.total_storage_fee_withdrawal = withdrawal
1082 .total_storage_fee_withdrawal
1083 .checked_add(&withdraw_storage_fee)
1084 .ok_or(Error::BalanceOverflow)?;
1085
1086 *maybe_withdrawal = Some(withdrawal);
1087 Ok(())
1088 })
1089 })
1090 })?;
1091
1092 Ok(weight)
1093}
1094
1095pub(crate) fn do_unlock_funds<T: Config>(
1099 operator_id: OperatorId,
1100 nominator_id: NominatorId<T>,
1101) -> Result<u32, Error> {
1102 let operator = Operators::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
1103 ensure!(
1104 operator.is_operator_registered_or_deactivated::<T>(operator_id),
1105 Error::OperatorNotRegisterdOrDeactivated
1106 );
1107
1108 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1109 .ok_or(Error::DomainNotInitialized)?
1110 .current_epoch_index;
1111
1112 Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
1113 let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
1114 do_convert_previous_epoch_withdrawal::<T>(
1115 operator_id,
1116 withdrawal,
1117 current_domain_epoch_index,
1118 )?;
1119
1120 ensure!(!withdrawal.withdrawals.is_empty(), Error::MissingWithdrawal);
1121
1122 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
1123
1124 let mut withdrawal_count = 0;
1125 let mut total_unlocked_amount = BalanceOf::<T>::zero();
1126 let mut total_storage_fee_refund = BalanceOf::<T>::zero();
1127 loop {
1128 if withdrawal
1129 .withdrawals
1130 .front()
1131 .map(|w| w.unlock_at_confirmed_domain_block_number > head_domain_number)
1132 .unwrap_or(true)
1133 {
1134 break;
1135 }
1136
1137 let WithdrawalInBalance {
1138 amount_to_unlock,
1139 storage_fee_refund,
1140 ..
1141 } = withdrawal
1142 .withdrawals
1143 .pop_front()
1144 .expect("Must not empty as checked above; qed");
1145
1146 total_unlocked_amount = total_unlocked_amount
1147 .checked_add(&amount_to_unlock)
1148 .ok_or(Error::BalanceOverflow)?;
1149
1150 total_storage_fee_refund = total_storage_fee_refund
1151 .checked_add(&storage_fee_refund)
1152 .ok_or(Error::BalanceOverflow)?;
1153
1154 withdrawal_count += 1;
1155 }
1156
1157 ensure!(
1160 !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
1161 Error::UnlockPeriodNotComplete
1162 );
1163
1164 withdrawal.total_withdrawal_amount = withdrawal
1166 .total_withdrawal_amount
1167 .checked_sub(&total_unlocked_amount)
1168 .ok_or(Error::BalanceUnderflow)?;
1169
1170 withdrawal.total_storage_fee_withdrawal = withdrawal
1171 .total_storage_fee_withdrawal
1172 .checked_sub(&total_storage_fee_refund)
1173 .ok_or(Error::BalanceUnderflow)?;
1174
1175 let (amount_to_mint, amount_to_release) = DepositOnHold::<T>::try_mutate(
1178 (operator_id, nominator_id.clone()),
1179 |deposit_on_hold| {
1180 let amount_to_release = total_unlocked_amount.min(*deposit_on_hold);
1181 let amount_to_mint = total_unlocked_amount.saturating_sub(*deposit_on_hold);
1182
1183 *deposit_on_hold = deposit_on_hold.saturating_sub(amount_to_release);
1184
1185 Ok((amount_to_mint, amount_to_release))
1186 },
1187 )?;
1188
1189 if !amount_to_mint.is_zero() {
1191 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1192 }
1193 if !amount_to_release.is_zero() {
1195 let staked_hold_id = T::HoldIdentifier::staking_staked();
1196 T::Currency::release(
1197 &staked_hold_id,
1198 &nominator_id,
1199 amount_to_release,
1200 Precision::Exact,
1201 )
1202 .map_err(|_| Error::RemoveLock)?;
1203 }
1204
1205 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1206 operator_id,
1207 nominator_id: nominator_id.clone(),
1208 unlocked_amount: total_unlocked_amount,
1209 });
1210
1211 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1213 T::Currency::release(
1214 &storage_fund_hold_id,
1215 &nominator_id,
1216 total_storage_fee_refund,
1217 Precision::Exact,
1218 )
1219 .map_err(|_| Error::RemoveLock)?;
1220
1221 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1222 operator_id,
1223 nominator_id: nominator_id.clone(),
1224 storage_fee: total_storage_fee_refund,
1225 });
1226
1227 if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1229 *maybe_withdrawal = None;
1230 Deposits::<T>::mutate_exists(operator_id, nominator_id.clone(), |maybe_deposit| {
1232 if let Some(deposit) = maybe_deposit
1233 && deposit.known.shares.is_zero()
1234 && deposit.pending.is_none()
1235 {
1236 *maybe_deposit = None;
1237
1238 DepositOnHold::<T>::mutate_exists(
1239 (operator_id, nominator_id),
1240 |maybe_deposit_on_hold| {
1241 if let Some(deposit_on_hold) = maybe_deposit_on_hold
1242 && deposit_on_hold.is_zero()
1243 {
1244 *maybe_deposit_on_hold = None
1245 }
1246 },
1247 );
1248 }
1249 });
1250 }
1251
1252 Ok(withdrawal_count)
1253 })
1254}
1255
1256pub(crate) fn do_unlock_nominator<T: Config>(
1258 operator_id: OperatorId,
1259 nominator_id: NominatorId<T>,
1260) -> Result<(), Error> {
1261 Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
1262 let mut operator = maybe_operator.take().ok_or(Error::UnknownOperator)?;
1264 let OperatorDeregisteredInfo {
1265 domain_epoch,
1266 unlock_at_confirmed_domain_block_number,
1267 } = match operator.status::<T>(operator_id) {
1268 OperatorStatus::Deregistered(operator_deregistered_info) => operator_deregistered_info,
1269 _ => return Err(Error::OperatorNotDeregistered),
1270 };
1271
1272 let (domain_id, _) = domain_epoch.deconstruct();
1273 let head_domain_number = HeadDomainNumber::<T>::get(domain_id);
1274 ensure!(
1275 *unlock_at_confirmed_domain_block_number <= head_domain_number,
1276 Error::UnlockPeriodNotComplete
1277 );
1278
1279 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1280 .ok_or(Error::DomainNotInitialized)?
1281 .current_epoch_index;
1282
1283 let mut total_shares = operator.current_total_shares;
1284 let mut total_stake = operator.current_total_stake;
1285 let share_price = SharePrice::new::<T>(total_shares, total_stake)?;
1286
1287 let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
1288 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1289 operator_id,
1290 total_storage_fee_deposit,
1291 );
1292 let mut deposit = Deposits::<T>::take(operator_id, nominator_id.clone())
1293 .ok_or(Error::UnknownNominator)?;
1294
1295 do_convert_previous_epoch_deposits::<T>(
1302 operator_id,
1303 &mut deposit,
1304 current_domain_epoch_index,
1305 )?;
1306
1307 let (
1310 amount_ready_to_withdraw,
1311 total_storage_fee_withdrawal,
1312 shares_withdrew_in_current_epoch,
1313 ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
1314 .map(|mut withdrawal| {
1315 do_convert_previous_epoch_withdrawal::<T>(
1320 operator_id,
1321 &mut withdrawal,
1322 current_domain_epoch_index,
1323 )?;
1324 Ok((
1325 withdrawal.total_withdrawal_amount,
1326 withdrawal.total_storage_fee_withdrawal,
1327 withdrawal
1328 .withdrawal_in_shares
1329 .map(|WithdrawalInShares { shares, .. }| shares)
1330 .unwrap_or_default(),
1331 ))
1332 })
1333 .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
1334
1335 let nominator_shares = deposit
1337 .known
1338 .shares
1339 .checked_add(&shares_withdrew_in_current_epoch)
1340 .ok_or(Error::ShareOverflow)?;
1341 total_shares = total_shares
1342 .checked_sub(&nominator_shares)
1343 .ok_or(Error::ShareOverflow)?;
1344
1345 let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
1347 total_stake = total_stake
1348 .checked_sub(&nominator_staked_amount)
1349 .ok_or(Error::BalanceOverflow)?;
1350
1351 let amount_deposited_in_epoch = deposit
1353 .pending
1354 .map(|pending_deposit| pending_deposit.amount)
1355 .unwrap_or_default();
1356
1357 let total_amount_to_unlock = nominator_staked_amount
1358 .checked_add(&amount_ready_to_withdraw)
1359 .and_then(|amount| amount.checked_add(&amount_deposited_in_epoch))
1360 .ok_or(Error::BalanceOverflow)?;
1361
1362 let current_locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
1364 if let Some(amount_to_mint) = total_amount_to_unlock.checked_sub(¤t_locked_amount) {
1365 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1366 }
1367 if !current_locked_amount.is_zero() {
1368 let staked_hold_id = T::HoldIdentifier::staking_staked();
1369 T::Currency::release(
1370 &staked_hold_id,
1371 &nominator_id,
1372 current_locked_amount,
1373 Precision::Exact,
1374 )
1375 .map_err(|_| Error::RemoveLock)?;
1376 }
1377
1378 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1379 operator_id,
1380 nominator_id: nominator_id.clone(),
1381 unlocked_amount: total_amount_to_unlock,
1382 });
1383
1384 let nominator_total_storage_fee_deposit = deposit
1386 .pending
1387 .map(|pending_deposit| pending_deposit.storage_fee_deposit)
1388 .unwrap_or(Zero::zero())
1389 .checked_add(&deposit.known.storage_fee_deposit)
1390 .ok_or(Error::BalanceOverflow)?;
1391
1392 bundle_storage_fund::withdraw_to::<T>(
1393 operator_id,
1394 &nominator_id,
1395 storage_fund_redeem_price.redeem(nominator_total_storage_fee_deposit),
1396 )
1397 .map_err(Error::BundleStorageFund)?;
1398
1399 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1401 T::Currency::release(
1402 &storage_fund_hold_id,
1403 &nominator_id,
1404 total_storage_fee_withdrawal,
1405 Precision::Exact,
1406 )
1407 .map_err(|_| Error::RemoveLock)?;
1408
1409 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1410 operator_id,
1411 nominator_id: nominator_id.clone(),
1412 storage_fee: total_storage_fee_withdrawal,
1413 });
1414
1415 total_storage_fee_deposit =
1417 total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1418
1419 let cleanup_operator = !Deposits::<T>::contains_prefix(operator_id)
1422 && !Withdrawals::<T>::contains_prefix(operator_id);
1423
1424 if cleanup_operator {
1425 do_cleanup_operator::<T>(operator_id, total_stake)?
1426 } else {
1427 operator.current_total_shares = total_shares;
1429 operator.current_total_stake = total_stake;
1430 operator.total_storage_fee_deposit = total_storage_fee_deposit;
1431
1432 *maybe_operator = Some(operator);
1433 }
1434
1435 Ok(())
1436 })
1437}
1438
1439pub(crate) fn do_cleanup_operator<T: Config>(
1441 operator_id: OperatorId,
1442 total_stake: BalanceOf<T>,
1443) -> Result<(), Error> {
1444 bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1446 .map_err(Error::BundleStorageFund)?;
1447
1448 mint_into_treasury::<T>(total_stake)?;
1450
1451 OperatorIdOwner::<T>::remove(operator_id);
1453
1454 OperatorHighestSlot::<T>::remove(operator_id);
1456
1457 let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1459
1460 Ok(())
1461}
1462
1463pub(crate) fn do_reward_operators<T: Config>(
1465 domain_id: DomainId,
1466 source: OperatorRewardSource<BlockNumberFor<T>>,
1467 operators: IntoIter<OperatorId>,
1468 rewards: BalanceOf<T>,
1469) -> Result<(), Error> {
1470 if rewards.is_zero() {
1471 return Ok(());
1472 }
1473 DomainStakingSummary::<T>::mutate(domain_id, |maybe_stake_summary| {
1474 let stake_summary = maybe_stake_summary
1475 .as_mut()
1476 .ok_or(Error::DomainNotInitialized)?;
1477
1478 let total_count = operators.len() as u64;
1479 let operator_weights = operators.into_iter().fold(
1481 BTreeMap::<OperatorId, u64>::new(),
1482 |mut acc, operator_id| {
1483 acc.entry(operator_id)
1484 .and_modify(|weight| *weight += 1)
1485 .or_insert(1);
1486 acc
1487 },
1488 );
1489
1490 let mut allocated_rewards = BalanceOf::<T>::zero();
1491 for (operator_id, weight) in operator_weights {
1492 let operator_reward = {
1493 let distribution = Perquintill::from_rational(weight, total_count);
1494 distribution.mul_floor(rewards)
1495 };
1496
1497 stake_summary
1498 .current_epoch_rewards
1499 .entry(operator_id)
1500 .and_modify(|rewards| *rewards = rewards.saturating_add(operator_reward))
1501 .or_insert(operator_reward);
1502
1503 Pallet::<T>::deposit_event(Event::OperatorRewarded {
1504 source: source.clone(),
1505 operator_id,
1506 reward: operator_reward,
1507 });
1508
1509 allocated_rewards = allocated_rewards
1510 .checked_add(&operator_reward)
1511 .ok_or(Error::BalanceOverflow)?;
1512 }
1513
1514 mint_into_treasury::<T>(
1516 rewards
1517 .checked_sub(&allocated_rewards)
1518 .ok_or(Error::BalanceUnderflow)?,
1519 )
1520 })
1521}
1522
1523pub(crate) fn do_mark_operators_as_slashed<T: Config>(
1526 operator_ids: impl AsRef<[OperatorId]>,
1527 slash_reason: SlashedReason<DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
1528) -> Result<(), Error> {
1529 for operator_id in operator_ids.as_ref() {
1530 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1531 let operator = match maybe_operator.as_mut() {
1532 None => return Ok(()),
1536 Some(operator) => operator,
1537 };
1538 let mut pending_slashes =
1539 PendingSlashes::<T>::get(operator.current_domain_id).unwrap_or_default();
1540
1541 if pending_slashes.contains(operator_id) {
1542 return Ok(());
1543 }
1544
1545 DomainStakingSummary::<T>::try_mutate(
1546 operator.current_domain_id,
1547 |maybe_domain_stake_summary| {
1548 let stake_summary = maybe_domain_stake_summary
1549 .as_mut()
1550 .ok_or(Error::DomainNotInitialized)?;
1551
1552 operator.update_status(OperatorStatus::Slashed);
1554
1555 if stake_summary
1558 .current_operators
1559 .remove(operator_id)
1560 .is_some()
1561 {
1562 stake_summary.current_total_stake = stake_summary
1563 .current_total_stake
1564 .checked_sub(&operator.current_total_stake)
1565 .ok_or(Error::BalanceUnderflow)?;
1566 }
1567 stake_summary.next_operators.remove(operator_id);
1568 pending_slashes.insert(*operator_id);
1569 PendingSlashes::<T>::insert(operator.current_domain_id, pending_slashes);
1570 Pallet::<T>::deposit_event(Event::OperatorSlashed {
1571 operator_id: *operator_id,
1572 reason: slash_reason.clone(),
1573 });
1574 Ok(())
1575 },
1576 )
1577 })?
1578 }
1579
1580 Ok(())
1581}
1582
1583pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1586 domain_id: DomainId,
1587 er: &ExecutionReceiptOf<T>,
1588) -> Result<(), Error> {
1589 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1590 let er_hash = er.hash::<DomainHashingFor<T>>();
1591 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1592 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1593 let mut stake_summary =
1594 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1595
1596 for operator_id in invalid_bundle_authors {
1597 if pending_slashes.contains(&operator_id) {
1598 continue;
1599 }
1600
1601 mark_invalid_bundle_author::<T>(
1602 operator_id,
1603 er_hash,
1604 &mut stake_summary,
1605 &mut invalid_bundle_authors_in_epoch,
1606 )?;
1607 }
1608
1609 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1610 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1611 Ok(())
1612}
1613
1614pub(crate) fn mark_invalid_bundle_author<T: Config>(
1615 operator_id: OperatorId,
1616 er_hash: ReceiptHashFor<T>,
1617 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1618 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1619) -> Result<(), Error> {
1620 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1621 let operator = match maybe_operator.as_mut() {
1622 None => return Ok(()),
1626 Some(operator) => operator,
1627 };
1628
1629 if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1632 return Ok(());
1633 }
1634
1635 operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1637 invalid_bundle_authors.insert(operator_id);
1638 if stake_summary
1639 .current_operators
1640 .remove(&operator_id)
1641 .is_some()
1642 {
1643 stake_summary.current_total_stake = stake_summary
1644 .current_total_stake
1645 .checked_sub(&operator.current_total_stake)
1646 .ok_or(Error::BalanceUnderflow)?;
1647 }
1648 stake_summary.next_operators.remove(&operator_id);
1649 Ok(())
1650 })
1651}
1652
1653pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1658 domain_id: DomainId,
1659 er: &ExecutionReceiptOf<T>,
1660) -> Result<(), Error> {
1661 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1662 let er_hash = er.hash::<DomainHashingFor<T>>();
1663 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1664 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1665 let mut stake_summary =
1666 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1667
1668 for operator_id in invalid_bundle_authors {
1669 if pending_slashes.contains(&operator_id)
1670 || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1671 {
1672 continue;
1673 }
1674
1675 unmark_invalid_bundle_author::<T>(
1676 operator_id,
1677 er_hash,
1678 &mut stake_summary,
1679 &mut invalid_bundle_authors_in_epoch,
1680 )?;
1681 }
1682
1683 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1684 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1685 Ok(())
1686}
1687
1688pub(crate) fn unmark_invalid_bundle_author<T: Config>(
1689 operator_id: OperatorId,
1690 er_hash: ReceiptHashFor<T>,
1691 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1692 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1693) -> Result<(), Error> {
1694 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1695 let operator = match maybe_operator.as_mut() {
1696 None => return Ok(()),
1700 Some(operator) => operator,
1701 };
1702
1703 if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1705 return Ok(());
1706 }
1707
1708 operator.update_status(OperatorStatus::Registered);
1710 invalid_bundle_authors.remove(&operator_id);
1711 stake_summary.next_operators.insert(operator_id);
1712 Ok(())
1713 })
1714}
1715
1716#[cfg(test)]
1717pub(crate) mod tests {
1718 use crate::domain_registry::{DomainConfig, DomainObject};
1719 use crate::mock::{
1720 AccountId, ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, TreasuryAccount,
1721 };
1722 use crate::pallet::{
1723 Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1724 NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1725 };
1726 use crate::staking::{
1727 DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorDeregisteredInfo,
1728 OperatorStatus, SharePrice, StakingSummary, do_convert_previous_epoch_withdrawal,
1729 do_mark_operators_as_slashed, do_nominate_operator, do_reward_operators, do_unlock_funds,
1730 do_withdraw_stake,
1731 };
1732 use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1733 use crate::tests::new_test_ext;
1734 use crate::{
1735 BalanceOf, DeactivatedOperators, DeregisteredOperators, Error, MAX_NOMINATORS_TO_SLASH,
1736 NominatorId, OperatorEpochSharePrice, SlashedReason, bundle_storage_fund,
1737 };
1738 use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
1739 use frame_support::traits::Currency;
1740 use frame_support::traits::fungible::Mutate;
1741 use frame_support::weights::Weight;
1742 use frame_support::{assert_err, assert_ok};
1743 use prop_test::prelude::*;
1744 use prop_test::proptest::test_runner::TestCaseResult;
1745 use sp_core::{Pair, sr25519};
1746 use sp_domains::{
1747 DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1748 OperatorRewardSource,
1749 };
1750 use sp_runtime::traits::Zero;
1751 use sp_runtime::{PerThing, Percent, Perquintill};
1752 use std::collections::{BTreeMap, BTreeSet};
1753 use std::ops::RangeInclusive;
1754 use std::vec;
1755 use subspace_runtime_primitives::AI3;
1756
1757 type Balances = pallet_balances::Pallet<Test>;
1758 type Domains = crate::Pallet<Test>;
1759
1760 const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1761
1762 #[allow(clippy::too_many_arguments)]
1763 pub(crate) fn register_operator(
1764 domain_id: DomainId,
1765 operator_account: <Test as frame_system::Config>::AccountId,
1766 operator_free_balance: BalanceOf<Test>,
1767 operator_stake: BalanceOf<Test>,
1768 minimum_nominator_stake: BalanceOf<Test>,
1769 signing_key: OperatorPublicKey,
1770 nomination_tax: Percent,
1771 mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1772 ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1773 nominators.insert(operator_account, (operator_free_balance, operator_stake));
1774 for nominator in &nominators {
1775 Balances::set_balance(nominator.0, nominator.1.0);
1776 assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1777 }
1778 nominators.remove(&operator_account);
1779
1780 if !DomainRegistry::<Test>::contains_key(domain_id) {
1781 let domain_config = DomainConfig {
1782 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1783 runtime_id: 0,
1784 max_bundle_size: u32::MAX,
1785 max_bundle_weight: Weight::MAX,
1786 bundle_slot_probability: (0, 0),
1787 operator_allow_list: OperatorAllowList::Anyone,
1788 initial_balances: Default::default(),
1789 };
1790
1791 let domain_obj = DomainObject {
1792 owner_account_id: 0,
1793 created_at: 0,
1794 genesis_receipt_hash: Default::default(),
1795 domain_config,
1796 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1797 domain_instantiation_deposit: Default::default(),
1798 };
1799
1800 DomainRegistry::<Test>::insert(domain_id, domain_obj);
1801 }
1802
1803 if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1804 DomainStakingSummary::<Test>::insert(
1805 domain_id,
1806 StakingSummary {
1807 current_epoch_index: 0,
1808 current_total_stake: 0,
1809 current_operators: BTreeMap::new(),
1810 next_operators: BTreeSet::new(),
1811 current_epoch_rewards: BTreeMap::new(),
1812 },
1813 );
1814 }
1815
1816 let operator_config = OperatorConfig {
1817 signing_key,
1818 minimum_nominator_stake,
1819 nomination_tax,
1820 };
1821
1822 let res = Domains::register_operator(
1823 RuntimeOrigin::signed(operator_account),
1824 domain_id,
1825 operator_stake,
1826 operator_config.clone(),
1827 );
1828 assert_ok!(res);
1829
1830 let operator_id = NextOperatorId::<Test>::get() - 1;
1831 for nominator in nominators {
1832 if nominator.1.1.is_zero() {
1833 continue;
1834 }
1835
1836 let res = Domains::nominate_operator(
1837 RuntimeOrigin::signed(nominator.0),
1838 operator_id,
1839 nominator.1.1,
1840 );
1841 assert_ok!(res);
1842 assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1843 }
1844
1845 (operator_id, operator_config)
1846 }
1847
1848 #[test]
1849 fn test_register_operator_invalid_signing_key() {
1850 let domain_id = DomainId::new(0);
1851 let operator_account = 1;
1852
1853 let mut ext = new_test_ext();
1854 ext.execute_with(|| {
1855 let operator_config = OperatorConfig {
1856 signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1857 minimum_nominator_stake: Default::default(),
1858 nomination_tax: Default::default(),
1859 };
1860
1861 let res = Domains::register_operator(
1862 RuntimeOrigin::signed(operator_account),
1863 domain_id,
1864 Default::default(),
1865 operator_config,
1866 );
1867 assert_err!(
1868 res,
1869 Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1870 );
1871 });
1872 }
1873
1874 #[test]
1875 fn test_register_operator_minimum_nominator_stake() {
1876 let domain_id = DomainId::new(0);
1877 let operator_account = 1;
1878 let pair = OperatorPair::from_seed(&[0; 32]);
1879
1880 let mut ext = new_test_ext();
1881 ext.execute_with(|| {
1882 let operator_config = OperatorConfig {
1883 signing_key: pair.public(),
1884 minimum_nominator_stake: Default::default(),
1885 nomination_tax: Default::default(),
1886 };
1887
1888 let res = Domains::register_operator(
1889 RuntimeOrigin::signed(operator_account),
1890 domain_id,
1891 Default::default(),
1892 operator_config,
1893 );
1894 assert_err!(
1895 res,
1896 Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1897 );
1898 });
1899 }
1900
1901 #[test]
1902 fn test_register_operator() {
1903 let domain_id = DomainId::new(0);
1904 let operator_account = 1;
1905 let operator_free_balance = 2500 * AI3;
1906 let operator_total_stake = 1000 * AI3;
1907 let operator_stake = 800 * AI3;
1908 let operator_storage_fee_deposit = 200 * AI3;
1909 let pair = OperatorPair::from_seed(&[0; 32]);
1910
1911 let mut ext = new_test_ext();
1912 ext.execute_with(|| {
1913 let (operator_id, mut operator_config) = register_operator(
1914 domain_id,
1915 operator_account,
1916 operator_free_balance,
1917 operator_total_stake,
1918 AI3,
1919 pair.public(),
1920 Default::default(),
1921 BTreeMap::new(),
1922 );
1923
1924 assert_eq!(NextOperatorId::<Test>::get(), 1);
1925 assert_eq!(
1927 OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1928 operator_account
1929 );
1930 assert_eq!(
1931 Operators::<Test>::get(operator_id).unwrap(),
1932 Operator {
1933 signing_key: pair.public(),
1934 current_domain_id: domain_id,
1935 next_domain_id: domain_id,
1936 minimum_nominator_stake: AI3,
1937 nomination_tax: Default::default(),
1938 current_total_stake: operator_stake,
1939 current_total_shares: operator_stake,
1940 partial_status: OperatorStatus::Registered,
1941 deposits_in_epoch: 0,
1942 withdrawals_in_epoch: 0,
1943 total_storage_fee_deposit: operator_storage_fee_deposit,
1944 }
1945 );
1946
1947 let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1948 assert!(stake_summary.next_operators.contains(&operator_id));
1949 assert_eq!(stake_summary.current_total_stake, operator_stake);
1950
1951 assert_eq!(
1952 Balances::usable_balance(operator_account),
1953 operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1954 );
1955
1956 let res = Domains::register_operator(
1958 RuntimeOrigin::signed(operator_account),
1959 domain_id,
1960 operator_stake,
1961 operator_config.clone(),
1962 );
1963 assert_ok!(res);
1964
1965 let new_pair = OperatorPair::from_seed(&[1; 32]);
1967 operator_config.signing_key = new_pair.public();
1968 let res = Domains::register_operator(
1969 RuntimeOrigin::signed(operator_account),
1970 domain_id,
1971 operator_stake,
1972 operator_config,
1973 );
1974 assert_err!(
1975 res,
1976 Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1977 );
1978 });
1979 }
1980
1981 #[test]
1982 fn nominate_operator() {
1983 let domain_id = DomainId::new(0);
1984 let operator_account = 1;
1985 let operator_free_balance = 1500 * AI3;
1986 let operator_total_stake = 1000 * AI3;
1987 let operator_stake = 800 * AI3;
1988 let operator_storage_fee_deposit = 200 * AI3;
1989 let pair = OperatorPair::from_seed(&[0; 32]);
1990
1991 let nominator_account = 2;
1992 let nominator_free_balance = 150 * AI3;
1993 let nominator_total_stake = 100 * AI3;
1994 let nominator_stake = 80 * AI3;
1995 let nominator_storage_fee_deposit = 20 * AI3;
1996
1997 let mut ext = new_test_ext();
1998 ext.execute_with(|| {
1999 let (operator_id, _) = register_operator(
2000 domain_id,
2001 operator_account,
2002 operator_free_balance,
2003 operator_total_stake,
2004 10 * AI3,
2005 pair.public(),
2006 Default::default(),
2007 BTreeMap::from_iter(vec![(
2008 nominator_account,
2009 (nominator_free_balance, nominator_total_stake),
2010 )]),
2011 );
2012
2013 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2014 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
2015
2016 let operator = Operators::<Test>::get(operator_id).unwrap();
2017 assert_eq!(operator.current_total_stake, operator_stake);
2018 assert_eq!(operator.current_total_shares, operator_stake);
2019 assert_eq!(
2020 operator.total_storage_fee_deposit,
2021 operator_storage_fee_deposit + nominator_storage_fee_deposit
2022 );
2023 assert_eq!(operator.deposits_in_epoch, nominator_stake);
2024
2025 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2026 .unwrap()
2027 .pending
2028 .unwrap();
2029 assert_eq!(pending_deposit.amount, nominator_stake);
2030 assert_eq!(
2031 pending_deposit.storage_fee_deposit,
2032 nominator_storage_fee_deposit
2033 );
2034 assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
2035
2036 assert_eq!(
2037 Balances::usable_balance(nominator_account),
2038 nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
2039 );
2040
2041 let additional_nomination_total_stake = 40 * AI3;
2043 let additional_nomination_stake = 32 * AI3;
2044 let additional_nomination_storage_fee_deposit = 8 * AI3;
2045 let res = Domains::nominate_operator(
2046 RuntimeOrigin::signed(nominator_account),
2047 operator_id,
2048 additional_nomination_total_stake,
2049 );
2050 assert_ok!(res);
2051 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2052 .unwrap()
2053 .pending
2054 .unwrap();
2055 assert_eq!(
2056 pending_deposit.amount,
2057 nominator_stake + additional_nomination_stake
2058 );
2059 assert_eq!(
2060 pending_deposit.storage_fee_deposit,
2061 nominator_storage_fee_deposit + additional_nomination_storage_fee_deposit
2062 );
2063
2064 let operator = Operators::<Test>::get(operator_id).unwrap();
2065 assert_eq!(operator.current_total_stake, operator_stake);
2066 assert_eq!(
2067 operator.deposits_in_epoch,
2068 nominator_stake + additional_nomination_stake
2069 );
2070 assert_eq!(
2071 operator.total_storage_fee_deposit,
2072 operator_storage_fee_deposit
2073 + nominator_storage_fee_deposit
2074 + additional_nomination_storage_fee_deposit
2075 );
2076
2077 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2079
2080 let operator = Operators::<Test>::get(operator_id).unwrap();
2081 assert_eq!(
2082 operator.current_total_stake,
2083 operator_stake + nominator_stake + additional_nomination_stake
2084 );
2085
2086 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2087 assert_eq!(
2088 domain_staking_summary.current_total_stake,
2089 operator_stake + nominator_stake + additional_nomination_stake
2090 );
2091 });
2092 }
2093
2094 #[test]
2095 fn operator_deregistration() {
2096 let domain_id = DomainId::new(0);
2097 let operator_account = 1;
2098 let operator_stake = 200 * AI3;
2099 let operator_free_balance = 250 * AI3;
2100 let pair = OperatorPair::from_seed(&[0; 32]);
2101 let mut ext = new_test_ext();
2102 ext.execute_with(|| {
2103 let (operator_id, _) = register_operator(
2104 domain_id,
2105 operator_account,
2106 operator_free_balance,
2107 operator_stake,
2108 AI3,
2109 pair.public(),
2110 Default::default(),
2111 BTreeMap::new(),
2112 );
2113
2114 let res =
2115 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2116 assert_ok!(res);
2117
2118 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2119 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2120
2121 let operator = Operators::<Test>::get(operator_id).unwrap();
2122 assert_eq!(
2123 *operator.status::<Test>(operator_id),
2124 OperatorStatus::Deregistered(
2125 (
2126 domain_id,
2127 domain_stake_summary.current_epoch_index,
2128 5
2130 )
2131 .into()
2132 )
2133 );
2134
2135 let new_domain_id = DomainId::new(1);
2137 let domain_config = DomainConfig {
2138 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
2139 runtime_id: 0,
2140 max_bundle_size: u32::MAX,
2141 max_bundle_weight: Weight::MAX,
2142 bundle_slot_probability: (0, 0),
2143 operator_allow_list: OperatorAllowList::Anyone,
2144 initial_balances: Default::default(),
2145 };
2146
2147 let domain_obj = DomainObject {
2148 owner_account_id: 0,
2149 created_at: 0,
2150 genesis_receipt_hash: Default::default(),
2151 domain_config,
2152 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
2153 domain_instantiation_deposit: Default::default(),
2154 };
2155
2156 DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
2157 DomainStakingSummary::<Test>::insert(
2158 new_domain_id,
2159 StakingSummary {
2160 current_epoch_index: 0,
2161 current_total_stake: 0,
2162 current_operators: BTreeMap::new(),
2163 next_operators: BTreeSet::new(),
2164 current_epoch_rewards: BTreeMap::new(),
2165 },
2166 );
2167
2168 let nominator_account = 100;
2170 let nominator_stake = 100 * AI3;
2171 let res = Domains::nominate_operator(
2172 RuntimeOrigin::signed(nominator_account),
2173 operator_id,
2174 nominator_stake,
2175 );
2176 assert_err!(
2177 res,
2178 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2179 );
2180 });
2181 }
2182
2183 #[test]
2184 fn operator_deactivation() {
2185 let domain_id = DomainId::new(0);
2186 let operator_account = 1;
2187 let operator_stake = 200 * AI3;
2188 let operator_free_balance = 250 * AI3;
2189 let pair = OperatorPair::from_seed(&[0; 32]);
2190 let mut ext = new_test_ext();
2191 ext.execute_with(|| {
2192 let (operator_id, _) = register_operator(
2193 domain_id,
2194 operator_account,
2195 operator_free_balance,
2196 operator_stake,
2197 AI3,
2198 pair.public(),
2199 Default::default(),
2200 BTreeMap::new(),
2201 );
2202
2203 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2204 let total_current_stake = domain_stake_summary.current_total_stake;
2205
2206 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2207 assert_ok!(res);
2208
2209 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2210 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2211 assert!(
2212 !domain_stake_summary
2213 .current_operators
2214 .contains_key(&operator_id)
2215 );
2216
2217 let current_epoch_index = domain_stake_summary.current_epoch_index;
2218 let reactivation_delay =
2219 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2220 let operator = Operators::<Test>::get(operator_id).unwrap();
2221 assert_eq!(
2222 *operator.status::<Test>(operator_id),
2223 OperatorStatus::Deactivated(reactivation_delay)
2224 );
2225
2226 let operator_stake = operator.current_total_stake;
2227 assert_eq!(
2228 total_current_stake,
2229 domain_stake_summary.current_total_stake + operator_stake
2230 );
2231
2232 let nominator_account = 100;
2234 let nominator_stake = 100 * AI3;
2235 let res = Domains::nominate_operator(
2236 RuntimeOrigin::signed(nominator_account),
2237 operator_id,
2238 nominator_stake,
2239 );
2240 assert_err!(
2241 res,
2242 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2243 );
2244 });
2245 }
2246
2247 #[test]
2248 fn operator_deactivation_withdraw_stake() {
2249 let domain_id = DomainId::new(0);
2250 let operator_account = 1;
2251 let operator_stake = 200 * AI3;
2252 let operator_free_balance = 250 * AI3;
2253 let pair = OperatorPair::from_seed(&[0; 32]);
2254 let nominator_account = 100;
2255 let nominator_free_balance = 150 * AI3;
2256 let nominator_total_stake = 100 * AI3;
2257 let mut ext = new_test_ext();
2258 ext.execute_with(|| {
2259 let (operator_id, _) = register_operator(
2260 domain_id,
2261 operator_account,
2262 operator_free_balance,
2263 operator_stake,
2264 AI3,
2265 pair.public(),
2266 Default::default(),
2267 BTreeMap::from_iter(vec![(
2268 nominator_account,
2269 (nominator_free_balance, nominator_total_stake),
2270 )]),
2271 );
2272
2273 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2274 let total_current_stake = domain_stake_summary.current_total_stake;
2275
2276 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2278 assert_ok!(res);
2279
2280 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2281 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2282 assert!(
2283 !domain_stake_summary
2284 .current_operators
2285 .contains_key(&operator_id)
2286 );
2287
2288 let current_epoch_index = domain_stake_summary.current_epoch_index;
2289 let reactivation_delay =
2290 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2291 let operator = Operators::<Test>::get(operator_id).unwrap();
2292 assert_eq!(
2293 *operator.status::<Test>(operator_id),
2294 OperatorStatus::Deactivated(reactivation_delay)
2295 );
2296
2297 let operator_stake = operator.current_total_stake;
2298 assert_eq!(
2299 total_current_stake,
2300 domain_stake_summary.current_total_stake + operator_stake
2301 );
2302
2303 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2305
2306 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2308
2309 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2311
2312 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2314 let res = Domains::withdraw_stake(
2315 RuntimeOrigin::signed(nominator_account),
2316 operator_id,
2317 nominator_shares,
2318 );
2319 assert_ok!(res);
2320
2321 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2324
2325 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2326 let current_epoch_index = domain_stake_summary.current_epoch_index;
2327
2328 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2330
2331 let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2333 assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2334
2335 let nominator_withdrawals =
2336 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
2337 let unlock_funds_at = nominator_withdrawals
2338 .withdrawal_in_shares
2339 .unwrap()
2340 .unlock_at_confirmed_domain_block_number;
2341 HeadDomainNumber::<Test>::insert(domain_id, unlock_funds_at + 1);
2342
2343 let res = Domains::unlock_funds(RuntimeOrigin::signed(nominator_account), operator_id);
2345 assert_ok!(res);
2346 });
2347 }
2348
2349 #[test]
2350 fn operator_deactivation_deregister_operator() {
2351 let domain_id = DomainId::new(0);
2352 let operator_account = 1;
2353 let operator_stake = 200 * AI3;
2354 let operator_free_balance = 250 * AI3;
2355 let pair = OperatorPair::from_seed(&[0; 32]);
2356 let nominator_account = 100;
2357 let nominator_free_balance = 150 * AI3;
2358 let nominator_total_stake = 100 * AI3;
2359 let mut ext = new_test_ext();
2360 ext.execute_with(|| {
2361 let (operator_id, _) = register_operator(
2362 domain_id,
2363 operator_account,
2364 operator_free_balance,
2365 operator_stake,
2366 AI3,
2367 pair.public(),
2368 Default::default(),
2369 BTreeMap::from_iter(vec![(
2370 nominator_account,
2371 (nominator_free_balance, nominator_total_stake),
2372 )]),
2373 );
2374
2375 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2376 let total_current_stake = domain_stake_summary.current_total_stake;
2377
2378 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2380 assert_ok!(res);
2381
2382 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2383 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2384 assert!(
2385 !domain_stake_summary
2386 .current_operators
2387 .contains_key(&operator_id)
2388 );
2389
2390 let current_epoch_index = domain_stake_summary.current_epoch_index;
2391 let reactivation_delay =
2392 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2393 let operator = Operators::<Test>::get(operator_id).unwrap();
2394 assert_eq!(
2395 *operator.status::<Test>(operator_id),
2396 OperatorStatus::Deactivated(reactivation_delay)
2397 );
2398
2399 let operator_stake = operator.current_total_stake;
2400 assert_eq!(
2401 total_current_stake,
2402 domain_stake_summary.current_total_stake + operator_stake
2403 );
2404
2405 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2407
2408 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2410
2411 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2413
2414 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2416 let res = Domains::withdraw_stake(
2417 RuntimeOrigin::signed(nominator_account),
2418 operator_id,
2419 nominator_shares,
2420 );
2421 assert_ok!(res);
2422
2423 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2426
2427 let res =
2429 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2430 assert_ok!(res);
2431
2432 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2435 assert!(DeregisteredOperators::<Test>::get(domain_id).contains(&operator_id));
2436
2437 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2438 let current_epoch_index = domain_stake_summary.current_epoch_index;
2439
2440 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2442
2443 let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2445 assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2446
2447 let unlock_operator_at = HeadDomainNumber::<Test>::get(domain_id)
2448 + <Test as Config>::StakeWithdrawalLockingPeriod::get();
2449
2450 let operator = Operators::<Test>::get(operator_id).unwrap();
2451 assert_eq!(
2452 operator.partial_status,
2453 OperatorStatus::Deregistered(OperatorDeregisteredInfo {
2454 domain_epoch,
2455 unlock_at_confirmed_domain_block_number: unlock_operator_at
2456 })
2457 );
2458
2459 HeadDomainNumber::<Test>::insert(domain_id, unlock_operator_at);
2460
2461 let res =
2463 Domains::unlock_nominator(RuntimeOrigin::signed(nominator_account), operator_id);
2464 assert_ok!(res);
2465
2466 let res =
2468 Domains::unlock_nominator(RuntimeOrigin::signed(operator_account), operator_id);
2469 assert_ok!(res);
2470
2471 assert!(OperatorIdOwner::<Test>::get(operator_id).is_none());
2473 });
2474 }
2475
2476 #[test]
2477 fn operator_reactivation() {
2478 let domain_id = DomainId::new(0);
2479 let operator_account = 1;
2480 let operator_stake = 200 * AI3;
2481 let operator_free_balance = 250 * AI3;
2482 let nominator_account = 100;
2483 let nominator_free_balance = 150 * AI3;
2484 let nominator_total_stake = 100 * AI3;
2485 let pair = OperatorPair::from_seed(&[0; 32]);
2486 let mut ext = new_test_ext();
2487 ext.execute_with(|| {
2488 let (operator_id, _) = register_operator(
2489 domain_id,
2490 operator_account,
2491 operator_free_balance,
2492 operator_stake,
2493 AI3,
2494 pair.public(),
2495 Default::default(),
2496 BTreeMap::from_iter(vec![(
2497 nominator_account,
2498 (nominator_free_balance, nominator_total_stake),
2499 )]),
2500 );
2501
2502 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2503 assert_ok!(res);
2504
2505 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2506 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2507 assert!(
2508 !domain_stake_summary
2509 .current_operators
2510 .contains_key(&operator_id)
2511 );
2512
2513 let current_epoch_index = domain_stake_summary.current_epoch_index;
2514 let reactivation_delay =
2515 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2516 let operator = Operators::<Test>::get(operator_id).unwrap();
2517 assert_eq!(
2518 *operator.status::<Test>(operator_id),
2519 OperatorStatus::Deactivated(reactivation_delay)
2520 );
2521
2522 let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2524 assert_err!(
2525 res,
2526 Error::<Test>::Staking(crate::staking::Error::ReactivationDelayPeriodIncomplete)
2527 );
2528
2529 for expected_epoch in (current_epoch_index + 1)..=reactivation_delay {
2530 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2531 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2532 assert_eq!(domain_stake_summary.current_epoch_index, expected_epoch);
2533 }
2534
2535 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2537
2538 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2540 let res = Domains::withdraw_stake(
2541 RuntimeOrigin::signed(nominator_account),
2542 operator_id,
2543 nominator_shares,
2544 );
2545 assert_ok!(res);
2546
2547 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2549
2550 let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2551 assert_ok!(res);
2552
2553 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2556
2557 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2558 assert!(domain_stake_summary.next_operators.contains(&operator_id));
2559
2560 let operator = Operators::<Test>::get(operator_id).unwrap();
2561 assert_eq!(
2562 *operator.status::<Test>(operator_id),
2563 OperatorStatus::Registered
2564 );
2565 });
2566 }
2567
2568 type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2569
2570 type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2574
2575 type StorageFundChange = (bool, u32);
2577
2578 pub(crate) type Share = <Test as Config>::Share;
2579
2580 struct WithdrawParams {
2581 minimum_nominator_stake: BalanceOf<Test>,
2583 nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2586 operator_reward: BalanceOf<Test>,
2588 nominator_id: NominatorId<Test>,
2590 withdraws: WithdrawWithResult,
2592 maybe_deposit: Option<BalanceOf<Test>>,
2594 expected_withdraw: ExpectedWithdrawAmount,
2597 expected_nominator_count_reduced_by: u32,
2599 storage_fund_change: StorageFundChange,
2601 }
2602
2603 fn withdraw_stake(params: WithdrawParams) {
2605 withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2606 }
2607
2608 fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2610 withdraw_stake_inner(params, true)
2611 }
2612
2613 fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2615 let WithdrawParams {
2616 minimum_nominator_stake,
2617 nominators,
2618 operator_reward,
2619 nominator_id,
2620 withdraws,
2621 maybe_deposit,
2622 expected_withdraw,
2623 expected_nominator_count_reduced_by,
2624 storage_fund_change,
2625 } = params;
2626 let domain_id = DomainId::new(0);
2627 let operator_account = 0;
2628 let pair = OperatorPair::from_seed(&[0; 32]);
2629 let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2630 + operator_reward
2631 + maybe_deposit.unwrap_or(0);
2632
2633 let mut nominators = BTreeMap::from_iter(
2634 nominators
2635 .into_iter()
2636 .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2637 .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2638 );
2639
2640 let mut ext = new_test_ext();
2641 ext.execute_with(|| {
2642 let (operator_free_balance, operator_stake) =
2643 nominators.remove(&operator_account).unwrap();
2644 let (operator_id, _) = register_operator(
2645 domain_id,
2646 operator_account,
2647 operator_free_balance,
2648 operator_stake,
2649 minimum_nominator_stake,
2650 pair.public(),
2651 Default::default(),
2652 nominators,
2653 );
2654
2655 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2656
2657 if !operator_reward.is_zero() {
2658 do_reward_operators::<Test>(
2659 domain_id,
2660 OperatorRewardSource::Dummy,
2661 vec![operator_id].into_iter(),
2662 operator_reward,
2663 )
2664 .unwrap();
2665 }
2666
2667 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2668
2669 if let Some(deposit_amount) = maybe_deposit {
2670 Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2671 let res = Domains::nominate_operator(
2672 RuntimeOrigin::signed(nominator_id),
2673 operator_id,
2674 deposit_amount,
2675 );
2676 assert_ok!(res);
2677 }
2678
2679 let operator = Operators::<Test>::get(operator_id).unwrap();
2680 let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2681 if is_storage_fund_increased {
2682 bundle_storage_fund::refund_storage_fee::<Test>(
2683 storage_fund_change_amount as u128 * AI3,
2684 BTreeMap::from_iter([(operator_id, 1)]),
2685 )
2686 .unwrap();
2687 assert_eq!(
2688 operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2689 bundle_storage_fund::total_balance::<Test>(operator_id)
2690 );
2691 total_balance += storage_fund_change_amount as u128 * AI3;
2692 } else {
2693 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2694 operator_id,
2695 storage_fund_change_amount,
2696 )
2697 .unwrap();
2698 assert_eq!(
2699 operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2700 bundle_storage_fund::total_balance::<Test>(operator_id)
2701 );
2702 total_balance -= storage_fund_change_amount as u128 * AI3;
2703 }
2704
2705 for (withdraw, expected_result) in withdraws {
2706 let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2707 let res = Domains::withdraw_stake(
2708 RuntimeOrigin::signed(nominator_id),
2709 operator_id,
2710 withdraw_share_amount,
2711 )
2712 .map(|_| ());
2713 if is_proptest {
2714 prop_assert_eq!(
2715 res,
2716 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2717 "unexpected withdraw_stake result",
2718 );
2719 } else {
2720 assert_eq!(
2721 res,
2722 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2723 "unexpected withdraw_stake result",
2724 );
2725 }
2726 }
2727
2728 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2729
2730 if let Some((withdraw, include_ed)) = expected_withdraw {
2731 let previous_usable_balance = Balances::usable_balance(nominator_id);
2732
2733 HeadDomainNumber::<Test>::set(
2735 domain_id,
2736 head_domain_number
2737 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2738 );
2739 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2740
2741 let expected_balance = if include_ed {
2742 total_balance += ExistentialDeposit::get();
2743 previous_usable_balance + withdraw + ExistentialDeposit::get()
2744 } else {
2745 previous_usable_balance + withdraw
2746 };
2747
2748 if is_proptest {
2749 prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2750 } else {
2751 assert_eq!(
2752 Balances::usable_balance(nominator_id),
2753 expected_balance,
2754 "usable balance is not equal to expected balance:\n{} !=\n{}",
2755 Balances::usable_balance(nominator_id),
2756 expected_balance,
2757 );
2758 }
2759
2760 assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2762 }
2763
2764 if expected_nominator_count_reduced_by > 0 {
2767 if is_proptest {
2768 prop_assert!(
2769 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2770 "deposit exists for nominator: {nominator_id}",
2771 );
2772 prop_assert!(
2773 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2774 "deposit on hold exists for nominator: {nominator_id}",
2775 );
2776 } else {
2777 assert!(
2778 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2779 "deposit exists for nominator: {nominator_id}",
2780 );
2781 assert!(
2782 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2783 "deposit on hold exists for nominator: {nominator_id}",
2784 );
2785 }
2786 }
2787
2788 let operator = Operators::<Test>::get(operator_id).unwrap();
2790 if is_proptest {
2791 prop_assert_approx!(
2792 Balances::usable_balance(nominator_id)
2793 + operator.current_total_stake
2794 + bundle_storage_fund::total_balance::<Test>(operator_id),
2795 total_balance,
2796 "\n{} + {} + {} =",
2797 Balances::usable_balance(nominator_id),
2798 operator.current_total_stake,
2799 bundle_storage_fund::total_balance::<Test>(operator_id),
2800 );
2801 } else {
2802 assert_eq!(
2803 Balances::usable_balance(nominator_id)
2804 + operator.current_total_stake
2805 + bundle_storage_fund::total_balance::<Test>(operator_id),
2806 total_balance,
2807 "initial total balance is not equal to final total balance:\n\
2808 {} + {} + {} =\n{} !=\n{}",
2809 Balances::usable_balance(nominator_id),
2810 operator.current_total_stake,
2811 bundle_storage_fund::total_balance::<Test>(operator_id),
2812 Balances::usable_balance(nominator_id)
2813 + operator.current_total_stake
2814 + bundle_storage_fund::total_balance::<Test>(operator_id),
2815 total_balance,
2816 );
2817 }
2818
2819 Ok(())
2820 })
2821 }
2822
2823 macro_rules! prop_assert_approx {
2829 ($actual:expr, $expected:expr $(,)?) => {
2830 prop_assert_approx!($actual, $expected, "");
2831 };
2832
2833 ($actual:expr, $expected:expr, $fmt:expr) => {
2834 prop_assert_approx!($actual, $expected, $fmt,);
2835 };
2836
2837 ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2838 let actual = $actual;
2839 let expected = $expected;
2840 let extra = format!($fmt, $($args)*);
2841 prop_test::proptest::prop_assert!(
2842 actual <= expected,
2843 "extra minting: actual amount is greater than expected amount:{}{}\
2844 \n{} >\
2845 \n{}",
2846 if extra.is_empty() { "" } else { "\n" },
2847 extra,
2848 actual,
2849 expected,
2850 );
2851 let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2852 .mul_floor(expected)
2853 .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2854 prop_test::proptest::prop_assert!(
2855 actual >= expected_rounded_down,
2856 "excess rounding losses: actual amount is less than expected amount \
2857 rounded down:{}{}\
2858 \n{} <\
2859 \n{} (from\
2860 \n{})",
2861 if extra.is_empty() { "" } else { "\n" },
2862 extra,
2863 actual,
2864 expected_rounded_down,
2865 expected,
2866 );
2867 }};
2868 }
2869
2870 pub(crate) use prop_assert_approx;
2872
2873 pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2877 Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2878
2879 pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2882
2883 pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2891
2892 pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2895
2896 pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2898 <Test as Config>::MinNominatorStake::get() + 1;
2899
2900 pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2902 MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2903
2904 pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2906 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2907
2908 pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2911 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2912
2913 pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2915
2916 pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2918 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2919
2920 #[test]
2926 fn prop_withdraw_excess_operator_stake() {
2927 prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2928 let mut excess_stake =
2929 operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2930
2931 excess_stake = Perquintill::from_parts(1)
2933 .left_from_one()
2934 .mul_ceil(excess_stake);
2935 excess_stake -= 1;
2936
2937 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2938
2939 let expected_withdraw = (excess_stake, false);
2940
2941 withdraw_stake_prop(WithdrawParams {
2942 minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2943 nominators: vec![(0, operator_stake)],
2944 operator_reward: 0,
2945 nominator_id: 0,
2946 withdraws: vec![(excess_stake, Ok(()))],
2947 maybe_deposit: None,
2948 expected_withdraw: Some(expected_withdraw),
2949 expected_nominator_count_reduced_by: 0,
2950 storage_fund_change: (true, 0),
2951 })
2952 });
2953 }
2954
2955 #[test]
2958 fn prop_withdraw_excess_operator_stake_with_nominator() {
2959 prop_test!(
2960 &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2961 |(operator_stake, nominator_stake)| {
2962 prop_assert!(
2964 [operator_stake, nominator_stake]
2965 .into_iter()
2966 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2967 .is_some()
2968 );
2969
2970 let expected_withdraw = (nominator_stake, true);
2971
2972 let excess_stake = expected_withdraw
2973 .0
2974 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2975
2976 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2977
2978 withdraw_stake_prop(WithdrawParams {
2979 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2980 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2981 operator_reward: 0,
2982 nominator_id: 1,
2983 withdraws: vec![(excess_stake, Ok(()))],
2984 maybe_deposit: None,
2985 expected_withdraw: Some(expected_withdraw),
2986 expected_nominator_count_reduced_by: 1,
2987 storage_fund_change: (true, 0),
2988 })
2989 }
2990 );
2991 }
2992
2993 #[test]
2996 fn prop_withdraw_excess_operator_stake_with_deposit() {
2997 prop_test!(
2998 &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2999 |(mut operator_stake, maybe_deposit)| {
3000 operator_stake = operator_stake.saturating_add(maybe_deposit);
3001
3002 prop_assert!(
3003 operator_stake.saturating_sub(maybe_deposit)
3004 >= <Test as Config>::MinOperatorStake::get(),
3005 "would cause MinimumOperatorStake error"
3006 );
3007
3008 prop_assert!(
3010 [operator_stake, maybe_deposit]
3011 .into_iter()
3012 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3013 .is_some()
3014 );
3015
3016 let expected_withdraw = (
3017 STORAGE_FEE_RESERVE
3019 .left_from_one()
3020 .mul_ceil(operator_stake)
3021 .saturating_sub(maybe_deposit),
3022 true,
3023 );
3024
3025 let excess_stake = expected_withdraw
3026 .0
3027 .saturating_sub(<Test as Config>::MinOperatorStake::get());
3028
3029 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3030
3031 let maybe_deposit = if maybe_deposit == 0 {
3033 None
3034 } else {
3035 Some(maybe_deposit)
3036 };
3037
3038 withdraw_stake_prop(WithdrawParams {
3039 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3040 nominators: vec![(0, operator_stake)],
3041 operator_reward: 0,
3042 nominator_id: 0,
3043 withdraws: vec![(excess_stake, Ok(()))],
3044 maybe_deposit,
3045 expected_withdraw: Some(expected_withdraw),
3046 expected_nominator_count_reduced_by: 0,
3047 storage_fund_change: (true, 0),
3048 })
3049 }
3050 );
3051 }
3052
3053 #[test]
3057 fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
3058 prop_test!(
3059 &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
3060 |(mut nominator_stake, maybe_deposit)| {
3061 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3062
3063 prop_assert!(
3064 nominator_stake.saturating_sub(maybe_deposit)
3065 >= <Test as Config>::MinNominatorStake::get(),
3066 "would cause MinimumNominatorStake error"
3067 );
3068
3069 let operator_stake = <Test as Config>::MinOperatorStake::get();
3071
3072 prop_assert!(
3074 [operator_stake, nominator_stake, maybe_deposit]
3075 .into_iter()
3076 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3077 .is_some()
3078 );
3079
3080 let expected_withdraw = (
3081 STORAGE_FEE_RESERVE
3082 .left_from_one()
3083 .mul_ceil(nominator_stake)
3084 .saturating_sub(maybe_deposit),
3085 true,
3086 );
3087
3088 let excess_stake = expected_withdraw
3089 .0
3090 .saturating_sub(<Test as Config>::MinNominatorStake::get());
3091
3092 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3093
3094 let maybe_deposit = if maybe_deposit == 0 {
3096 None
3097 } else {
3098 Some(maybe_deposit)
3099 };
3100
3101 withdraw_stake_prop(WithdrawParams {
3102 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3103 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3104 operator_reward: 0,
3105 nominator_id: 1,
3106 withdraws: vec![(excess_stake, Ok(()))],
3107 maybe_deposit,
3108 expected_withdraw: Some(expected_withdraw),
3109 expected_nominator_count_reduced_by: 0,
3110 storage_fund_change: (true, 0),
3111 })
3112 }
3113 );
3114 }
3115
3116 #[test]
3119 fn prop_withdraw_excess_nominator_stake_with_deposit() {
3120 prop_test!(
3121 &(
3122 PROP_OPERATOR_STAKE_RANGE,
3123 PROP_NOMINATOR_STAKE_RANGE,
3124 PROP_DEPOSIT_RANGE,
3125 ),
3126 |(operator_stake, mut nominator_stake, maybe_deposit)| {
3127 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3128
3129 prop_assert!(
3130 nominator_stake.saturating_sub(maybe_deposit)
3131 >= <Test as Config>::MinNominatorStake::get(),
3132 "would cause MinimumNominatorStake error"
3133 );
3134
3135 prop_assert!(
3137 [operator_stake, nominator_stake, maybe_deposit]
3138 .into_iter()
3139 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3140 .is_some()
3141 );
3142
3143 let expected_withdraw = (
3145 STORAGE_FEE_RESERVE
3146 .left_from_one()
3147 .mul_ceil(nominator_stake)
3148 .saturating_sub(maybe_deposit),
3149 true,
3150 );
3151
3152 let excess_stake = expected_withdraw
3153 .0
3154 .saturating_sub(<Test as Config>::MinNominatorStake::get());
3155
3156 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3157
3158 let maybe_deposit = if maybe_deposit == 0 {
3160 None
3161 } else {
3162 Some(maybe_deposit)
3163 };
3164
3165 withdraw_stake_prop(WithdrawParams {
3166 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3167 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3168 operator_reward: 0,
3169 nominator_id: 1,
3170 withdraws: vec![(excess_stake, Ok(()))],
3171 maybe_deposit,
3172 expected_withdraw: Some(expected_withdraw),
3173 expected_nominator_count_reduced_by: 0,
3174 storage_fund_change: (true, 0),
3175 })
3176 }
3177 );
3178 }
3179
3180 #[test]
3183 fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
3184 prop_test!(
3185 &(
3186 PROP_OPERATOR_STAKE_RANGE,
3187 PROP_NOMINATOR_STAKE_RANGE,
3188 PROP_REWARD_RANGE,
3189 ),
3190 |(operator_stake, nominator_stake, operator_reward)| {
3191 prop_assert!(
3193 [operator_stake, nominator_stake, operator_reward]
3194 .into_iter()
3195 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3196 .is_some()
3197 );
3198
3199 let total_stake = operator_stake.saturating_add(nominator_stake);
3201 let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
3202 .mul_floor(operator_reward);
3203
3204 let excess_stake =
3207 operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
3208
3209 let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
3210
3211 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
3212
3213 withdraw_stake_prop(WithdrawParams {
3214 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3215 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3216 operator_reward,
3217 nominator_id: 0,
3218 withdraws: vec![(excess_stake, Ok(()))],
3219 maybe_deposit: None,
3220 expected_withdraw: Some(expected_withdraw),
3221 expected_nominator_count_reduced_by: 0,
3222 storage_fund_change: (true, 0),
3223 })
3224 }
3225 );
3226 }
3227
3228 #[test]
3231 fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
3232 prop_test!(
3233 &(
3234 PROP_OPERATOR_STAKE_RANGE,
3235 PROP_DEPOSIT_RANGE,
3236 PROP_REWARD_RANGE,
3237 ),
3238 |(mut operator_stake, maybe_deposit, operator_reward)| {
3239 operator_stake = operator_stake.saturating_add(maybe_deposit);
3240
3241 prop_assert!(
3242 operator_stake.saturating_sub(maybe_deposit)
3243 >= <Test as Config>::MinOperatorStake::get(),
3244 "would cause MinimumOperatorStake error"
3245 );
3246
3247 prop_assert!(
3249 [operator_stake, maybe_deposit, operator_reward]
3250 .into_iter()
3251 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3252 .is_some()
3253 );
3254
3255 let reserve = 2 * <Test as Config>::MinOperatorStake::get();
3258 let excess_stake = operator_stake.saturating_sub(reserve);
3259
3260 let expected_withdraw = (
3262 operator_stake
3263 .saturating_add(operator_reward)
3264 .saturating_sub(reserve),
3265 true,
3266 );
3267
3268 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3269
3270 let maybe_deposit = if maybe_deposit == 0 {
3272 None
3273 } else {
3274 Some(maybe_deposit)
3275 };
3276
3277 withdraw_stake_prop(WithdrawParams {
3278 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3279 nominators: vec![(0, operator_stake)],
3280 operator_reward,
3281 nominator_id: 0,
3282 withdraws: vec![(excess_stake, Ok(()))],
3283 maybe_deposit,
3284 expected_withdraw: Some(expected_withdraw),
3285 expected_nominator_count_reduced_by: 0,
3286 storage_fund_change: (true, 0),
3287 })
3288 }
3289 );
3290 }
3291
3292 #[test]
3293 fn withdraw_stake_operator_all() {
3294 withdraw_stake(WithdrawParams {
3295 minimum_nominator_stake: 10 * AI3,
3296 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3297 operator_reward: 20 * AI3,
3298 nominator_id: 0,
3299 withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
3300 maybe_deposit: None,
3301 expected_withdraw: None,
3302 expected_nominator_count_reduced_by: 0,
3303 storage_fund_change: (true, 0),
3304 })
3305 }
3306
3307 #[test]
3308 fn withdraw_stake_operator_below_minimum() {
3309 withdraw_stake(WithdrawParams {
3310 minimum_nominator_stake: 10 * AI3,
3311 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3312 operator_reward: 20 * AI3,
3313 nominator_id: 0,
3314 withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
3315 maybe_deposit: None,
3316 expected_withdraw: None,
3317 expected_nominator_count_reduced_by: 0,
3318 storage_fund_change: (true, 0),
3319 })
3320 }
3321
3322 #[test]
3323 fn withdraw_stake_operator_below_minimum_no_rewards() {
3324 withdraw_stake(WithdrawParams {
3325 minimum_nominator_stake: 10 * AI3,
3326 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3327 operator_reward: Zero::zero(),
3328 nominator_id: 0,
3329 withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
3330 maybe_deposit: None,
3331 expected_withdraw: None,
3332 expected_nominator_count_reduced_by: 0,
3333 storage_fund_change: (true, 0),
3334 })
3335 }
3336
3337 #[test]
3338 fn withdraw_stake_operator_above_minimum() {
3339 withdraw_stake(WithdrawParams {
3340 minimum_nominator_stake: 10 * AI3,
3341 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3342 operator_reward: 20 * AI3,
3343 nominator_id: 0,
3344 withdraws: vec![(58 * AI3, Ok(()))],
3345 maybe_deposit: None,
3348 expected_withdraw: Some((63523809523809523770, false)),
3349 expected_nominator_count_reduced_by: 0,
3350 storage_fund_change: (true, 0),
3351 })
3352 }
3353
3354 #[test]
3355 fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
3356 withdraw_stake(WithdrawParams {
3357 minimum_nominator_stake: 10 * AI3,
3358 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3359 operator_reward: 20 * AI3,
3360 nominator_id: 0,
3361 withdraws: vec![
3362 (58 * AI3, Ok(())),
3363 (5 * AI3, Err(StakingError::MinimumOperatorStake)),
3364 ],
3365 maybe_deposit: None,
3366 expected_withdraw: Some((63523809523809523770, false)),
3367 expected_nominator_count_reduced_by: 0,
3368 storage_fund_change: (true, 0),
3369 })
3370 }
3371
3372 #[test]
3373 fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
3374 withdraw_stake(WithdrawParams {
3375 minimum_nominator_stake: 10 * AI3,
3376 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3377 operator_reward: 20 * AI3,
3378 nominator_id: 0,
3379 withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
3380 maybe_deposit: None,
3381 expected_withdraw: Some((63523809523809523769, false)),
3382 expected_nominator_count_reduced_by: 0,
3383 storage_fund_change: (true, 0),
3384 })
3385 }
3386
3387 #[test]
3388 fn withdraw_stake_operator_above_minimum_no_rewards() {
3389 withdraw_stake(WithdrawParams {
3390 minimum_nominator_stake: 10 * AI3,
3391 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3392 operator_reward: Zero::zero(),
3393 nominator_id: 0,
3394 withdraws: vec![(49 * AI3, Ok(()))],
3395 maybe_deposit: None,
3396 expected_withdraw: Some((48999999999999999980, false)),
3397 expected_nominator_count_reduced_by: 0,
3398 storage_fund_change: (true, 0),
3399 })
3400 }
3401
3402 #[test]
3403 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
3404 withdraw_stake(WithdrawParams {
3405 minimum_nominator_stake: 10 * AI3,
3406 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3407 operator_reward: Zero::zero(),
3408 nominator_id: 0,
3409 withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
3410 maybe_deposit: None,
3411 expected_withdraw: Some((48999999999999999981, false)),
3412 expected_nominator_count_reduced_by: 0,
3413 storage_fund_change: (true, 0),
3414 })
3415 }
3416
3417 #[test]
3418 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
3419 withdraw_stake(WithdrawParams {
3420 minimum_nominator_stake: 10 * AI3,
3421 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3422 operator_reward: Zero::zero(),
3423 nominator_id: 0,
3424 withdraws: vec![
3425 (29 * AI3, Ok(())),
3426 (20 * AI3, Ok(())),
3427 (20 * AI3, Err(StakingError::MinimumOperatorStake)),
3428 ],
3429 maybe_deposit: None,
3430 expected_withdraw: Some((48999999999999999981, false)),
3431 expected_nominator_count_reduced_by: 0,
3432 storage_fund_change: (true, 0),
3433 })
3434 }
3435
3436 #[test]
3437 fn withdraw_stake_nominator_below_minimum_with_rewards() {
3438 withdraw_stake(WithdrawParams {
3439 minimum_nominator_stake: 10 * AI3,
3440 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3441 operator_reward: 20 * AI3,
3442 nominator_id: 1,
3443 withdraws: vec![(45 * AI3, Ok(()))],
3444 maybe_deposit: None,
3448 expected_withdraw: Some((54761904761904761888, true)),
3449 expected_nominator_count_reduced_by: 1,
3450 storage_fund_change: (true, 0),
3451 })
3452 }
3453
3454 #[test]
3455 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
3456 withdraw_stake(WithdrawParams {
3457 minimum_nominator_stake: 10 * AI3,
3458 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3459 operator_reward: 20 * AI3,
3460 nominator_id: 1,
3461 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3462 maybe_deposit: None,
3466 expected_withdraw: Some((54761904761904761888, true)),
3467 expected_nominator_count_reduced_by: 1,
3468 storage_fund_change: (true, 0),
3469 })
3470 }
3471
3472 #[test]
3473 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
3474 withdraw_stake(WithdrawParams {
3475 minimum_nominator_stake: 10 * AI3,
3476 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3477 operator_reward: 20 * AI3,
3478 nominator_id: 1,
3479 withdraws: vec![
3480 (25 * AI3, Ok(())),
3481 (20 * AI3, Ok(())),
3482 (20 * AI3, Err(StakingError::InsufficientShares)),
3483 ],
3484 maybe_deposit: None,
3488 expected_withdraw: Some((54761904761904761888, true)),
3489 expected_nominator_count_reduced_by: 1,
3490 storage_fund_change: (true, 0),
3491 })
3492 }
3493
3494 #[test]
3495 fn withdraw_stake_nominator_below_minimum_no_reward() {
3496 withdraw_stake(WithdrawParams {
3497 minimum_nominator_stake: 10 * AI3,
3498 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3499 operator_reward: Zero::zero(),
3500 nominator_id: 1,
3501 withdraws: vec![(45 * AI3, Ok(()))],
3502 maybe_deposit: None,
3503 expected_withdraw: Some((50 * AI3, true)),
3504 expected_nominator_count_reduced_by: 1,
3505 storage_fund_change: (true, 0),
3506 })
3507 }
3508
3509 #[test]
3510 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
3511 withdraw_stake(WithdrawParams {
3512 minimum_nominator_stake: 10 * AI3,
3513 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3514 operator_reward: Zero::zero(),
3515 nominator_id: 1,
3516 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3517 maybe_deposit: None,
3518 expected_withdraw: Some((50 * AI3, true)),
3519 expected_nominator_count_reduced_by: 1,
3520 storage_fund_change: (true, 0),
3521 })
3522 }
3523
3524 #[test]
3525 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
3526 withdraw_stake(WithdrawParams {
3527 minimum_nominator_stake: 10 * AI3,
3528 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3529 operator_reward: Zero::zero(),
3530 nominator_id: 1,
3531 withdraws: vec![
3532 (25 * AI3, Ok(())),
3533 (20 * AI3, Ok(())),
3534 (20 * AI3, Err(StakingError::InsufficientShares)),
3535 ],
3536 maybe_deposit: None,
3537 expected_withdraw: Some((50 * AI3, true)),
3538 expected_nominator_count_reduced_by: 1,
3539 storage_fund_change: (true, 0),
3540 })
3541 }
3542
3543 #[test]
3544 fn withdraw_stake_nominator_above_minimum() {
3545 withdraw_stake(WithdrawParams {
3546 minimum_nominator_stake: 10 * AI3,
3547 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3548 operator_reward: 20 * AI3,
3549 nominator_id: 1,
3550 withdraws: vec![(40 * AI3, Ok(()))],
3551 maybe_deposit: None,
3552 expected_withdraw: Some((43809523809523809511, false)),
3553 expected_nominator_count_reduced_by: 0,
3554 storage_fund_change: (true, 0),
3555 })
3556 }
3557
3558 #[test]
3559 fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
3560 withdraw_stake(WithdrawParams {
3561 minimum_nominator_stake: 10 * AI3,
3562 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3563 operator_reward: 20 * AI3,
3564 nominator_id: 1,
3565 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3566 maybe_deposit: None,
3567 expected_withdraw: Some((43809523809523809510, false)),
3568 expected_nominator_count_reduced_by: 0,
3569 storage_fund_change: (true, 0),
3570 })
3571 }
3572
3573 #[test]
3574 fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3575 withdraw_stake(WithdrawParams {
3576 minimum_nominator_stake: 10 * AI3,
3577 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3578 operator_reward: 20 * AI3,
3579 nominator_id: 1,
3580 withdraws: vec![
3581 (35 * AI3, Ok(())),
3582 (5 * AI3, Ok(())),
3583 (15 * AI3, Err(StakingError::InsufficientShares)),
3584 ],
3585 maybe_deposit: None,
3586 expected_withdraw: Some((43809523809523809510, false)),
3587 expected_nominator_count_reduced_by: 0,
3588 storage_fund_change: (true, 0),
3589 })
3590 }
3591
3592 #[test]
3593 fn withdraw_stake_nominator_above_minimum_no_rewards() {
3594 withdraw_stake(WithdrawParams {
3595 minimum_nominator_stake: 10 * AI3,
3596 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3597 operator_reward: Zero::zero(),
3598 nominator_id: 1,
3599 withdraws: vec![(39 * AI3, Ok(()))],
3600 maybe_deposit: None,
3601 expected_withdraw: Some((39 * AI3, false)),
3602 expected_nominator_count_reduced_by: 0,
3603 storage_fund_change: (true, 0),
3604 })
3605 }
3606
3607 #[test]
3608 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3609 withdraw_stake(WithdrawParams {
3610 minimum_nominator_stake: 10 * AI3,
3611 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3612 operator_reward: Zero::zero(),
3613 nominator_id: 1,
3614 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3615 maybe_deposit: None,
3616 expected_withdraw: Some((39999999899999999998, false)),
3617 expected_nominator_count_reduced_by: 0,
3618 storage_fund_change: (true, 0),
3619 })
3620 }
3621
3622 #[test]
3623 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3624 withdraw_stake(WithdrawParams {
3625 minimum_nominator_stake: 10 * AI3,
3626 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3627 operator_reward: Zero::zero(),
3628 nominator_id: 1,
3629 withdraws: vec![
3630 (35 * AI3, Ok(())),
3631 (5 * AI3 - 100000000000, Ok(())),
3632 (15 * AI3, Err(StakingError::InsufficientShares)),
3633 ],
3634 maybe_deposit: None,
3635 expected_withdraw: Some((39999999899999999998, false)),
3636 expected_nominator_count_reduced_by: 0,
3637 storage_fund_change: (true, 0),
3638 })
3639 }
3640
3641 #[test]
3642 fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3643 withdraw_stake(WithdrawParams {
3644 minimum_nominator_stake: 10 * AI3,
3645 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3646 operator_reward: Zero::zero(),
3647 nominator_id: 1,
3648 withdraws: vec![
3649 (35 * AI3, Ok(())),
3650 (5 * AI3 - 100000000000, Ok(())),
3651 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3652 ],
3653 maybe_deposit: Some(2 * AI3),
3654 expected_withdraw: Some((39999999899999999998, false)),
3655 expected_nominator_count_reduced_by: 0,
3656 storage_fund_change: (true, 0),
3657 })
3658 }
3659
3660 #[test]
3661 fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3662 withdraw_stake(WithdrawParams {
3663 minimum_nominator_stake: 10 * AI3,
3664 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3665 operator_reward: 20 * AI3,
3666 nominator_id: 1,
3667 withdraws: vec![
3668 (35 * AI3, Ok(())),
3669 (5 * AI3, Ok(())),
3670 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3671 ],
3672 maybe_deposit: Some(2 * AI3),
3676 expected_withdraw: Some((43809523809523809510, false)),
3677 expected_nominator_count_reduced_by: 0,
3678 storage_fund_change: (true, 0),
3679 })
3680 }
3681
3682 #[test]
3683 fn withdraw_stake_nominator_zero_amount() {
3684 withdraw_stake(WithdrawParams {
3685 minimum_nominator_stake: 10 * AI3,
3686 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3687 operator_reward: Zero::zero(),
3688 nominator_id: 1,
3689 withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3690 maybe_deposit: None,
3691 expected_withdraw: None,
3692 expected_nominator_count_reduced_by: 0,
3693 storage_fund_change: (true, 0),
3694 })
3695 }
3696
3697 #[test]
3698 fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3699 withdraw_stake(WithdrawParams {
3700 minimum_nominator_stake: 10 * AI3,
3701 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3702 operator_reward: Zero::zero(),
3703 nominator_id: 1,
3704 withdraws: vec![(50 * AI3, Ok(()))],
3705 maybe_deposit: None,
3706 storage_fund_change: (true, 21),
3709 expected_withdraw: Some((54999999999999999985, true)),
3710 expected_nominator_count_reduced_by: 1,
3711 })
3712 }
3713
3714 #[test]
3715 fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3716 withdraw_stake(WithdrawParams {
3717 minimum_nominator_stake: 10 * AI3,
3718 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3719 operator_reward: Zero::zero(),
3720 nominator_id: 1,
3721 withdraws: vec![(50 * AI3, Ok(()))],
3722 maybe_deposit: None,
3723 storage_fund_change: (false, 21),
3726 expected_withdraw: Some((44999999999999999995, true)),
3727 expected_nominator_count_reduced_by: 1,
3728 })
3729 }
3730
3731 #[test]
3732 fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3733 withdraw_stake(WithdrawParams {
3734 minimum_nominator_stake: 10 * AI3,
3735 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3736 operator_reward: Zero::zero(),
3737 nominator_id: 1,
3738 withdraws: vec![(50 * AI3, Ok(()))],
3739 maybe_deposit: None,
3740 storage_fund_change: (false, 42),
3743 expected_withdraw: Some((40 * AI3, true)),
3744 expected_nominator_count_reduced_by: 1,
3745 })
3746 }
3747
3748 #[test]
3749 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3750 withdraw_stake(WithdrawParams {
3751 minimum_nominator_stake: 10 * AI3,
3752 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3753 operator_reward: Zero::zero(),
3754 nominator_id: 1,
3755 withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3756 maybe_deposit: None,
3757 storage_fund_change: (true, 21),
3761 expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3762 expected_nominator_count_reduced_by: 0,
3763 })
3764 }
3765
3766 #[test]
3767 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3768 withdraw_stake(WithdrawParams {
3769 minimum_nominator_stake: 10 * AI3,
3770 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3771 operator_reward: Zero::zero(),
3772 nominator_id: 1,
3773 withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3774 maybe_deposit: None,
3775 storage_fund_change: (false, 21),
3779 expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3780 expected_nominator_count_reduced_by: 0,
3781 })
3782 }
3783
3784 #[test]
3785 fn unlock_multiple_withdrawals() {
3786 let domain_id = DomainId::new(0);
3787 let operator_account = 1;
3788 let operator_free_balance = 250 * AI3;
3789 let operator_stake = 200 * AI3;
3790 let pair = OperatorPair::from_seed(&[0; 32]);
3791 let nominator_account = 2;
3792 let nominator_free_balance = 150 * AI3;
3793 let nominator_stake = 100 * AI3;
3794
3795 let nominators = vec![
3796 (operator_account, (operator_free_balance, operator_stake)),
3797 (nominator_account, (nominator_free_balance, nominator_stake)),
3798 ];
3799
3800 let total_deposit = 300 * AI3;
3801 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3802 let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3803
3804 let mut ext = new_test_ext();
3805 ext.execute_with(|| {
3806 let (operator_id, _) = register_operator(
3807 domain_id,
3808 operator_account,
3809 operator_free_balance,
3810 operator_stake,
3811 10 * AI3,
3812 pair.public(),
3813 Default::default(),
3814 BTreeMap::from_iter(nominators),
3815 );
3816
3817 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3818 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3819 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3820
3821 let operator = Operators::<Test>::get(operator_id).unwrap();
3822 assert_eq!(operator.current_total_stake, init_total_stake);
3823 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3824 assert_eq!(
3825 operator.total_storage_fee_deposit,
3826 bundle_storage_fund::total_balance::<Test>(operator_id)
3827 );
3828
3829 let shares_per_withdraw = init_total_stake / 100;
3831 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3832
3833 for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3835 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3836 .unwrap();
3837 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3838 }
3839 HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3841
3842 for _ in 0..5 {
3845 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3846 .unwrap();
3847 }
3848 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3849
3850 assert_err!(
3852 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3853 StakingError::TooManyWithdrawals
3854 );
3855 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3856 Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3857 let withdrawal = maybe_withdrawal.as_mut().unwrap();
3858 do_convert_previous_epoch_withdrawal::<Test>(
3859 operator_id,
3860 withdrawal,
3861 domain_stake_summary.current_epoch_index,
3862 )
3863 .unwrap();
3864 assert_eq!(
3865 withdrawal.withdrawals.len() as u32,
3866 <Test as crate::Config>::WithdrawalLimit::get()
3867 );
3868 Ok::<(), StakingError>(())
3869 })
3870 .unwrap();
3871
3872 HeadDomainNumber::<Test>::set(
3874 domain_id,
3875 head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3876 );
3877 let total_balance = Balances::usable_balance(nominator_account);
3878 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3879 assert_eq!(
3880 Balances::usable_balance(nominator_account) + 74, total_balance
3882 + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3883 / 100
3884 );
3885 let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3886 assert_eq!(withdrawal.withdrawals.len(), 1);
3887
3888 HeadDomainNumber::<Test>::set(
3890 domain_id,
3891 head_domain_number
3892 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3893 + 1,
3894 );
3895 let total_balance = Balances::usable_balance(nominator_account);
3896 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3897 assert_eq!(
3898 Balances::usable_balance(nominator_account) - 2, total_balance + 5 * total_deposit / 100
3900 );
3901 assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3902 });
3903 }
3904
3905 #[test]
3906 fn slash_operator() {
3907 let domain_id = DomainId::new(0);
3908 let operator_account = 1;
3909 let operator_free_balance = 250 * AI3;
3910 let operator_stake = 200 * AI3;
3911 let operator_extra_deposit = 40 * AI3;
3912 let pair = OperatorPair::from_seed(&[0; 32]);
3913 let nominator_account = 2;
3914 let nominator_free_balance = 150 * AI3;
3915 let nominator_stake = 100 * AI3;
3916 let nominator_extra_deposit = 40 * AI3;
3917
3918 let nominators = vec![
3919 (operator_account, (operator_free_balance, operator_stake)),
3920 (nominator_account, (nominator_free_balance, nominator_stake)),
3921 ];
3922
3923 let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3924
3925 let deposits = vec![
3926 (operator_account, operator_extra_deposit),
3927 (nominator_account, nominator_extra_deposit),
3928 ];
3929
3930 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3931 let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3932
3933 let mut ext = new_test_ext();
3934 ext.execute_with(|| {
3935 let (operator_id, _) = register_operator(
3936 domain_id,
3937 operator_account,
3938 operator_free_balance,
3939 operator_stake,
3940 10 * AI3,
3941 pair.public(),
3942 Default::default(),
3943 BTreeMap::from_iter(nominators),
3944 );
3945
3946 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3947 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3948 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3949
3950 let operator = Operators::<Test>::get(operator_id).unwrap();
3951 assert_eq!(operator.current_total_stake, init_total_stake);
3952 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3953 assert_eq!(
3954 operator.total_storage_fee_deposit,
3955 bundle_storage_fund::total_balance::<Test>(operator_id)
3956 );
3957
3958 for unlock in &unlocking {
3959 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3960 }
3961
3962 do_reward_operators::<Test>(
3963 domain_id,
3964 OperatorRewardSource::Dummy,
3965 vec![operator_id].into_iter(),
3966 20 * AI3,
3967 )
3968 .unwrap();
3969 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3970
3971 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3973 for id in [operator_account, nominator_account] {
3974 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3975 do_convert_previous_epoch_withdrawal::<Test>(
3976 operator_id,
3977 maybe_withdrawal.as_mut().unwrap(),
3978 domain_stake_summary.current_epoch_index,
3979 )
3980 })
3981 .unwrap();
3982 }
3983
3984 let operator = Operators::<Test>::get(operator_id).unwrap();
3987 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3988 let operator_withdrawal =
3989 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3990 let nominator_withdrawal =
3991 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3992
3993 let total_deposit =
3994 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3995 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3996 + nominator_withdrawal.total_withdrawal_amount;
3997 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3998 .storage_fee_refund
3999 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
4000 assert_eq!(293333333333333333336, total_deposit,);
4001 assert_eq!(21666666666666666664, total_stake_withdrawal);
4002 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
4003 assert_eq!(
4004 320 * AI3,
4005 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
4006 );
4007 assert_eq!(
4008 operator.total_storage_fee_deposit,
4009 bundle_storage_fund::total_balance::<Test>(operator_id)
4010 );
4011
4012 for deposit in deposits {
4013 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
4014 }
4015
4016 do_mark_operators_as_slashed::<Test>(
4017 vec![operator_id],
4018 SlashedReason::InvalidBundle(1),
4019 )
4020 .unwrap();
4021
4022 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4023 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4024
4025 let operator = Operators::<Test>::get(operator_id).unwrap();
4026 assert_eq!(
4027 *operator.status::<Test>(operator_id),
4028 OperatorStatus::Slashed
4029 );
4030
4031 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4032 assert!(pending_slashes.contains(&operator_id));
4033
4034 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0);
4035
4036 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4037 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4038 assert_eq!(Operators::<Test>::get(operator_id), None);
4039 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4040
4041 assert_eq!(
4042 Balances::total_balance(&operator_account),
4043 operator_free_balance - operator_stake
4044 );
4045 assert_eq!(
4046 Balances::total_balance(&nominator_account),
4047 nominator_free_balance - nominator_stake
4048 );
4049
4050 assert!(Balances::total_balance(&TreasuryAccount::get()) >= 320 * AI3);
4051 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4052 });
4053 }
4054
4055 #[test]
4056 fn slash_operator_with_more_than_max_nominators_to_slash() {
4057 let domain_id = DomainId::new(0);
4058 let operator_account = 1;
4059 let operator_free_balance = 250 * AI3;
4060 let operator_stake = 200 * AI3;
4061 let operator_extra_deposit = 40 * AI3;
4062 let operator_extra_withdraw = 5 * AI3;
4063 let pair = OperatorPair::from_seed(&[0; 32]);
4064
4065 let nominator_accounts: Vec<AccountId> = (2..22).collect();
4066 let nominator_free_balance = 150 * AI3;
4067 let nominator_stake = 100 * AI3;
4068 let nominator_extra_deposit = 40 * AI3;
4069 let nominator_extra_withdraw = 5 * AI3;
4070
4071 let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
4072 for nominator_account in nominator_accounts.clone() {
4073 nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
4074 }
4075
4076 let last_nominator_account = nominator_accounts.last().cloned().unwrap();
4077 let unlocking = vec![
4078 (operator_account, 10 * AI3),
4079 (last_nominator_account, 10 * AI3),
4080 ];
4081
4082 let deposits = vec![
4083 (operator_account, operator_extra_deposit),
4084 (last_nominator_account, nominator_extra_deposit),
4085 ];
4086 let withdrawals = vec![
4087 (operator_account, operator_extra_withdraw),
4088 (last_nominator_account, nominator_extra_withdraw),
4089 ];
4090
4091 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
4092 * (200 + (100 * nominator_accounts.len() as u128))
4093 * AI3;
4094 let init_total_storage_fund =
4095 STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
4096
4097 let mut ext = new_test_ext();
4098 ext.execute_with(|| {
4099 let (operator_id, _) = register_operator(
4100 domain_id,
4101 operator_account,
4102 operator_free_balance,
4103 operator_stake,
4104 10 * AI3,
4105 pair.public(),
4106 Default::default(),
4107 BTreeMap::from_iter(nominators),
4108 );
4109
4110 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4111 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4112 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4113
4114 let operator = Operators::<Test>::get(operator_id).unwrap();
4115 assert_eq!(operator.current_total_stake, init_total_stake);
4116 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
4117 assert_eq!(
4118 operator.total_storage_fee_deposit,
4119 bundle_storage_fund::total_balance::<Test>(operator_id)
4120 );
4121
4122 for unlock in &unlocking {
4123 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
4124 }
4125
4126 do_reward_operators::<Test>(
4127 domain_id,
4128 OperatorRewardSource::Dummy,
4129 vec![operator_id].into_iter(),
4130 20 * AI3,
4131 )
4132 .unwrap();
4133 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4134
4135 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4137 for id in [operator_account, last_nominator_account] {
4138 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
4139 do_convert_previous_epoch_withdrawal::<Test>(
4140 operator_id,
4141 maybe_withdrawal.as_mut().unwrap(),
4142 domain_stake_summary.current_epoch_index,
4143 )
4144 })
4145 .unwrap();
4146 }
4147
4148 let operator = Operators::<Test>::get(operator_id).unwrap();
4151 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4152 let operator_withdrawal =
4153 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
4154 let nominator_withdrawal =
4155 Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
4156
4157 let total_deposit =
4158 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
4159 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
4160 + nominator_withdrawal.total_withdrawal_amount;
4161 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
4162 .storage_fee_refund
4163 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
4164 assert_eq!(2194772727272727272734, total_deposit,);
4165 assert_eq!(20227272727272727266, total_stake_withdrawal);
4166 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
4167 assert_eq!(
4168 2220 * AI3,
4169 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
4170 );
4171
4172 assert_eq!(
4173 operator.total_storage_fee_deposit,
4174 bundle_storage_fund::total_balance::<Test>(operator_id)
4175 );
4176
4177 for deposit in deposits {
4178 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
4179 }
4180 for withdrawal in withdrawals {
4181 do_withdraw_stake::<Test>(
4182 operator_id,
4183 withdrawal.0,
4184 withdrawal.1,
4187 )
4188 .unwrap();
4189 }
4190
4191 do_mark_operators_as_slashed::<Test>(
4192 vec![operator_id],
4193 SlashedReason::InvalidBundle(1),
4194 )
4195 .unwrap();
4196
4197 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4198
4199 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4200 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4201
4202 let operator = Operators::<Test>::get(operator_id).unwrap();
4203 assert_eq!(
4204 *operator.status::<Test>(operator_id),
4205 OperatorStatus::Slashed
4206 );
4207
4208 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4209 assert!(pending_slashes.contains(&operator_id));
4210
4211 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0);
4212
4213 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4216 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4217 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4218
4219 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4220 assert_eq!(Operators::<Test>::get(operator_id), None);
4221 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4222
4223 assert_eq!(
4224 Balances::total_balance(&operator_account),
4225 operator_free_balance - operator_stake
4226 );
4227 for nominator_account in nominator_accounts {
4228 assert_eq!(
4229 Balances::total_balance(&nominator_account),
4230 nominator_free_balance - nominator_stake
4231 );
4232 }
4233
4234 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 2220 * AI3);
4235 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4236 });
4237 }
4238
4239 #[test]
4240 fn slash_operators() {
4241 let domain_id = DomainId::new(0);
4242 let operator_free_balance = 250 * AI3;
4243 let operator_stake = 200 * AI3;
4244
4245 let operator_account_1 = 1;
4246 let operator_account_2 = 2;
4247 let operator_account_3 = 3;
4248
4249 let pair_1 = OperatorPair::from_seed(&[0; 32]);
4250 let pair_2 = OperatorPair::from_seed(&[1; 32]);
4251 let pair_3 = OperatorPair::from_seed(&[2; 32]);
4252
4253 let mut ext = new_test_ext();
4254 ext.execute_with(|| {
4255 let (operator_id_1, _) = register_operator(
4256 domain_id,
4257 operator_account_1,
4258 operator_free_balance,
4259 operator_stake,
4260 10 * AI3,
4261 pair_1.public(),
4262 Default::default(),
4263 Default::default(),
4264 );
4265
4266 let (operator_id_2, _) = register_operator(
4267 domain_id,
4268 operator_account_2,
4269 operator_free_balance,
4270 operator_stake,
4271 10 * AI3,
4272 pair_2.public(),
4273 Default::default(),
4274 Default::default(),
4275 );
4276
4277 let (operator_id_3, _) = register_operator(
4278 domain_id,
4279 operator_account_3,
4280 operator_free_balance,
4281 operator_stake,
4282 10 * AI3,
4283 pair_3.public(),
4284 Default::default(),
4285 Default::default(),
4286 );
4287
4288 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4289 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4290 assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
4291 assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
4292 assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
4293 assert_eq!(
4294 domain_stake_summary.current_total_stake,
4295 STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
4296 );
4297 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4298 let operator = Operators::<Test>::get(operator_id).unwrap();
4299 assert_eq!(
4300 operator.total_storage_fee_deposit,
4301 STORAGE_FEE_RESERVE * operator_stake
4302 );
4303 assert_eq!(
4304 operator.total_storage_fee_deposit,
4305 bundle_storage_fund::total_balance::<Test>(operator_id)
4306 );
4307 }
4308
4309 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id_3);
4311 assert_ok!(res);
4312 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4313
4314 let operator = Operators::<Test>::get(operator_id_3).unwrap();
4315 assert_eq!(
4316 *operator.status::<Test>(operator_id_3),
4317 OperatorStatus::Deactivated(7)
4318 );
4319
4320 do_mark_operators_as_slashed::<Test>(
4321 vec![operator_id_1],
4322 SlashedReason::InvalidBundle(1),
4323 )
4324 .unwrap();
4325 do_mark_operators_as_slashed::<Test>(
4326 vec![operator_id_2],
4327 SlashedReason::InvalidBundle(2),
4328 )
4329 .unwrap();
4330 do_mark_operators_as_slashed::<Test>(
4331 vec![operator_id_3],
4332 SlashedReason::InvalidBundle(3),
4333 )
4334 .unwrap();
4335
4336 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4337 assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
4338 assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
4339 assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
4340
4341 let operator = Operators::<Test>::get(operator_id_1).unwrap();
4342 assert_eq!(
4343 *operator.status::<Test>(operator_id_1),
4344 OperatorStatus::Slashed
4345 );
4346
4347 let operator = Operators::<Test>::get(operator_id_2).unwrap();
4348 assert_eq!(
4349 *operator.status::<Test>(operator_id_2),
4350 OperatorStatus::Slashed
4351 );
4352
4353 let operator = Operators::<Test>::get(operator_id_3).unwrap();
4354 assert_eq!(
4355 *operator.status::<Test>(operator_id_3),
4356 OperatorStatus::Slashed
4357 );
4358
4359 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0);
4360
4361 let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
4362 slashed_operators.into_iter().for_each(|_| {
4363 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4364 });
4365
4366 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4367 assert_eq!(Operators::<Test>::get(operator_id_1), None);
4368 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
4369 assert_eq!(Operators::<Test>::get(operator_id_2), None);
4370 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
4371 assert_eq!(Operators::<Test>::get(operator_id_3), None);
4372 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
4373
4374 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 600 * AI3);
4375 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4376 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4377 }
4378 });
4379 }
4380
4381 #[test]
4382 fn bundle_storage_fund_charged_and_refund_storage_fee() {
4383 let domain_id = DomainId::new(0);
4384 let operator_account = 1;
4385 let operator_free_balance = 150 * AI3;
4386 let operator_total_stake = 100 * AI3;
4387 let operator_stake = 80 * AI3;
4388 let operator_storage_fee_deposit = 20 * AI3;
4389 let pair = OperatorPair::from_seed(&[0; 32]);
4390 let nominator_account = 2;
4391
4392 let mut ext = new_test_ext();
4393 ext.execute_with(|| {
4394 let (operator_id, _) = register_operator(
4395 domain_id,
4396 operator_account,
4397 operator_free_balance,
4398 operator_total_stake,
4399 AI3,
4400 pair.public(),
4401 Default::default(),
4402 BTreeMap::default(),
4403 );
4404
4405 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4406 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
4407
4408 let operator = Operators::<Test>::get(operator_id).unwrap();
4409 assert_eq!(operator.current_total_stake, operator_stake);
4410 assert_eq!(operator.current_total_shares, operator_stake);
4411 assert_eq!(
4412 operator.total_storage_fee_deposit,
4413 operator_storage_fee_deposit
4414 );
4415
4416 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
4418 operator_id,
4419 (operator_storage_fee_deposit / AI3) as u32,
4421 )
4422 .unwrap();
4423 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4424 assert_err!(
4425 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
4426 bundle_storage_fund::Error::BundleStorageFeePayment
4427 );
4428
4429 do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
4431 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4432
4433 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4434 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4435
4436 Balances::set_balance(&nominator_account, 100 * AI3);
4438 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4439 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4440
4441 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4442 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4443
4444 bundle_storage_fund::refund_storage_fee::<Test>(
4446 10 * AI3,
4447 BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
4448 )
4449 .unwrap();
4450 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4451
4452 assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 9 * AI3);
4454
4455 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4456 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4457 });
4458 }
4459
4460 #[test]
4461 fn zero_amount_deposit_and_withdraw() {
4462 let domain_id = DomainId::new(0);
4463 let operator_account = 1;
4464 let operator_free_balance = 250 * AI3;
4465 let operator_stake = 200 * AI3;
4466 let pair = OperatorPair::from_seed(&[0; 32]);
4467 let nominator_account = 2;
4468 let nominator_free_balance = 150 * AI3;
4469 let nominator_stake = 100 * AI3;
4470
4471 let nominators = vec![
4472 (operator_account, (operator_free_balance, operator_stake)),
4473 (nominator_account, (nominator_free_balance, nominator_stake)),
4474 ];
4475
4476 let total_deposit = 300 * AI3;
4477 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4478
4479 let mut ext = new_test_ext();
4480 ext.execute_with(|| {
4481 let (operator_id, _) = register_operator(
4482 domain_id,
4483 operator_account,
4484 operator_free_balance,
4485 operator_stake,
4486 10 * AI3,
4487 pair.public(),
4488 Default::default(),
4489 BTreeMap::from_iter(nominators),
4490 );
4491
4492 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4493 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4494 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4495
4496 assert_err!(
4498 do_nominate_operator::<Test>(operator_id, nominator_account, 0),
4499 StakingError::ZeroDeposit
4500 );
4501
4502 assert_err!(
4504 do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
4505 StakingError::ZeroWithdraw
4506 );
4507
4508 do_withdraw_stake::<Test>(
4510 operator_id,
4511 nominator_account,
4512 STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
4514 )
4515 .unwrap();
4516 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4517 });
4518 }
4519
4520 #[test]
4521 fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
4522 let domain_id = DomainId::new(0);
4523 let operator_account = 1;
4524 let operator_free_balance = 250 * AI3;
4525 let operator_stake = 200 * AI3;
4526 let pair = OperatorPair::from_seed(&[0; 32]);
4527 let nominator_account = 2;
4528 let nominator_free_balance = 150 * AI3;
4529 let nominator_stake = 100 * AI3;
4530
4531 let nominators = vec![
4532 (operator_account, (operator_free_balance, operator_stake)),
4533 (nominator_account, (nominator_free_balance, nominator_stake)),
4534 ];
4535
4536 let total_deposit = 300 * AI3;
4537 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4538
4539 let mut ext = new_test_ext();
4540 ext.execute_with(|| {
4541 let (operator_id, _) = register_operator(
4542 domain_id,
4543 operator_account,
4544 operator_free_balance,
4545 operator_stake,
4546 10 * AI3,
4547 pair.public(),
4548 Default::default(),
4549 BTreeMap::from_iter(nominators),
4550 );
4551
4552 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4553 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4554 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4555
4556 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4557 do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4559
4560 let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4562 OperatorEpochSharePrice::<Test>::remove(
4564 operator_id,
4565 DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4566 );
4567
4568 assert_err!(
4570 do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4571 StakingError::MissingOperatorEpochSharePrice
4572 );
4573 assert_err!(
4574 do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4575 StakingError::MissingOperatorEpochSharePrice
4576 );
4577 });
4578 }
4579
4580 #[test]
4581 fn test_share_price_deposit() {
4582 let total_shares = 45 * AI3;
4583 let total_stake = 45 * AI3 + 37;
4584 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4585
4586 let to_deposit_stakes = [
4588 5,
4589 7,
4590 9,
4591 11,
4592 17,
4593 23,
4594 934,
4595 24931,
4596 349083467,
4597 2 * AI3 + 32,
4598 52 * AI3 - 4729034,
4599 2732 * AI3 - 1720,
4600 1117 * AI3 + 1839832,
4601 31232 * AI3 - 987654321,
4602 ];
4603
4604 let mut deposited_share = 0;
4605 let mut deposited_stake = 0;
4606 for to_deposit_stake in to_deposit_stakes {
4607 let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4608
4609 deposited_stake += to_deposit_stake;
4611 deposited_share += to_deposit_share;
4614
4615 let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4620
4621 assert!(total_deposited_share >= deposited_share);
4626
4627 assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4630 }
4631 }
4632
4633 #[test]
4634 fn test_share_price_withdraw() {
4635 let total_shares = 123 * AI3;
4636 let total_stake = 123 * AI3 + 13;
4637 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4638
4639 let to_withdraw_shares = [
4641 1,
4642 3,
4643 7,
4644 13,
4645 17,
4646 123,
4647 43553,
4648 546393039,
4649 15 * AI3 + 1342,
4650 2 * AI3 - 423,
4651 31 * AI3 - 1321,
4652 42 * AI3 + 4564234,
4653 7 * AI3 - 987654321,
4654 3 * AI3 + 987654321123879,
4655 ];
4656
4657 let mut withdrawn_share = 0;
4658 let mut withdrawn_stake = 0;
4659 for to_withdraw_share in to_withdraw_shares {
4660 let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4661
4662 withdrawn_share += to_withdraw_share;
4664 withdrawn_stake += to_withdraw_stake;
4667
4668 let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4673
4674 assert!(total_withdrawn_stake >= withdrawn_stake);
4678
4679 assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4682 }
4683 }
4684
4685 #[test]
4686 fn test_share_price_unlock() {
4687 let mut total_shares = 20 * AI3;
4688 let mut total_stake = 20 * AI3 + 12;
4689
4690 for to_unlock_share in [
4696 AI3 + 123,
4697 2 * AI3 - 456,
4698 3 * AI3 - 789,
4699 4 * AI3 - 123 + 456,
4700 7 * AI3 + 789 - 987654321,
4701 3 * AI3 + 987654321,
4702 ] {
4703 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4704
4705 let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4706
4707 total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4708 total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4709 }
4710 assert_eq!(total_shares, 0);
4711 }
4712}