pallet_domains/
staking.rs

1//! Staking for domains
2
3#[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/// A nominators deposit.
35#[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/// A share price is parts per billion of shares/ai3.
42/// Note: Shares must always be equal to or lower than ai3, and both shares and ai3 can't be zero.
43#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
44pub struct SharePrice(pub Perquintill);
45
46impl SharePrice {
47    /// Creates a new instance of share price from shares and stake.
48    /// Returns an error if there are more shares than stake or either value is zero.
49    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            // Invalid share price, can't be greater than one.
55            Err(Error::ShareOverflow)
56        } else if total_stake.is_zero() || total_shares.is_zero() {
57            // If there are no shares or no stake, we can't construct a zero share price.
58            Err(Error::ZeroSharePrice)
59        } else {
60            Ok(SharePrice(Perquintill::from_rational(
61                total_shares.into(),
62                total_stake,
63            )))
64        }
65    }
66
67    /// Converts stake to shares based on the share price.
68    /// Always rounding down i.e. may return less share due to arithmetic dust.
69    pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
70        self.0.mul_floor(stake).into()
71    }
72
73    /// Converts shares to stake based on the share price.
74    /// Always rounding down i.e. may return less stake due to arithmetic dust.
75    pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
76        // NOTE: `stakes = shares / share_price = shares / (total_shares / total_stake)`
77        // every `div` operation come with an arithmetic dust, to return a rounding down stakes,
78        // we want the first `div` rounding down (i.e. `saturating_reciprocal_mul_floor`) and
79        // the second `div` rounding up (i.e. `plus_epsilon`).
80        self.0
81            // Within the `SharePrice::new`, `Perquintill::from_rational` is internally rouding down,
82            // `plus_epsilon` essentially return a rounding up share price.
83            .plus_epsilon()
84            .saturating_reciprocal_mul_floor(shares.into())
85    }
86
87    /// Return a 1:1 share price
88    pub(crate) fn one() -> Self {
89        Self(Perquintill::one())
90    }
91}
92
93/// Unique epoch identifier across all domains. A combination of Domain and its epoch.
94#[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/// A nominator's shares against their deposits to given operator pool.
115#[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/// A nominators pending deposit in AI3 that needs to be converted to shares once domain epoch is complete.
122#[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/// A nominator's withdrawal from a given operator pool.
138#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
139pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
140    /// Total withdrawal amount requested by the nominator that are in unlocking state excluding withdrawal
141    /// in shares and the storage fee
142    pub(crate) total_withdrawal_amount: Balance,
143    /// Total amount of storage fee on withdraw (including withdrawal in shares)
144    pub(crate) total_storage_fee_withdrawal: Balance,
145    /// Individual withdrawal amounts with their unlocking block for a given domain
146    pub(crate) withdrawals: VecDeque<WithdrawalInBalance<DomainBlockNumber, Balance>>,
147    /// Withdrawal that was initiated by nominator and not converted to balance due to
148    /// unfinished domain epoch.
149    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/// Type that represents an operator status.
185#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
186pub enum OperatorStatus<DomainBlockNumber, ReceiptHash> {
187    #[codec(index = 0)]
188    Registered,
189    /// De-registered at given domain epoch.
190    #[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    /// Operator was deactivated due to being offline.
199    /// Operator can be activated after given epoch_index.
200    #[codec(index = 5)]
201    Deactivated(EpochIndex),
202}
203
204/// Type that represents an operator details.
205#[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    /// Total active stake of combined nominators under this operator.
213    pub current_total_stake: Balance,
214    /// Total shares of all the nominators under this operator.
215    pub current_total_shares: Share,
216    /// The status of the operator, it may be stale due to the `OperatorStatus::PendingSlash` is
217    /// not assigned to this field directly, thus MUST use the `status()` method to query the status
218    /// instead.
219    partial_status: OperatorStatus<DomainBlockNumber, ReceiptHash>,
220    /// Total deposits during the previous epoch
221    pub deposits_in_epoch: Balance,
222    /// Total withdrew shares during the previous epoch
223    pub withdrawals_in_epoch: Share,
224    /// Total balance deposited to the bundle storage fund
225    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    /// Returns true if the operator is either Registered or Deactivated.
249    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    /// Returns true if the operator can submit a bundle.
261    pub fn can_operator_submit_bundle<T: Config>(&self, operator_id: OperatorId) -> bool {
262        let status = self.status::<T>(operator_id);
263        // negate result if the operator is slashed, pending_slash, deactivated, or InvalidBundle
264        !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    /// Current epoch index for the domain.
277    pub current_epoch_index: EpochIndex,
278    /// Total active stake for the current epoch.
279    pub current_total_stake: Balance,
280    /// Current operators for this epoch
281    pub current_operators: BTreeMap<OperatorId, Balance>,
282    /// Operators for the next epoch.
283    pub next_operators: BTreeSet<OperatorId>,
284    /// Operator's current Epoch rewards
285    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
337// Increase `PendingStakingOperationCount` by one and check if the `MaxPendingStakingOperation`
338// limit is exceeded
339fn 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        // reserve stake balance
387        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        // When the operator just registered, the operator owner is the first and only nominator
409        // thus it is safe to finalize the operator owner's deposit here by:
410        // - Adding the first share price, which is 1:1 since there is no reward
411        // - Adding this deposit to the operator's `current_total_shares` and `current_total_shares`
412        //
413        // NOTE: this is needed so we can ensure the operator's `current_total_shares` and `current_total_shares`
414        // will never be zero after it is registered and before all nominators is unlocked, thus we
415        // will never construct a zero share price.
416        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            // sum total deposits added during this epoch.
427            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        // update stake summary to include new operator for next epoch
439        domain_stake_summary.next_operators.insert(operator_id);
440        // update pending transfers
441        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
454/// Calculates shares for any pending deposit for previous epoch using the epoch share price and
455/// then create a new pending deposit in the current epoch.
456/// If there is a pending deposit for the current epoch, then simply increment the amount.
457/// Returns updated deposit info
458pub(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        // add or create new pending deposit
470        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    // if it is one of the previous domain epoch, then calculate shares for the epoch and update known deposit
510    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
551/// Converts any epoch withdrawals into balance using the operator epoch share price.
552///
553/// If there is withdrawal happened in the current epoch (thus share price is unavailable),
554/// this will be no-op. If there is withdrawal happened in the previous epoch and the share
555/// price is unavailable, `MissingOperatorEpochSharePrice` error will be return.
556pub(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            // `withdraw.domain_epoch` is not end yet so the share price won't be available
565            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 this is the first staking request of this operator `note_pending_staking_operation` for it
616        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        // Reserve for the bundle storage fund
624        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        // increment total deposit for operator pool within this epoch
635        operator.deposits_in_epoch = operator
636            .deposits_in_epoch
637            .checked_add(&new_deposit.staking)
638            .ok_or(Error::BalanceOverflow)?;
639
640        // Increase total storage fee deposit as there is new deposit to the storage fund
641        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 there is enough free balance to lock
670    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
688/// Deregisters a given operator who is either registered or deactivated.
689/// Operator is removed from the next operator set.
690pub(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 the operator status is deactivated, then remove from DeactivatedOperator Storage
727                // since the operator epoch share price will be calculated anyway since they will be in
728                // DeregisteredOperators list.
729                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
751/// Deactivates a given operator.
752/// Operator status is marked as Deactivated with epoch_index after which they can reactivate back
753/// into operator set. Their stake is removed from the total domain stake since they will not be
754/// producing bundles anymore until re-registration.
755pub(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                // remove operator from the current and next operator set.
779                // ensure to reduce the total stake if operator is actually present in the
780                // current_operator set
781                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
808/// Reactivate a given deactivated operator if the activation delay in epochs has passed.
809/// The operator is added to next operator set and will be able to produce bundles from next epoch.
810pub(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                // since the operator is active again in this epoch and added to the next
838                // operator set, the share price will be calculated at the end of the epoch.
839                // Remove them from DeactivatedOperator storage.
840                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
855/// A helper function used to calculate the share price at this instant
856/// Returns an error if there are more shares than stake, or if either value is zero.
857pub(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    // Total stake including any reward within this epoch.
863    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                // deduct operator tax
872                .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
879/// Withdraw some or all of the stake, using an amount of shares.
880///
881/// Withdrawal validity depends on the current share price and number of shares, so requests can
882/// pass the initial checks, but fail because the most recent share amount is lower than expected.
883///
884/// Absolute stake amount and percentage withdrawals can be handled in the frontend.
885/// Full stake withdrawals are handled by withdrawing everything, if the remaining number of shares
886/// is less than the minimum nominator stake, and the nominator is not the operator.
887pub(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    // Some withdraws are always zero, others require calculations to check if they are zero.
893    // So this check is redundant, but saves us some work if the request will always be rejected.
894    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 this is the first staking request of this operator `note_pending_staking_operation` for it
905        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        // calculate shares for any previous epoch
910        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 operator is deactivated, to calculate the share price at the end of the epoch,
944        // add them to DeactivatedOperators Storage
945        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                // short circuit to check if remaining shares can be zero
967                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                    // ensure the remaining share value is at least the defined MinOperatorStake if
987                    // a nominator is the operator pool owner
988                    if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
989                        return Err(Error::MinimumOperatorStake);
990                    }
991
992                    // if not an owner, if remaining balance < MinNominatorStake, then withdraw all shares.
993                    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            // Withdraw storage fund, the `withdraw_storage_fee` amount of fund will be transferred
1002            // and hold on the nominator account
1003            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            // update operator pool to note withdrew shares in the epoch
1032            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                // if there is a pending deposit, then ensure
1042                // the new deposit is atleast minimum nominator stake
1043                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                // if this is some, then the withdrawal was initiated in this current epoch due to conversion
1057                // of previous epoch withdrawals from shares to balances above. So just update it instead
1058                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
1095/// Unlocks any withdraws that are ready to be unlocked.
1096///
1097/// Return the number of withdrawals being unlocked
1098pub(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        // There is withdrawal but none being processed meaning the first withdrawal's unlock period has
1158        // not completed yet
1159        ensure!(
1160            !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
1161            Error::UnlockPeriodNotComplete
1162        );
1163
1164        // deduct the amount unlocked from total
1165        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        // If the amount to release is more than currently locked,
1176        // mint the diff and release the rest
1177        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        // Mint any gains
1190        if !amount_to_mint.is_zero() {
1191            mint_funds::<T>(&nominator_id, amount_to_mint)?;
1192        }
1193        // Release staking fund
1194        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        // Release storage fund
1212        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 there are no withdrawals, then delete the storage as well
1228        if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1229            *maybe_withdrawal = None;
1230            // if there is no deposit or pending deposits, then clean up the deposit state as well
1231            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
1256/// Unlocks an already de-registered operator's nominator given unlock wait period is complete.
1257pub(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        // take the operator so this operator info is removed once we unlock the operator.
1263        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        // convert any deposits from the previous epoch to shares.
1296        // share prices will always be present because
1297        // - if there are any deposits before operator de-registered, we ensure to create a
1298        //   share price for them at the time of epoch transition.
1299        // - if the operator got rewarded after the being de-registered and due to nomination tax
1300        //   operator self deposits said tax amount, we calculate share price at the time of epoch transition.
1301        do_convert_previous_epoch_deposits::<T>(
1302            operator_id,
1303            &mut deposit,
1304            current_domain_epoch_index,
1305        )?;
1306
1307        // if there are any withdrawals from this operator, account for them
1308        // if the withdrawals has share price noted, then convert them to AI3
1309        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                // convert any withdrawals from the previous epoch to stake.
1316                // share prices will always be present because
1317                // - if there are any withdrawals before operator de-registered, we ensure to create a
1318                //   share price for them at the time of epoch transition.
1319                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        // include all the known shares and shares that were withdrawn in the current epoch
1336        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        // current staked amount
1346        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        // amount deposited by this nominator before operator de-registered.
1352        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        // Remove the lock and mint any gains
1363        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(&current_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        // Withdraw all storage fee for the nominator
1385        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        // Release all storage fee on withdraw of the nominator
1400        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        // reduce total storage fee deposit with nominator total fee deposit
1416        total_storage_fee_deposit =
1417            total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1418
1419        // The operator state is safe to cleanup if there is no entry in `Deposits` and `Withdrawals`
1420        // which means all nominator (including the operator owner) have unlocked their stake.
1421        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            // set update total shares, total stake and total storage fee deposit for operator
1428            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
1439/// Removes all operator storages and mints the total stake back to treasury.
1440pub(crate) fn do_cleanup_operator<T: Config>(
1441    operator_id: OperatorId,
1442    total_stake: BalanceOf<T>,
1443) -> Result<(), Error> {
1444    // transfer any remaining storage fund to treasury
1445    bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1446        .map_err(Error::BundleStorageFund)?;
1447
1448    // transfer any remaining amount to treasury
1449    mint_into_treasury::<T>(total_stake)?;
1450
1451    // remove OperatorOwner Details
1452    OperatorIdOwner::<T>::remove(operator_id);
1453
1454    // remove `OperatorHighestSlot`
1455    OperatorHighestSlot::<T>::remove(operator_id);
1456
1457    // remove operator epoch share prices
1458    let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1459
1460    Ok(())
1461}
1462
1463/// Distribute the reward to the operators equally and drop any dust to treasury.
1464pub(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        // calculate the operator weights based on the number of times they are repeated in the original list.
1480        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 remaining funds to treasury
1515        mint_into_treasury::<T>(
1516            rewards
1517                .checked_sub(&allocated_rewards)
1518                .ok_or(Error::BalanceUnderflow)?,
1519        )
1520    })
1521}
1522
1523/// Freezes the slashed operators and moves the operator to be removed once the domain they are
1524/// operating finishes the epoch.
1525pub(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                // If the operator is already slashed and removed due to fraud proof, when the operator
1533                // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1534                // not find the operator here thus just return.
1535                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                    // slash and remove operator from next and current epoch set
1553                    operator.update_status(OperatorStatus::Slashed);
1554
1555                    // ensure to reduce the total stake if operator is actually present in the
1556                    // current_operator set
1557                    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
1583/// Mark all the invalid bundle authors from this ER and remove them from operator set.
1584/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent
1585pub(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            // If the operator is already slashed and removed due to fraud proof, when the operator
1623            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1624            // not find the operator here thus just return.
1625            None => return Ok(()),
1626            Some(operator) => operator,
1627        };
1628
1629        // operator must be in registered status.
1630        // for other states, we anyway do not allow bundle submission.
1631        if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1632            return Ok(());
1633        }
1634
1635        // slash and remove operator from next and current epoch set
1636        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
1653/// Unmark all the invalid bundle authors from this ER that were marked invalid.
1654/// Assumed the ER is invalid and add the marked operators as registered and add them
1655/// back to next operator set.
1656/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent
1657pub(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            // If the operator is already slashed and removed due to fraud proof, when the operator
1697            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1698            // not find the operator here thus just return.
1699            None => return Ok(()),
1700            Some(operator) => operator,
1701        };
1702
1703        // operator must be in invalid bundle state with the exact er
1704        if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1705            return Ok(());
1706        }
1707
1708        // add operator to next set
1709        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            // operator_id should be 0 and be registered
1926            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            // registering with same operator key is allowed
1957            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            // cannot use the locked funds to register a new operator
1966            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            // another transfer with an existing transfer in place should lead to single
2042            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 epoch transition
2078            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                        // since the Withdrawals locking period is 5 and confirmed domain block is 0
2129                        5
2130                    )
2131                        .into()
2132                )
2133            );
2134
2135            // operator nomination will not work since the operator is already de-registered
2136            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            // nominations will not work since the is frozen
2169            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            // operator nomination will not work since the operator is deactivated
2233            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            // deactivate operator
2277            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            // operator should be part of the DeactivatedOperator storage
2304            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2305
2306            // transition epoch
2307            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2308
2309            // operator not should be part of the DeactivatedOperator storage after epoch transition
2310            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2311
2312            // nominator withdraw should work even though operator is deactivated
2313            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            // operator should be part of the DeactivatedOperator storage since there is a new
2322            // withdrawal and share prices needs to be calculated.
2323            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            // transition epoch
2329            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2330
2331            // share price should exist for previous epoch
2332            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            // unlocking nominator funds should work when operator is still deactivated
2344            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            // deactivate operator
2379            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            // operator should be part of the DeactivatedOperator storage
2406            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2407
2408            // transition epoch
2409            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2410
2411            // operator not should be part of the DeactivatedOperator storage after epoch transition
2412            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2413
2414            // nominator withdraw should work even though operator is deactivated
2415            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            // operator should be part of the DeactivatedOperator storage since there is a new
2424            // withdrawal and share prices needs to be calculated.
2425            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2426
2427            // operator deregistration should work even if operator is deactivated
2428            let res =
2429                Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2430            assert_ok!(res);
2431
2432            // operator not should be part of the DeactivatedOperator storage since operator is
2433            // deregistered but instead should be part of DeregisteredOperators storage
2434            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            // transition epoch
2441            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2442
2443            // share price should exist for previous epoch
2444            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            // unlocking nominator should work
2462            let res =
2463                Domains::unlock_nominator(RuntimeOrigin::signed(nominator_account), operator_id);
2464            assert_ok!(res);
2465
2466            // unlocking operator also should work
2467            let res =
2468                Domains::unlock_nominator(RuntimeOrigin::signed(operator_account), operator_id);
2469            assert_ok!(res);
2470
2471            // cleanup of operator should be done
2472            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            // reactivation should not work before cool off period
2523            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            // operator not should be part of the DeactivatedOperator storage
2536            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2537
2538            // nominator withdraw should work even though operator is deactivated
2539            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            // operator should be part of the DeactivatedOperator storage since there is a withdrawal
2548            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            // operator not should be part of the DeactivatedOperator storage since operator is
2554            // reactivated and moved to next operator set
2555            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    /// Expected withdrawal amount.
2571    /// Bool indicates to include existential deposit while asserting the final balance
2572    /// since ED is not holded back from usable balance when there are no holds on the account.
2573    type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2574
2575    /// The storage fund change in AI3, `true` means increase of the storage fund, `false` means decrease.
2576    type StorageFundChange = (bool, u32);
2577
2578    pub(crate) type Share = <Test as Config>::Share;
2579
2580    struct WithdrawParams {
2581        /// The minimum valid nominator stake.
2582        minimum_nominator_stake: BalanceOf<Test>,
2583        /// The nominator IDs and their stakes.
2584        /// Account 0 is the operator and its stake.
2585        nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2586        /// The operator reward.
2587        operator_reward: BalanceOf<Test>,
2588        /// The nominator ID to withdraw from.
2589        nominator_id: NominatorId<Test>,
2590        /// The withdraw attempts to be made, in order, with their expected results.
2591        withdraws: WithdrawWithResult,
2592        /// The deposit to be made when nominating the operator, if any.
2593        maybe_deposit: Option<BalanceOf<Test>>,
2594        /// The expected withdraw amount for `nominator_id`.
2595        /// Includes the existential deposit if `true`.
2596        expected_withdraw: ExpectedWithdrawAmount,
2597        /// The expected reduction in the number of nominators.
2598        expected_nominator_count_reduced_by: u32,
2599        /// The storage fund change, increase if `true`.
2600        storage_fund_change: StorageFundChange,
2601    }
2602
2603    /// Withdraw stake in a non-proptest.
2604    fn withdraw_stake(params: WithdrawParams) {
2605        withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2606    }
2607
2608    /// Withdraw stake in a proptest.
2609    fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2610        withdraw_stake_inner(params, true)
2611    }
2612
2613    /// Inner function for withdrawing stake.
2614    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                // Update `HeadDomainNumber` to ensure unlock success
2734                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                // ensure there are no withdrawals left
2761                assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2762            }
2763
2764            // if the nominator count reduced, then there should be no storage for deposits as well
2765            // TODO: assert this matches the change in the number of nominators
2766            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            // The total balance is distributed in different places but never changed.
2789            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    /// Do an approximate amount comparison in a proptest.
2824    /// Takes actual and expected amounts, and an optional extra format string and arguments.
2825    ///
2826    /// The user should never get more, and they shouldn't get significantly less.
2827    /// Accounts for both absolute and relative rounding errors.
2828    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    // Export the macro for use in other modules (and earlier in this file).
2871    pub(crate) use prop_assert_approx;
2872
2873    /// Rounding down factor for property tests to account for arithmetic precision errors.
2874    /// This factor is used to allow for small rounding errors in calculations.
2875    // Perquintill::from_parts(...).left_from_one(), as a constant.
2876    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    /// Absolute rounding error tolerance for property tests.
2880    /// This constant defines the maximum acceptable absolute rounding error in calculations.
2881    pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2882
2883    /// The maximum balance we test for in property tests.
2884    /// This balance should be just below the maximum possible issuance.
2885    ///
2886    /// Limiting the balances avoids arithmetic errors in the withdraw_stake test function, and
2887    /// RemoveLock (likely converted from BalanceOverflow) in the staking functions.
2888    //
2889    // TODO: fix the code so we can get closer to 2^128
2890    pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2891
2892    /// The minimum operator stake we test for in property tests.
2893    // TODO: edit the test harness so we can go as low as MinOperatorStake + 1
2894    pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2895
2896    /// The minimum nominator stake we test for in property tests.
2897    pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2898        <Test as Config>::MinNominatorStake::get() + 1;
2899
2900    /// The range of operator stakes we test for in property tests.
2901    pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2902        MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2903
2904    /// The range of nominator stakes we test for in property tests.
2905    pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2906        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2907
2908    /// The range of operator or nominator deposits we test for in property tests.
2909    // TODO: edit the test harness so we can go as low as zero
2910    pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2911        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2912
2913    /// The range of operator rewards we test for in property tests.
2914    pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2915
2916    /// The range of operator free balances we test for in property tests.
2917    pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2918        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2919
2920    // Using too many random parameters and prop_assume()s can reduce test coverage.
2921    // Try to limit the number of parameters to 3.
2922
2923    /// Property test for withdrawing an operator's excess stake.
2924    /// Their balance should be almost the same before and after.
2925    #[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            // Account for both absolute and relative rounding errors.
2932            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    /// Property test for withdrawing all a nominator's excess stake.
2956    /// Their balance should be almost the same before and after.
2957    #[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                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2963                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    /// Property test for withdrawing an operator's excess stake with a deposit.
2994    /// Their balance should be almost the same before and after.
2995    #[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                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3009                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                    // TODO: work out how to avoid this multiplication on WithdrawParams.withdraws
3018                    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                // Avoid ZeroDeposit errors
3032                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    /// Property test for withdrawing an operator's excess stake with a deposit and fixed
3054    /// operator stake.
3055    /// Their balance should be almost the same before and after.
3056    #[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                // MinimumOperatorStake error
3070                let operator_stake = <Test as Config>::MinOperatorStake::get();
3071
3072                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3073                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                // Avoid ZeroDeposit errors
3095                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    /// Property test for withdrawing a nominator's excess stake with a deposit.
3117    /// Their balance should be almost the same before and after.
3118    #[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                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3136                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                // Some deposit gets left as storage fees.
3144                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                // Avoid ZeroDeposit errors
3159                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    /// Property test for withdrawing an operator's excess stake with a nominator and a reward.
3181    /// The total balance should be increased by part of the reward afterwards.
3182    #[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                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3192                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                // Withdraw is stake plus reward split.
3200                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                // Shares are approximately equal to the original stake.
3205                // TODO: fix the tests so we can go as low as MinOperatorStake
3206                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    /// Property test for withdrawing an operator's excess stake with a deposit and a reward.
3229    /// Their balance should be almost the same before and after.
3230    #[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                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3248                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                // Shares are approximately equal to the original stake.
3256                // TODO: fix the tests so we can go as low as MinOperatorStake
3257                let reserve = 2 * <Test as Config>::MinOperatorStake::get();
3258                let excess_stake = operator_stake.saturating_sub(reserve);
3259
3260                // Withdraw is stake plus reward, but some deposit gets left as storage fees.
3261                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                // Avoid ZeroDeposit errors
3271                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            // given the reward, operator will get 164.28 AI3
3346            // taking 58 shares will give this following approximate amount.
3347            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            // given nominator remaining stake goes below minimum
3445            // we withdraw everything, so for their 50 shares with reward,
3446            // price would be following
3447            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            // given nominator remaining stake goes below minimum
3463            // we withdraw everything, so for their 50 shares with reward,
3464            // price would be following
3465            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            // given nominator remaining stake goes below minimum
3485            // we withdraw everything, so for their 50 shares with reward,
3486            // price would be following
3487            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            // given nominator remaining stake goes below minimum
3673            // we withdraw everything, so for their 50 shares with reward,
3674            // price would be following
3675            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            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3707            // storage fee profit i.e. 5 * AI3 with rounding dust deducted
3708            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            // The storage fund decreased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3724            // storage fee deposit i.e. 5 * AI3 with rounding dust deducted
3725            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            // The storage fund decreased 100% (i.e. 42 * AI3) thus the nominator loss 100%
3741            // storage fee deposit i.e. 10 * AI3
3742            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            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3758            // storage fee profit i.e. 5 * AI3 with rounding dust deducted, withdraw 60% of
3759            // the stake and the storage fee profit
3760            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            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3776            // storage fee i.e. 5 * AI3 with rounding dust deducted, withdraw 40% of
3777            // the stake and 40% of the storage fee loss are deducted
3778            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            // Guess that the number of shares will be approximately the same as the stake amount.
3830            let shares_per_withdraw = init_total_stake / 100;
3831            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3832
3833            // Request `WithdrawalLimit - 1` number of withdrawal
3834            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            // Increase the head domain number by 1
3840            HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3841
3842            // All withdrawals of a given nominator submitted in the same epoch will merge into one,
3843            // so we can submit as many as we want, even though the withdrawal limit is met.
3844            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            // After the withdrawal limit is met, any new withdraw will be rejected in the next epoch
3851            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            // Make the first set of withdrawals pass the unlock period then unlock fund
3873            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, // `74` is a minor rounding dust
3881                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            // Make the second set of withdrawals pass the unlock period then unlock funds
3889            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, // `2` is a minor rounding dust
3899                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            // Manually convert previous withdrawal in share to balance
3972            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            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
3985            // due to withdrawal of 20 shares
3986            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            // Manually convert previous withdrawal in share to balance
4136            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            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
4149            // due to withdrawal of 20 shares
4150            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                    // Guess that the number of shares will be approximately the same as the stake
4185                    // amount.
4186                    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            // since we only slash 10 nominators a time but we have a total of 21 nominators,
4214            // do 3 iterations
4215            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            // deactivated operators can be slashed
4310            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            // Drain the bundle storage fund
4417            bundle_storage_fund::charge_bundle_storage_fee::<Test>(
4418                operator_id,
4419                // the transaction fee is one AI3 per byte thus div AI3 here
4420                (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            // The operator add more stake thus add deposit to the bundle storage fund
4430            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            // New nominator add deposit to the bundle storage fund
4437            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            // Refund of the storage fee add deposit to the bundle storage fund
4445            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            // The operator `operator_id + 1` not exist thus the refund storage fee added to treasury
4453            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            // Zero deposits should be rejected
4497            assert_err!(
4498                do_nominate_operator::<Test>(operator_id, nominator_account, 0),
4499                StakingError::ZeroDeposit
4500            );
4501
4502            // Zero withdraws should be rejected
4503            assert_err!(
4504                do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
4505                StakingError::ZeroWithdraw
4506            );
4507
4508            // Withdraw all
4509            do_withdraw_stake::<Test>(
4510                operator_id,
4511                nominator_account,
4512                // Assume shares are similar to the stake amount
4513                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            // Assume shares will be approximately the same as the stake amount.
4558            do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4559
4560            // Completed current epoch
4561            let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4562            // Remove the epoch share price intentionally
4563            OperatorEpochSharePrice::<Test>::remove(
4564                operator_id,
4565                DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4566            );
4567
4568            // Both deposit and withdraw should fail due to the share price is missing unexpectedly
4569            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        // Each item in this list represents an individual deposit requested by a nominator
4587        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` is sum of the stake deposited so far.
4610            deposited_stake += to_deposit_stake;
4611            // `deposited_share` is sum of the share that converted from `deposited_stake` so far,
4612            // this is also the share the nominator entitled to withdraw.
4613            deposited_share += to_deposit_share;
4614
4615            // Assuming an epoch transition happened
4616            //
4617            // `total_deposited_share` is the share converted from `operator.deposits_in_epoch`
4618            // and will be added to the `operator.current_total_shares`.
4619            let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4620
4621            // `total_deposited_share` must larger or equal to `deposited_share`, meaning the
4622            // arithmetic dust generated during stake-to-share convertion are leave to the pool
4623            // and can't withdraw/unlock, otherwise, `ShareOverflow` error will happen on `current_total_shares`
4624            // during withdraw/unlock.
4625            assert!(total_deposited_share >= deposited_share);
4626
4627            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4628            // lost during stake-to-share convertion.
4629            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        // Each item in this list represents an individual withdrawal requested by a nominator
4640        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` is sum of the share withdrawn so far.
4663            withdrawn_share += to_withdraw_share;
4664            // `withdrawn_stake` is sum of the stake that converted from `withdrawn_share` so far,
4665            // this is also the stake the nominator entitled to release/mint during unlock.
4666            withdrawn_stake += to_withdraw_stake;
4667
4668            // Assuming an epoch transition happened
4669            //
4670            // `total_withdrawn_stake` is the stake converted from `operator.withdrawals_in_epoch`
4671            // and will be removed to the `operator.current_total_stake`.
4672            let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4673
4674            // `total_withdrawn_stake` must larger or equal to `withdrawn_stake`, meaning the
4675            // arithmetic dust generated during share-to-stake convertion are leave to the pool,
4676            // otherwise, the nominator will be able to mint reward out of thin air during unlock.
4677            assert!(total_withdrawn_stake >= withdrawn_stake);
4678
4679            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4680            // lost during share-to-stake convertion.
4681            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        // Each item in this list represents a nominator unlock after the operator de-registered.
4691        //
4692        // The following is simulating how `do_unlock_nominator` work, `shares-to-stake` must return a
4693        // rouding down result, otherwise, `BalanceOverflow` error will happen on `current_total_stake`
4694        // during `do_unlock_nominator`.
4695        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}