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.
1584pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1585    domain_id: DomainId,
1586    er: &ExecutionReceiptOf<T>,
1587) -> Result<(), Error> {
1588    let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1589    let er_hash = er.hash::<DomainHashingFor<T>>();
1590    let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1591    let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1592    let mut stake_summary =
1593        DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1594
1595    for operator_id in invalid_bundle_authors {
1596        if pending_slashes.contains(&operator_id) {
1597            continue;
1598        }
1599
1600        mark_invalid_bundle_author::<T>(
1601            operator_id,
1602            er_hash,
1603            &mut stake_summary,
1604            &mut invalid_bundle_authors_in_epoch,
1605        )?;
1606    }
1607
1608    DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1609    InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1610    Ok(())
1611}
1612
1613pub(crate) fn mark_invalid_bundle_author<T: Config>(
1614    operator_id: OperatorId,
1615    er_hash: ReceiptHashFor<T>,
1616    stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1617    invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1618) -> Result<(), Error> {
1619    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1620        let operator = match maybe_operator.as_mut() {
1621            // If the operator is already slashed and removed due to fraud proof, when the operator
1622            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1623            // not find the operator here thus just return.
1624            None => return Ok(()),
1625            Some(operator) => operator,
1626        };
1627
1628        // operator must be in registered status.
1629        // for other states, we anyway do not allow bundle submission.
1630        if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1631            return Ok(());
1632        }
1633
1634        // slash and remove operator from next and current epoch set
1635        operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1636        invalid_bundle_authors.insert(operator_id);
1637        if stake_summary
1638            .current_operators
1639            .remove(&operator_id)
1640            .is_some()
1641        {
1642            stake_summary.current_total_stake = stake_summary
1643                .current_total_stake
1644                .checked_sub(&operator.current_total_stake)
1645                .ok_or(Error::BalanceUnderflow)?;
1646        }
1647        stake_summary.next_operators.remove(&operator_id);
1648        Ok(())
1649    })
1650}
1651
1652/// Unmark all the invalid bundle authors from this ER that were marked invalid.
1653/// Assumed the ER is invalid and add the marked operators as registered and add them
1654/// back to next operator set.
1655pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1656    domain_id: DomainId,
1657    er: &ExecutionReceiptOf<T>,
1658) -> Result<(), Error> {
1659    let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1660    let er_hash = er.hash::<DomainHashingFor<T>>();
1661    let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1662    let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1663    let mut stake_summary =
1664        DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1665
1666    for operator_id in invalid_bundle_authors {
1667        if pending_slashes.contains(&operator_id)
1668            || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1669        {
1670            continue;
1671        }
1672
1673        unmark_invalid_bundle_author::<T>(
1674            operator_id,
1675            er_hash,
1676            &mut stake_summary,
1677            &mut invalid_bundle_authors_in_epoch,
1678        )?;
1679    }
1680
1681    DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1682    InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1683    Ok(())
1684}
1685
1686fn unmark_invalid_bundle_author<T: Config>(
1687    operator_id: OperatorId,
1688    er_hash: ReceiptHashFor<T>,
1689    stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1690    invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1691) -> Result<(), Error> {
1692    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1693        let operator = match maybe_operator.as_mut() {
1694            // If the operator is already slashed and removed due to fraud proof, when the operator
1695            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1696            // not find the operator here thus just return.
1697            None => return Ok(()),
1698            Some(operator) => operator,
1699        };
1700
1701        // operator must be in invalid bundle state with the exact er
1702        if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1703            return Ok(());
1704        }
1705
1706        // add operator to next set
1707        operator.update_status(OperatorStatus::Registered);
1708        invalid_bundle_authors.remove(&operator_id);
1709        stake_summary.next_operators.insert(operator_id);
1710        Ok(())
1711    })
1712}
1713
1714#[cfg(test)]
1715pub(crate) mod tests {
1716    use crate::domain_registry::{DomainConfig, DomainObject};
1717    use crate::pallet::{
1718        Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1719        NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1720    };
1721    use crate::staking::{
1722        DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorDeregisteredInfo,
1723        OperatorStatus, SharePrice, StakingSummary, do_convert_previous_epoch_withdrawal,
1724        do_mark_operators_as_slashed, do_nominate_operator, do_reward_operators, do_unlock_funds,
1725        do_withdraw_stake,
1726    };
1727    use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1728    use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext};
1729    use crate::{
1730        BalanceOf, DeactivatedOperators, DeregisteredOperators, Error, MAX_NOMINATORS_TO_SLASH,
1731        NominatorId, OperatorEpochSharePrice, SlashedReason, bundle_storage_fund,
1732    };
1733    use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
1734    use frame_support::traits::Currency;
1735    use frame_support::traits::fungible::Mutate;
1736    use frame_support::weights::Weight;
1737    use frame_support::{assert_err, assert_ok};
1738    use prop_test::prelude::*;
1739    use prop_test::proptest::test_runner::TestCaseResult;
1740    use sp_core::{Pair, sr25519};
1741    use sp_domains::{
1742        DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1743        OperatorRewardSource,
1744    };
1745    use sp_runtime::traits::Zero;
1746    use sp_runtime::{PerThing, Percent, Perquintill};
1747    use std::collections::{BTreeMap, BTreeSet};
1748    use std::ops::RangeInclusive;
1749    use std::vec;
1750    use subspace_runtime_primitives::AI3;
1751
1752    type Balances = pallet_balances::Pallet<Test>;
1753    type Domains = crate::Pallet<Test>;
1754
1755    const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1756
1757    #[allow(clippy::too_many_arguments)]
1758    pub(crate) fn register_operator(
1759        domain_id: DomainId,
1760        operator_account: <Test as frame_system::Config>::AccountId,
1761        operator_free_balance: BalanceOf<Test>,
1762        operator_stake: BalanceOf<Test>,
1763        minimum_nominator_stake: BalanceOf<Test>,
1764        signing_key: OperatorPublicKey,
1765        nomination_tax: Percent,
1766        mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1767    ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1768        nominators.insert(operator_account, (operator_free_balance, operator_stake));
1769        for nominator in &nominators {
1770            Balances::set_balance(nominator.0, nominator.1.0);
1771            assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1772        }
1773        nominators.remove(&operator_account);
1774
1775        if !DomainRegistry::<Test>::contains_key(domain_id) {
1776            let domain_config = DomainConfig {
1777                domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1778                runtime_id: 0,
1779                max_bundle_size: u32::MAX,
1780                max_bundle_weight: Weight::MAX,
1781                bundle_slot_probability: (0, 0),
1782                operator_allow_list: OperatorAllowList::Anyone,
1783                initial_balances: Default::default(),
1784            };
1785
1786            let domain_obj = DomainObject {
1787                owner_account_id: 0,
1788                created_at: 0,
1789                genesis_receipt_hash: Default::default(),
1790                domain_config,
1791                domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1792                domain_instantiation_deposit: Default::default(),
1793            };
1794
1795            DomainRegistry::<Test>::insert(domain_id, domain_obj);
1796        }
1797
1798        if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1799            DomainStakingSummary::<Test>::insert(
1800                domain_id,
1801                StakingSummary {
1802                    current_epoch_index: 0,
1803                    current_total_stake: 0,
1804                    current_operators: BTreeMap::new(),
1805                    next_operators: BTreeSet::new(),
1806                    current_epoch_rewards: BTreeMap::new(),
1807                },
1808            );
1809        }
1810
1811        let operator_config = OperatorConfig {
1812            signing_key,
1813            minimum_nominator_stake,
1814            nomination_tax,
1815        };
1816
1817        let res = Domains::register_operator(
1818            RuntimeOrigin::signed(operator_account),
1819            domain_id,
1820            operator_stake,
1821            operator_config.clone(),
1822        );
1823        assert_ok!(res);
1824
1825        let operator_id = NextOperatorId::<Test>::get() - 1;
1826        for nominator in nominators {
1827            if nominator.1.1.is_zero() {
1828                continue;
1829            }
1830
1831            let res = Domains::nominate_operator(
1832                RuntimeOrigin::signed(nominator.0),
1833                operator_id,
1834                nominator.1.1,
1835            );
1836            assert_ok!(res);
1837            assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1838        }
1839
1840        (operator_id, operator_config)
1841    }
1842
1843    #[test]
1844    fn test_register_operator_invalid_signing_key() {
1845        let domain_id = DomainId::new(0);
1846        let operator_account = 1;
1847
1848        let mut ext = new_test_ext();
1849        ext.execute_with(|| {
1850            let operator_config = OperatorConfig {
1851                signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1852                minimum_nominator_stake: Default::default(),
1853                nomination_tax: Default::default(),
1854            };
1855
1856            let res = Domains::register_operator(
1857                RuntimeOrigin::signed(operator_account),
1858                domain_id,
1859                Default::default(),
1860                operator_config,
1861            );
1862            assert_err!(
1863                res,
1864                Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1865            );
1866        });
1867    }
1868
1869    #[test]
1870    fn test_register_operator_minimum_nominator_stake() {
1871        let domain_id = DomainId::new(0);
1872        let operator_account = 1;
1873        let pair = OperatorPair::from_seed(&[0; 32]);
1874
1875        let mut ext = new_test_ext();
1876        ext.execute_with(|| {
1877            let operator_config = OperatorConfig {
1878                signing_key: pair.public(),
1879                minimum_nominator_stake: Default::default(),
1880                nomination_tax: Default::default(),
1881            };
1882
1883            let res = Domains::register_operator(
1884                RuntimeOrigin::signed(operator_account),
1885                domain_id,
1886                Default::default(),
1887                operator_config,
1888            );
1889            assert_err!(
1890                res,
1891                Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1892            );
1893        });
1894    }
1895
1896    #[test]
1897    fn test_register_operator() {
1898        let domain_id = DomainId::new(0);
1899        let operator_account = 1;
1900        let operator_free_balance = 2500 * AI3;
1901        let operator_total_stake = 1000 * AI3;
1902        let operator_stake = 800 * AI3;
1903        let operator_storage_fee_deposit = 200 * AI3;
1904        let pair = OperatorPair::from_seed(&[0; 32]);
1905
1906        let mut ext = new_test_ext();
1907        ext.execute_with(|| {
1908            let (operator_id, mut operator_config) = register_operator(
1909                domain_id,
1910                operator_account,
1911                operator_free_balance,
1912                operator_total_stake,
1913                AI3,
1914                pair.public(),
1915                Default::default(),
1916                BTreeMap::new(),
1917            );
1918
1919            assert_eq!(NextOperatorId::<Test>::get(), 1);
1920            // operator_id should be 0 and be registered
1921            assert_eq!(
1922                OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1923                operator_account
1924            );
1925            assert_eq!(
1926                Operators::<Test>::get(operator_id).unwrap(),
1927                Operator {
1928                    signing_key: pair.public(),
1929                    current_domain_id: domain_id,
1930                    next_domain_id: domain_id,
1931                    minimum_nominator_stake: AI3,
1932                    nomination_tax: Default::default(),
1933                    current_total_stake: operator_stake,
1934                    current_total_shares: operator_stake,
1935                    partial_status: OperatorStatus::Registered,
1936                    deposits_in_epoch: 0,
1937                    withdrawals_in_epoch: 0,
1938                    total_storage_fee_deposit: operator_storage_fee_deposit,
1939                }
1940            );
1941
1942            let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1943            assert!(stake_summary.next_operators.contains(&operator_id));
1944            assert_eq!(stake_summary.current_total_stake, operator_stake);
1945
1946            assert_eq!(
1947                Balances::usable_balance(operator_account),
1948                operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1949            );
1950
1951            // registering with same operator key is allowed
1952            let res = Domains::register_operator(
1953                RuntimeOrigin::signed(operator_account),
1954                domain_id,
1955                operator_stake,
1956                operator_config.clone(),
1957            );
1958            assert_ok!(res);
1959
1960            // cannot use the locked funds to register a new operator
1961            let new_pair = OperatorPair::from_seed(&[1; 32]);
1962            operator_config.signing_key = new_pair.public();
1963            let res = Domains::register_operator(
1964                RuntimeOrigin::signed(operator_account),
1965                domain_id,
1966                operator_stake,
1967                operator_config,
1968            );
1969            assert_err!(
1970                res,
1971                Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1972            );
1973        });
1974    }
1975
1976    #[test]
1977    fn nominate_operator() {
1978        let domain_id = DomainId::new(0);
1979        let operator_account = 1;
1980        let operator_free_balance = 1500 * AI3;
1981        let operator_total_stake = 1000 * AI3;
1982        let operator_stake = 800 * AI3;
1983        let operator_storage_fee_deposit = 200 * AI3;
1984        let pair = OperatorPair::from_seed(&[0; 32]);
1985
1986        let nominator_account = 2;
1987        let nominator_free_balance = 150 * AI3;
1988        let nominator_total_stake = 100 * AI3;
1989        let nominator_stake = 80 * AI3;
1990        let nominator_storage_fee_deposit = 20 * AI3;
1991
1992        let mut ext = new_test_ext();
1993        ext.execute_with(|| {
1994            let (operator_id, _) = register_operator(
1995                domain_id,
1996                operator_account,
1997                operator_free_balance,
1998                operator_total_stake,
1999                10 * AI3,
2000                pair.public(),
2001                Default::default(),
2002                BTreeMap::from_iter(vec![(
2003                    nominator_account,
2004                    (nominator_free_balance, nominator_total_stake),
2005                )]),
2006            );
2007
2008            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2009            assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
2010
2011            let operator = Operators::<Test>::get(operator_id).unwrap();
2012            assert_eq!(operator.current_total_stake, operator_stake);
2013            assert_eq!(operator.current_total_shares, operator_stake);
2014            assert_eq!(
2015                operator.total_storage_fee_deposit,
2016                operator_storage_fee_deposit + nominator_storage_fee_deposit
2017            );
2018            assert_eq!(operator.deposits_in_epoch, nominator_stake);
2019
2020            let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2021                .unwrap()
2022                .pending
2023                .unwrap();
2024            assert_eq!(pending_deposit.amount, nominator_stake);
2025            assert_eq!(
2026                pending_deposit.storage_fee_deposit,
2027                nominator_storage_fee_deposit
2028            );
2029            assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
2030
2031            assert_eq!(
2032                Balances::usable_balance(nominator_account),
2033                nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
2034            );
2035
2036            // another transfer with an existing transfer in place should lead to single
2037            let additional_nomination_total_stake = 40 * AI3;
2038            let additional_nomination_stake = 32 * AI3;
2039            let additional_nomination_storage_fee_deposit = 8 * AI3;
2040            let res = Domains::nominate_operator(
2041                RuntimeOrigin::signed(nominator_account),
2042                operator_id,
2043                additional_nomination_total_stake,
2044            );
2045            assert_ok!(res);
2046            let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2047                .unwrap()
2048                .pending
2049                .unwrap();
2050            assert_eq!(
2051                pending_deposit.amount,
2052                nominator_stake + additional_nomination_stake
2053            );
2054            assert_eq!(
2055                pending_deposit.storage_fee_deposit,
2056                nominator_storage_fee_deposit + additional_nomination_storage_fee_deposit
2057            );
2058
2059            let operator = Operators::<Test>::get(operator_id).unwrap();
2060            assert_eq!(operator.current_total_stake, operator_stake);
2061            assert_eq!(
2062                operator.deposits_in_epoch,
2063                nominator_stake + additional_nomination_stake
2064            );
2065            assert_eq!(
2066                operator.total_storage_fee_deposit,
2067                operator_storage_fee_deposit
2068                    + nominator_storage_fee_deposit
2069                    + additional_nomination_storage_fee_deposit
2070            );
2071
2072            // do epoch transition
2073            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2074
2075            let operator = Operators::<Test>::get(operator_id).unwrap();
2076            assert_eq!(
2077                operator.current_total_stake,
2078                operator_stake + nominator_stake + additional_nomination_stake
2079            );
2080
2081            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2082            assert_eq!(
2083                domain_staking_summary.current_total_stake,
2084                operator_stake + nominator_stake + additional_nomination_stake
2085            );
2086        });
2087    }
2088
2089    #[test]
2090    fn operator_deregistration() {
2091        let domain_id = DomainId::new(0);
2092        let operator_account = 1;
2093        let operator_stake = 200 * AI3;
2094        let operator_free_balance = 250 * AI3;
2095        let pair = OperatorPair::from_seed(&[0; 32]);
2096        let mut ext = new_test_ext();
2097        ext.execute_with(|| {
2098            let (operator_id, _) = register_operator(
2099                domain_id,
2100                operator_account,
2101                operator_free_balance,
2102                operator_stake,
2103                AI3,
2104                pair.public(),
2105                Default::default(),
2106                BTreeMap::new(),
2107            );
2108
2109            let res =
2110                Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2111            assert_ok!(res);
2112
2113            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2114            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2115
2116            let operator = Operators::<Test>::get(operator_id).unwrap();
2117            assert_eq!(
2118                *operator.status::<Test>(operator_id),
2119                OperatorStatus::Deregistered(
2120                    (
2121                        domain_id,
2122                        domain_stake_summary.current_epoch_index,
2123                        // since the Withdrawals locking period is 5 and confirmed domain block is 0
2124                        5
2125                    )
2126                        .into()
2127                )
2128            );
2129
2130            // operator nomination will not work since the operator is already de-registered
2131            let new_domain_id = DomainId::new(1);
2132            let domain_config = DomainConfig {
2133                domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
2134                runtime_id: 0,
2135                max_bundle_size: u32::MAX,
2136                max_bundle_weight: Weight::MAX,
2137                bundle_slot_probability: (0, 0),
2138                operator_allow_list: OperatorAllowList::Anyone,
2139                initial_balances: Default::default(),
2140            };
2141
2142            let domain_obj = DomainObject {
2143                owner_account_id: 0,
2144                created_at: 0,
2145                genesis_receipt_hash: Default::default(),
2146                domain_config,
2147                domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
2148                domain_instantiation_deposit: Default::default(),
2149            };
2150
2151            DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
2152            DomainStakingSummary::<Test>::insert(
2153                new_domain_id,
2154                StakingSummary {
2155                    current_epoch_index: 0,
2156                    current_total_stake: 0,
2157                    current_operators: BTreeMap::new(),
2158                    next_operators: BTreeSet::new(),
2159                    current_epoch_rewards: BTreeMap::new(),
2160                },
2161            );
2162
2163            // nominations will not work since the is frozen
2164            let nominator_account = 100;
2165            let nominator_stake = 100 * AI3;
2166            let res = Domains::nominate_operator(
2167                RuntimeOrigin::signed(nominator_account),
2168                operator_id,
2169                nominator_stake,
2170            );
2171            assert_err!(
2172                res,
2173                Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2174            );
2175        });
2176    }
2177
2178    #[test]
2179    fn operator_deactivation() {
2180        let domain_id = DomainId::new(0);
2181        let operator_account = 1;
2182        let operator_stake = 200 * AI3;
2183        let operator_free_balance = 250 * AI3;
2184        let pair = OperatorPair::from_seed(&[0; 32]);
2185        let mut ext = new_test_ext();
2186        ext.execute_with(|| {
2187            let (operator_id, _) = register_operator(
2188                domain_id,
2189                operator_account,
2190                operator_free_balance,
2191                operator_stake,
2192                AI3,
2193                pair.public(),
2194                Default::default(),
2195                BTreeMap::new(),
2196            );
2197
2198            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2199            let total_current_stake = domain_stake_summary.current_total_stake;
2200
2201            let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2202            assert_ok!(res);
2203
2204            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2205            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2206            assert!(
2207                !domain_stake_summary
2208                    .current_operators
2209                    .contains_key(&operator_id)
2210            );
2211
2212            let current_epoch_index = domain_stake_summary.current_epoch_index;
2213            let reactivation_delay =
2214                current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2215            let operator = Operators::<Test>::get(operator_id).unwrap();
2216            assert_eq!(
2217                *operator.status::<Test>(operator_id),
2218                OperatorStatus::Deactivated(reactivation_delay)
2219            );
2220
2221            let operator_stake = operator.current_total_stake;
2222            assert_eq!(
2223                total_current_stake,
2224                domain_stake_summary.current_total_stake + operator_stake
2225            );
2226
2227            // operator nomination will not work since the operator is deactivated
2228            let nominator_account = 100;
2229            let nominator_stake = 100 * AI3;
2230            let res = Domains::nominate_operator(
2231                RuntimeOrigin::signed(nominator_account),
2232                operator_id,
2233                nominator_stake,
2234            );
2235            assert_err!(
2236                res,
2237                Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2238            );
2239        });
2240    }
2241
2242    #[test]
2243    fn operator_deactivation_withdraw_stake() {
2244        let domain_id = DomainId::new(0);
2245        let operator_account = 1;
2246        let operator_stake = 200 * AI3;
2247        let operator_free_balance = 250 * AI3;
2248        let pair = OperatorPair::from_seed(&[0; 32]);
2249        let nominator_account = 100;
2250        let nominator_free_balance = 150 * AI3;
2251        let nominator_total_stake = 100 * AI3;
2252        let mut ext = new_test_ext();
2253        ext.execute_with(|| {
2254            let (operator_id, _) = register_operator(
2255                domain_id,
2256                operator_account,
2257                operator_free_balance,
2258                operator_stake,
2259                AI3,
2260                pair.public(),
2261                Default::default(),
2262                BTreeMap::from_iter(vec![(
2263                    nominator_account,
2264                    (nominator_free_balance, nominator_total_stake),
2265                )]),
2266            );
2267
2268            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2269            let total_current_stake = domain_stake_summary.current_total_stake;
2270
2271            // deactivate operator
2272            let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2273            assert_ok!(res);
2274
2275            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2276            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2277            assert!(
2278                !domain_stake_summary
2279                    .current_operators
2280                    .contains_key(&operator_id)
2281            );
2282
2283            let current_epoch_index = domain_stake_summary.current_epoch_index;
2284            let reactivation_delay =
2285                current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2286            let operator = Operators::<Test>::get(operator_id).unwrap();
2287            assert_eq!(
2288                *operator.status::<Test>(operator_id),
2289                OperatorStatus::Deactivated(reactivation_delay)
2290            );
2291
2292            let operator_stake = operator.current_total_stake;
2293            assert_eq!(
2294                total_current_stake,
2295                domain_stake_summary.current_total_stake + operator_stake
2296            );
2297
2298            // operator should be part of the DeactivatedOperator storage
2299            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2300
2301            // transition epoch
2302            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2303
2304            // operator not should be part of the DeactivatedOperator storage after epoch transition
2305            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2306
2307            // nominator withdraw should work even though operator is deactivated
2308            let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2309            let res = Domains::withdraw_stake(
2310                RuntimeOrigin::signed(nominator_account),
2311                operator_id,
2312                nominator_shares,
2313            );
2314            assert_ok!(res);
2315
2316            // operator should be part of the DeactivatedOperator storage since there is a new
2317            // withdrawal and share prices needs to be calculated.
2318            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2319
2320            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2321            let current_epoch_index = domain_stake_summary.current_epoch_index;
2322
2323            // transition epoch
2324            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2325
2326            // share price should exist for previous epoch
2327            let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2328            assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2329
2330            let nominator_withdrawals =
2331                Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
2332            let unlock_funds_at = nominator_withdrawals
2333                .withdrawal_in_shares
2334                .unwrap()
2335                .unlock_at_confirmed_domain_block_number;
2336            HeadDomainNumber::<Test>::insert(domain_id, unlock_funds_at + 1);
2337
2338            // unlocking nominator funds should work when operator is still deactivated
2339            let res = Domains::unlock_funds(RuntimeOrigin::signed(nominator_account), operator_id);
2340            assert_ok!(res);
2341        });
2342    }
2343
2344    #[test]
2345    fn operator_deactivation_deregister_operator() {
2346        let domain_id = DomainId::new(0);
2347        let operator_account = 1;
2348        let operator_stake = 200 * AI3;
2349        let operator_free_balance = 250 * AI3;
2350        let pair = OperatorPair::from_seed(&[0; 32]);
2351        let nominator_account = 100;
2352        let nominator_free_balance = 150 * AI3;
2353        let nominator_total_stake = 100 * AI3;
2354        let mut ext = new_test_ext();
2355        ext.execute_with(|| {
2356            let (operator_id, _) = register_operator(
2357                domain_id,
2358                operator_account,
2359                operator_free_balance,
2360                operator_stake,
2361                AI3,
2362                pair.public(),
2363                Default::default(),
2364                BTreeMap::from_iter(vec![(
2365                    nominator_account,
2366                    (nominator_free_balance, nominator_total_stake),
2367                )]),
2368            );
2369
2370            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2371            let total_current_stake = domain_stake_summary.current_total_stake;
2372
2373            // deactivate operator
2374            let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2375            assert_ok!(res);
2376
2377            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2378            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2379            assert!(
2380                !domain_stake_summary
2381                    .current_operators
2382                    .contains_key(&operator_id)
2383            );
2384
2385            let current_epoch_index = domain_stake_summary.current_epoch_index;
2386            let reactivation_delay =
2387                current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2388            let operator = Operators::<Test>::get(operator_id).unwrap();
2389            assert_eq!(
2390                *operator.status::<Test>(operator_id),
2391                OperatorStatus::Deactivated(reactivation_delay)
2392            );
2393
2394            let operator_stake = operator.current_total_stake;
2395            assert_eq!(
2396                total_current_stake,
2397                domain_stake_summary.current_total_stake + operator_stake
2398            );
2399
2400            // operator should be part of the DeactivatedOperator storage
2401            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2402
2403            // transition epoch
2404            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2405
2406            // operator not should be part of the DeactivatedOperator storage after epoch transition
2407            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2408
2409            // nominator withdraw should work even though operator is deactivated
2410            let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2411            let res = Domains::withdraw_stake(
2412                RuntimeOrigin::signed(nominator_account),
2413                operator_id,
2414                nominator_shares,
2415            );
2416            assert_ok!(res);
2417
2418            // operator should be part of the DeactivatedOperator storage since there is a new
2419            // withdrawal and share prices needs to be calculated.
2420            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2421
2422            // operator deregistration should work even if operator is deactivated
2423            let res =
2424                Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2425            assert_ok!(res);
2426
2427            // operator not should be part of the DeactivatedOperator storage since operator is
2428            // deregistered but instead should be part of DeregisteredOperators storage
2429            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2430            assert!(DeregisteredOperators::<Test>::get(domain_id).contains(&operator_id));
2431
2432            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2433            let current_epoch_index = domain_stake_summary.current_epoch_index;
2434
2435            // transition epoch
2436            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2437
2438            // share price should exist for previous epoch
2439            let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2440            assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2441
2442            let unlock_operator_at = HeadDomainNumber::<Test>::get(domain_id)
2443                + <Test as Config>::StakeWithdrawalLockingPeriod::get();
2444
2445            let operator = Operators::<Test>::get(operator_id).unwrap();
2446            assert_eq!(
2447                operator.partial_status,
2448                OperatorStatus::Deregistered(OperatorDeregisteredInfo {
2449                    domain_epoch,
2450                    unlock_at_confirmed_domain_block_number: unlock_operator_at
2451                })
2452            );
2453
2454            HeadDomainNumber::<Test>::insert(domain_id, unlock_operator_at);
2455
2456            // unlocking nominator should work
2457            let res =
2458                Domains::unlock_nominator(RuntimeOrigin::signed(nominator_account), operator_id);
2459            assert_ok!(res);
2460
2461            // unlocking operator also should work
2462            let res =
2463                Domains::unlock_nominator(RuntimeOrigin::signed(operator_account), operator_id);
2464            assert_ok!(res);
2465
2466            // cleanup of operator should be done
2467            assert!(OperatorIdOwner::<Test>::get(operator_id).is_none());
2468        });
2469    }
2470
2471    #[test]
2472    fn operator_reactivation() {
2473        let domain_id = DomainId::new(0);
2474        let operator_account = 1;
2475        let operator_stake = 200 * AI3;
2476        let operator_free_balance = 250 * AI3;
2477        let nominator_account = 100;
2478        let nominator_free_balance = 150 * AI3;
2479        let nominator_total_stake = 100 * AI3;
2480        let pair = OperatorPair::from_seed(&[0; 32]);
2481        let mut ext = new_test_ext();
2482        ext.execute_with(|| {
2483            let (operator_id, _) = register_operator(
2484                domain_id,
2485                operator_account,
2486                operator_free_balance,
2487                operator_stake,
2488                AI3,
2489                pair.public(),
2490                Default::default(),
2491                BTreeMap::from_iter(vec![(
2492                    nominator_account,
2493                    (nominator_free_balance, nominator_total_stake),
2494                )]),
2495            );
2496
2497            let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2498            assert_ok!(res);
2499
2500            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2501            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2502            assert!(
2503                !domain_stake_summary
2504                    .current_operators
2505                    .contains_key(&operator_id)
2506            );
2507
2508            let current_epoch_index = domain_stake_summary.current_epoch_index;
2509            let reactivation_delay =
2510                current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2511            let operator = Operators::<Test>::get(operator_id).unwrap();
2512            assert_eq!(
2513                *operator.status::<Test>(operator_id),
2514                OperatorStatus::Deactivated(reactivation_delay)
2515            );
2516
2517            // reactivation should not work before cool off period
2518            let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2519            assert_err!(
2520                res,
2521                Error::<Test>::Staking(crate::staking::Error::ReactivationDelayPeriodIncomplete)
2522            );
2523
2524            for expected_epoch in (current_epoch_index + 1)..=reactivation_delay {
2525                do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2526                let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2527                assert_eq!(domain_stake_summary.current_epoch_index, expected_epoch);
2528            }
2529
2530            // operator not should be part of the DeactivatedOperator storage
2531            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2532
2533            // nominator withdraw should work even though operator is deactivated
2534            let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2535            let res = Domains::withdraw_stake(
2536                RuntimeOrigin::signed(nominator_account),
2537                operator_id,
2538                nominator_shares,
2539            );
2540            assert_ok!(res);
2541
2542            // operator should be part of the DeactivatedOperator storage since there is a withdrawal
2543            assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2544
2545            let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2546            assert_ok!(res);
2547
2548            // operator not should be part of the DeactivatedOperator storage since operator is
2549            // reactivated and moved to next operator set
2550            assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2551
2552            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2553            assert!(domain_stake_summary.next_operators.contains(&operator_id));
2554
2555            let operator = Operators::<Test>::get(operator_id).unwrap();
2556            assert_eq!(
2557                *operator.status::<Test>(operator_id),
2558                OperatorStatus::Registered
2559            );
2560        });
2561    }
2562
2563    type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2564
2565    /// Expected withdrawal amount.
2566    /// Bool indicates to include existential deposit while asserting the final balance
2567    /// since ED is not holded back from usable balance when there are no holds on the account.
2568    type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2569
2570    /// The storage fund change in AI3, `true` means increase of the storage fund, `false` means decrease.
2571    type StorageFundChange = (bool, u32);
2572
2573    pub(crate) type Share = <Test as Config>::Share;
2574
2575    struct WithdrawParams {
2576        /// The minimum valid nominator stake.
2577        minimum_nominator_stake: BalanceOf<Test>,
2578        /// The nominator IDs and their stakes.
2579        /// Account 0 is the operator and its stake.
2580        nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2581        /// The operator reward.
2582        operator_reward: BalanceOf<Test>,
2583        /// The nominator ID to withdraw from.
2584        nominator_id: NominatorId<Test>,
2585        /// The withdraw attempts to be made, in order, with their expected results.
2586        withdraws: WithdrawWithResult,
2587        /// The deposit to be made when nominating the operator, if any.
2588        maybe_deposit: Option<BalanceOf<Test>>,
2589        /// The expected withdraw amount for `nominator_id`.
2590        /// Includes the existential deposit if `true`.
2591        expected_withdraw: ExpectedWithdrawAmount,
2592        /// The expected reduction in the number of nominators.
2593        expected_nominator_count_reduced_by: u32,
2594        /// The storage fund change, increase if `true`.
2595        storage_fund_change: StorageFundChange,
2596    }
2597
2598    /// Withdraw stake in a non-proptest.
2599    fn withdraw_stake(params: WithdrawParams) {
2600        withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2601    }
2602
2603    /// Withdraw stake in a proptest.
2604    fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2605        withdraw_stake_inner(params, true)
2606    }
2607
2608    /// Inner function for withdrawing stake.
2609    fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2610        let WithdrawParams {
2611            minimum_nominator_stake,
2612            nominators,
2613            operator_reward,
2614            nominator_id,
2615            withdraws,
2616            maybe_deposit,
2617            expected_withdraw,
2618            expected_nominator_count_reduced_by,
2619            storage_fund_change,
2620        } = params;
2621        let domain_id = DomainId::new(0);
2622        let operator_account = 0;
2623        let pair = OperatorPair::from_seed(&[0; 32]);
2624        let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2625            + operator_reward
2626            + maybe_deposit.unwrap_or(0);
2627
2628        let mut nominators = BTreeMap::from_iter(
2629            nominators
2630                .into_iter()
2631                .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2632                .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2633        );
2634
2635        let mut ext = new_test_ext();
2636        ext.execute_with(|| {
2637            let (operator_free_balance, operator_stake) =
2638                nominators.remove(&operator_account).unwrap();
2639            let (operator_id, _) = register_operator(
2640                domain_id,
2641                operator_account,
2642                operator_free_balance,
2643                operator_stake,
2644                minimum_nominator_stake,
2645                pair.public(),
2646                Default::default(),
2647                nominators,
2648            );
2649
2650            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2651
2652            if !operator_reward.is_zero() {
2653                do_reward_operators::<Test>(
2654                    domain_id,
2655                    OperatorRewardSource::Dummy,
2656                    vec![operator_id].into_iter(),
2657                    operator_reward,
2658                )
2659                .unwrap();
2660            }
2661
2662            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2663
2664            if let Some(deposit_amount) = maybe_deposit {
2665                Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2666                let res = Domains::nominate_operator(
2667                    RuntimeOrigin::signed(nominator_id),
2668                    operator_id,
2669                    deposit_amount,
2670                );
2671                assert_ok!(res);
2672            }
2673
2674            let operator = Operators::<Test>::get(operator_id).unwrap();
2675            let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2676            if is_storage_fund_increased {
2677                bundle_storage_fund::refund_storage_fee::<Test>(
2678                    storage_fund_change_amount as u128 * AI3,
2679                    BTreeMap::from_iter([(operator_id, 1)]),
2680                )
2681                .unwrap();
2682                assert_eq!(
2683                    operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2684                    bundle_storage_fund::total_balance::<Test>(operator_id)
2685                );
2686                total_balance += storage_fund_change_amount as u128 * AI3;
2687            } else {
2688                bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2689                    operator_id,
2690                    storage_fund_change_amount,
2691                )
2692                .unwrap();
2693                assert_eq!(
2694                    operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2695                    bundle_storage_fund::total_balance::<Test>(operator_id)
2696                );
2697                total_balance -= storage_fund_change_amount as u128 * AI3;
2698            }
2699
2700            for (withdraw, expected_result) in withdraws {
2701                let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2702                let res = Domains::withdraw_stake(
2703                    RuntimeOrigin::signed(nominator_id),
2704                    operator_id,
2705                    withdraw_share_amount,
2706                )
2707                .map(|_| ());
2708                if is_proptest {
2709                    prop_assert_eq!(
2710                        res,
2711                        expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2712                        "unexpected withdraw_stake result",
2713                    );
2714                } else {
2715                    assert_eq!(
2716                        res,
2717                        expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2718                        "unexpected withdraw_stake result",
2719                    );
2720                }
2721            }
2722
2723            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2724
2725            if let Some((withdraw, include_ed)) = expected_withdraw {
2726                let previous_usable_balance = Balances::usable_balance(nominator_id);
2727
2728                // Update `HeadDomainNumber` to ensure unlock success
2729                HeadDomainNumber::<Test>::set(
2730                    domain_id,
2731                    head_domain_number
2732                        + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2733                );
2734                assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2735
2736                let expected_balance = if include_ed {
2737                    total_balance += crate::tests::ExistentialDeposit::get();
2738                    previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get()
2739                } else {
2740                    previous_usable_balance + withdraw
2741                };
2742
2743                if is_proptest {
2744                    prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2745                } else {
2746                    assert_eq!(
2747                        Balances::usable_balance(nominator_id),
2748                        expected_balance,
2749                        "usable balance is not equal to expected balance:\n{} !=\n{}",
2750                        Balances::usable_balance(nominator_id),
2751                        expected_balance,
2752                    );
2753                }
2754
2755                // ensure there are no withdrawals left
2756                assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2757            }
2758
2759            // if the nominator count reduced, then there should be no storage for deposits as well
2760            // TODO: assert this matches the change in the number of nominators
2761            if expected_nominator_count_reduced_by > 0 {
2762                if is_proptest {
2763                    prop_assert!(
2764                        Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2765                        "deposit exists for nominator: {nominator_id}",
2766                    );
2767                    prop_assert!(
2768                        !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2769                        "deposit on hold exists for nominator: {nominator_id}",
2770                    );
2771                } else {
2772                    assert!(
2773                        Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2774                        "deposit exists for nominator: {nominator_id}",
2775                    );
2776                    assert!(
2777                        !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2778                        "deposit on hold exists for nominator: {nominator_id}",
2779                    );
2780                }
2781            }
2782
2783            // The total balance is distributed in different places but never changed.
2784            let operator = Operators::<Test>::get(operator_id).unwrap();
2785            if is_proptest {
2786                prop_assert_approx!(
2787                    Balances::usable_balance(nominator_id)
2788                        + operator.current_total_stake
2789                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2790                    total_balance,
2791                    "\n{} + {} + {} =",
2792                    Balances::usable_balance(nominator_id),
2793                    operator.current_total_stake,
2794                    bundle_storage_fund::total_balance::<Test>(operator_id),
2795                );
2796            } else {
2797                assert_eq!(
2798                    Balances::usable_balance(nominator_id)
2799                        + operator.current_total_stake
2800                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2801                    total_balance,
2802                    "initial total balance is not equal to final total balance:\n\
2803                    {} + {} + {} =\n{} !=\n{}",
2804                    Balances::usable_balance(nominator_id),
2805                    operator.current_total_stake,
2806                    bundle_storage_fund::total_balance::<Test>(operator_id),
2807                    Balances::usable_balance(nominator_id)
2808                        + operator.current_total_stake
2809                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2810                    total_balance,
2811                );
2812            }
2813
2814            Ok(())
2815        })
2816    }
2817
2818    /// Do an approximate amount comparison in a proptest.
2819    /// Takes actual and expected amounts, and an optional extra format string and arguments.
2820    ///
2821    /// The user should never get more, and they shouldn't get significantly less.
2822    /// Accounts for both absolute and relative rounding errors.
2823    macro_rules! prop_assert_approx {
2824            ($actual:expr, $expected:expr $(,)?) => {
2825                prop_assert_approx!($actual, $expected, "");
2826            };
2827
2828            ($actual:expr, $expected:expr, $fmt:expr) => {
2829                prop_assert_approx!($actual, $expected, $fmt,);
2830            };
2831
2832            ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2833                let actual = $actual;
2834                let expected = $expected;
2835                let extra = format!($fmt, $($args)*);
2836                prop_test::proptest::prop_assert!(
2837                    actual <= expected,
2838                    "extra minting: actual amount is greater than expected amount:{}{}\
2839                    \n{} >\
2840                    \n{}",
2841                    if extra.is_empty() { "" } else { "\n" },
2842                    extra,
2843                    actual,
2844                    expected,
2845                );
2846                let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2847                    .mul_floor(expected)
2848                    .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2849                prop_test::proptest::prop_assert!(
2850                    actual >= expected_rounded_down,
2851                    "excess rounding losses: actual amount is less than expected amount \
2852                    rounded down:{}{}\
2853                    \n{} <\
2854                    \n{} (from\
2855                    \n{})",
2856                    if extra.is_empty() { "" } else { "\n" },
2857                    extra,
2858                    actual,
2859                    expected_rounded_down,
2860                    expected,
2861                );
2862            }};
2863        }
2864
2865    // Export the macro for use in other modules (and earlier in this file).
2866    pub(crate) use prop_assert_approx;
2867
2868    /// Rounding down factor for property tests to account for arithmetic precision errors.
2869    /// This factor is used to allow for small rounding errors in calculations.
2870    // Perquintill::from_parts(...).left_from_one(), as a constant.
2871    pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2872        Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2873
2874    /// Absolute rounding error tolerance for property tests.
2875    /// This constant defines the maximum acceptable absolute rounding error in calculations.
2876    pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2877
2878    /// The maximum balance we test for in property tests.
2879    /// This balance should be just below the maximum possible issuance.
2880    ///
2881    /// Limiting the balances avoids arithmetic errors in the withdraw_stake test function, and
2882    /// RemoveLock (likely converted from BalanceOverflow) in the staking functions.
2883    //
2884    // TODO: fix the code so we can get closer to 2^128
2885    pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2886
2887    /// The minimum operator stake we test for in property tests.
2888    // TODO: edit the test harness so we can go as low as MinOperatorStake + 1
2889    pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2890
2891    /// The minimum nominator stake we test for in property tests.
2892    pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2893        <Test as Config>::MinNominatorStake::get() + 1;
2894
2895    /// The range of operator stakes we test for in property tests.
2896    pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2897        MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2898
2899    /// The range of nominator stakes we test for in property tests.
2900    pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2901        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2902
2903    /// The range of operator or nominator deposits we test for in property tests.
2904    // TODO: edit the test harness so we can go as low as zero
2905    pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2906        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2907
2908    /// The range of operator rewards we test for in property tests.
2909    pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2910
2911    /// The range of operator free balances we test for in property tests.
2912    pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2913        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2914
2915    // Using too many random parameters and prop_assume()s can reduce test coverage.
2916    // Try to limit the number of parameters to 3.
2917
2918    /// Property test for withdrawing an operator's excess stake.
2919    /// Their balance should be almost the same before and after.
2920    #[test]
2921    fn prop_withdraw_excess_operator_stake() {
2922        prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2923            let mut excess_stake =
2924                operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2925
2926            // Account for both absolute and relative rounding errors.
2927            excess_stake = Perquintill::from_parts(1)
2928                .left_from_one()
2929                .mul_ceil(excess_stake);
2930            excess_stake -= 1;
2931
2932            prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2933
2934            let expected_withdraw = (excess_stake, false);
2935
2936            withdraw_stake_prop(WithdrawParams {
2937                minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2938                nominators: vec![(0, operator_stake)],
2939                operator_reward: 0,
2940                nominator_id: 0,
2941                withdraws: vec![(excess_stake, Ok(()))],
2942                maybe_deposit: None,
2943                expected_withdraw: Some(expected_withdraw),
2944                expected_nominator_count_reduced_by: 0,
2945                storage_fund_change: (true, 0),
2946            })
2947        });
2948    }
2949
2950    /// Property test for withdrawing all a nominator's excess stake.
2951    /// Their balance should be almost the same before and after.
2952    #[test]
2953    fn prop_withdraw_excess_operator_stake_with_nominator() {
2954        prop_test!(
2955            &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2956            |(operator_stake, nominator_stake)| {
2957                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2958                prop_assert!(
2959                    [operator_stake, nominator_stake]
2960                        .into_iter()
2961                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2962                        .is_some()
2963                );
2964
2965                let expected_withdraw = (nominator_stake, true);
2966
2967                let excess_stake = expected_withdraw
2968                    .0
2969                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
2970
2971                prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2972
2973                withdraw_stake_prop(WithdrawParams {
2974                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2975                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
2976                    operator_reward: 0,
2977                    nominator_id: 1,
2978                    withdraws: vec![(excess_stake, Ok(()))],
2979                    maybe_deposit: None,
2980                    expected_withdraw: Some(expected_withdraw),
2981                    expected_nominator_count_reduced_by: 1,
2982                    storage_fund_change: (true, 0),
2983                })
2984            }
2985        );
2986    }
2987
2988    /// Property test for withdrawing an operator's excess stake with a deposit.
2989    /// Their balance should be almost the same before and after.
2990    #[test]
2991    fn prop_withdraw_excess_operator_stake_with_deposit() {
2992        prop_test!(
2993            &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2994            |(mut operator_stake, maybe_deposit)| {
2995                operator_stake = operator_stake.saturating_add(maybe_deposit);
2996
2997                prop_assert!(
2998                    operator_stake.saturating_sub(maybe_deposit)
2999                        >= <Test as Config>::MinOperatorStake::get(),
3000                    "would cause MinimumOperatorStake error"
3001                );
3002
3003                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3004                prop_assert!(
3005                    [operator_stake, maybe_deposit]
3006                        .into_iter()
3007                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
3008                        .is_some()
3009                );
3010
3011                let expected_withdraw = (
3012                    // TODO: work out how to avoid this multiplication on WithdrawParams.withdraws
3013                    STORAGE_FEE_RESERVE
3014                        .left_from_one()
3015                        .mul_ceil(operator_stake)
3016                        .saturating_sub(maybe_deposit),
3017                    true,
3018                );
3019
3020                let excess_stake = expected_withdraw
3021                    .0
3022                    .saturating_sub(<Test as Config>::MinOperatorStake::get());
3023
3024                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3025
3026                // Avoid ZeroDeposit errors
3027                let maybe_deposit = if maybe_deposit == 0 {
3028                    None
3029                } else {
3030                    Some(maybe_deposit)
3031                };
3032
3033                withdraw_stake_prop(WithdrawParams {
3034                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3035                    nominators: vec![(0, operator_stake)],
3036                    operator_reward: 0,
3037                    nominator_id: 0,
3038                    withdraws: vec![(excess_stake, Ok(()))],
3039                    maybe_deposit,
3040                    expected_withdraw: Some(expected_withdraw),
3041                    expected_nominator_count_reduced_by: 0,
3042                    storage_fund_change: (true, 0),
3043                })
3044            }
3045        );
3046    }
3047
3048    /// Property test for withdrawing an operator's excess stake with a deposit and fixed
3049    /// operator stake.
3050    /// Their balance should be almost the same before and after.
3051    #[test]
3052    fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
3053        prop_test!(
3054            &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
3055            |(mut nominator_stake, maybe_deposit)| {
3056                nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3057
3058                prop_assert!(
3059                    nominator_stake.saturating_sub(maybe_deposit)
3060                        >= <Test as Config>::MinNominatorStake::get(),
3061                    "would cause MinimumNominatorStake error"
3062                );
3063
3064                // MinimumOperatorStake error
3065                let operator_stake = <Test as Config>::MinOperatorStake::get();
3066
3067                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3068                prop_assert!(
3069                    [operator_stake, nominator_stake, maybe_deposit]
3070                        .into_iter()
3071                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
3072                        .is_some()
3073                );
3074
3075                let expected_withdraw = (
3076                    STORAGE_FEE_RESERVE
3077                        .left_from_one()
3078                        .mul_ceil(nominator_stake)
3079                        .saturating_sub(maybe_deposit),
3080                    true,
3081                );
3082
3083                let excess_stake = expected_withdraw
3084                    .0
3085                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
3086
3087                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3088
3089                // Avoid ZeroDeposit errors
3090                let maybe_deposit = if maybe_deposit == 0 {
3091                    None
3092                } else {
3093                    Some(maybe_deposit)
3094                };
3095
3096                withdraw_stake_prop(WithdrawParams {
3097                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3098                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
3099                    operator_reward: 0,
3100                    nominator_id: 1,
3101                    withdraws: vec![(excess_stake, Ok(()))],
3102                    maybe_deposit,
3103                    expected_withdraw: Some(expected_withdraw),
3104                    expected_nominator_count_reduced_by: 0,
3105                    storage_fund_change: (true, 0),
3106                })
3107            }
3108        );
3109    }
3110
3111    /// Property test for withdrawing a nominator's excess stake with a deposit.
3112    /// Their balance should be almost the same before and after.
3113    #[test]
3114    fn prop_withdraw_excess_nominator_stake_with_deposit() {
3115        prop_test!(
3116            &(
3117                PROP_OPERATOR_STAKE_RANGE,
3118                PROP_NOMINATOR_STAKE_RANGE,
3119                PROP_DEPOSIT_RANGE,
3120            ),
3121            |(operator_stake, mut nominator_stake, maybe_deposit)| {
3122                nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3123
3124                prop_assert!(
3125                    nominator_stake.saturating_sub(maybe_deposit)
3126                        >= <Test as Config>::MinNominatorStake::get(),
3127                    "would cause MinimumNominatorStake error"
3128                );
3129
3130                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3131                prop_assert!(
3132                    [operator_stake, nominator_stake, maybe_deposit]
3133                        .into_iter()
3134                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
3135                        .is_some()
3136                );
3137
3138                // Some deposit gets left as storage fees.
3139                let expected_withdraw = (
3140                    STORAGE_FEE_RESERVE
3141                        .left_from_one()
3142                        .mul_ceil(nominator_stake)
3143                        .saturating_sub(maybe_deposit),
3144                    true,
3145                );
3146
3147                let excess_stake = expected_withdraw
3148                    .0
3149                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
3150
3151                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3152
3153                // Avoid ZeroDeposit errors
3154                let maybe_deposit = if maybe_deposit == 0 {
3155                    None
3156                } else {
3157                    Some(maybe_deposit)
3158                };
3159
3160                withdraw_stake_prop(WithdrawParams {
3161                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3162                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
3163                    operator_reward: 0,
3164                    nominator_id: 1,
3165                    withdraws: vec![(excess_stake, Ok(()))],
3166                    maybe_deposit,
3167                    expected_withdraw: Some(expected_withdraw),
3168                    expected_nominator_count_reduced_by: 0,
3169                    storage_fund_change: (true, 0),
3170                })
3171            }
3172        );
3173    }
3174
3175    /// Property test for withdrawing an operator's excess stake with a nominator and a reward.
3176    /// The total balance should be increased by part of the reward afterwards.
3177    #[test]
3178    fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
3179        prop_test!(
3180            &(
3181                PROP_OPERATOR_STAKE_RANGE,
3182                PROP_NOMINATOR_STAKE_RANGE,
3183                PROP_REWARD_RANGE,
3184            ),
3185            |(operator_stake, nominator_stake, operator_reward)| {
3186                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3187                prop_assert!(
3188                    [operator_stake, nominator_stake, operator_reward]
3189                        .into_iter()
3190                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
3191                        .is_some()
3192                );
3193
3194                // Withdraw is stake plus reward split.
3195                let total_stake = operator_stake.saturating_add(nominator_stake);
3196                let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
3197                    .mul_floor(operator_reward);
3198
3199                // Shares are approximately equal to the original stake.
3200                // TODO: fix the tests so we can go as low as MinOperatorStake
3201                let excess_stake =
3202                    operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
3203
3204                let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
3205
3206                prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
3207
3208                withdraw_stake_prop(WithdrawParams {
3209                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3210                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
3211                    operator_reward,
3212                    nominator_id: 0,
3213                    withdraws: vec![(excess_stake, Ok(()))],
3214                    maybe_deposit: None,
3215                    expected_withdraw: Some(expected_withdraw),
3216                    expected_nominator_count_reduced_by: 0,
3217                    storage_fund_change: (true, 0),
3218                })
3219            }
3220        );
3221    }
3222
3223    /// Property test for withdrawing an operator's excess stake with a deposit and a reward.
3224    /// Their balance should be almost the same before and after.
3225    #[test]
3226    fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
3227        prop_test!(
3228            &(
3229                PROP_OPERATOR_STAKE_RANGE,
3230                PROP_DEPOSIT_RANGE,
3231                PROP_REWARD_RANGE,
3232            ),
3233            |(mut operator_stake, maybe_deposit, operator_reward)| {
3234                operator_stake = operator_stake.saturating_add(maybe_deposit);
3235
3236                prop_assert!(
3237                    operator_stake.saturating_sub(maybe_deposit)
3238                        >= <Test as Config>::MinOperatorStake::get(),
3239                    "would cause MinimumOperatorStake error"
3240                );
3241
3242                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
3243                prop_assert!(
3244                    [operator_stake, maybe_deposit, operator_reward]
3245                        .into_iter()
3246                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
3247                        .is_some()
3248                );
3249
3250                // Shares are approximately equal to the original stake.
3251                // TODO: fix the tests so we can go as low as MinOperatorStake
3252                let reserve = 2 * <Test as Config>::MinOperatorStake::get();
3253                let excess_stake = operator_stake.saturating_sub(reserve);
3254
3255                // Withdraw is stake plus reward, but some deposit gets left as storage fees.
3256                let expected_withdraw = (
3257                    operator_stake
3258                        .saturating_add(operator_reward)
3259                        .saturating_sub(reserve),
3260                    true,
3261                );
3262
3263                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3264
3265                // Avoid ZeroDeposit errors
3266                let maybe_deposit = if maybe_deposit == 0 {
3267                    None
3268                } else {
3269                    Some(maybe_deposit)
3270                };
3271
3272                withdraw_stake_prop(WithdrawParams {
3273                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3274                    nominators: vec![(0, operator_stake)],
3275                    operator_reward,
3276                    nominator_id: 0,
3277                    withdraws: vec![(excess_stake, Ok(()))],
3278                    maybe_deposit,
3279                    expected_withdraw: Some(expected_withdraw),
3280                    expected_nominator_count_reduced_by: 0,
3281                    storage_fund_change: (true, 0),
3282                })
3283            }
3284        );
3285    }
3286
3287    #[test]
3288    fn withdraw_stake_operator_all() {
3289        withdraw_stake(WithdrawParams {
3290            minimum_nominator_stake: 10 * AI3,
3291            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3292            operator_reward: 20 * AI3,
3293            nominator_id: 0,
3294            withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
3295            maybe_deposit: None,
3296            expected_withdraw: None,
3297            expected_nominator_count_reduced_by: 0,
3298            storage_fund_change: (true, 0),
3299        })
3300    }
3301
3302    #[test]
3303    fn withdraw_stake_operator_below_minimum() {
3304        withdraw_stake(WithdrawParams {
3305            minimum_nominator_stake: 10 * AI3,
3306            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3307            operator_reward: 20 * AI3,
3308            nominator_id: 0,
3309            withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
3310            maybe_deposit: None,
3311            expected_withdraw: None,
3312            expected_nominator_count_reduced_by: 0,
3313            storage_fund_change: (true, 0),
3314        })
3315    }
3316
3317    #[test]
3318    fn withdraw_stake_operator_below_minimum_no_rewards() {
3319        withdraw_stake(WithdrawParams {
3320            minimum_nominator_stake: 10 * AI3,
3321            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3322            operator_reward: Zero::zero(),
3323            nominator_id: 0,
3324            withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
3325            maybe_deposit: None,
3326            expected_withdraw: None,
3327            expected_nominator_count_reduced_by: 0,
3328            storage_fund_change: (true, 0),
3329        })
3330    }
3331
3332    #[test]
3333    fn withdraw_stake_operator_above_minimum() {
3334        withdraw_stake(WithdrawParams {
3335            minimum_nominator_stake: 10 * AI3,
3336            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3337            operator_reward: 20 * AI3,
3338            nominator_id: 0,
3339            withdraws: vec![(58 * AI3, Ok(()))],
3340            // given the reward, operator will get 164.28 AI3
3341            // taking 58 shares will give this following approximate amount.
3342            maybe_deposit: None,
3343            expected_withdraw: Some((63523809523809523770, false)),
3344            expected_nominator_count_reduced_by: 0,
3345            storage_fund_change: (true, 0),
3346        })
3347    }
3348
3349    #[test]
3350    fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
3351        withdraw_stake(WithdrawParams {
3352            minimum_nominator_stake: 10 * AI3,
3353            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3354            operator_reward: 20 * AI3,
3355            nominator_id: 0,
3356            withdraws: vec![
3357                (58 * AI3, Ok(())),
3358                (5 * AI3, Err(StakingError::MinimumOperatorStake)),
3359            ],
3360            maybe_deposit: None,
3361            expected_withdraw: Some((63523809523809523770, false)),
3362            expected_nominator_count_reduced_by: 0,
3363            storage_fund_change: (true, 0),
3364        })
3365    }
3366
3367    #[test]
3368    fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
3369        withdraw_stake(WithdrawParams {
3370            minimum_nominator_stake: 10 * AI3,
3371            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3372            operator_reward: 20 * AI3,
3373            nominator_id: 0,
3374            withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
3375            maybe_deposit: None,
3376            expected_withdraw: Some((63523809523809523769, false)),
3377            expected_nominator_count_reduced_by: 0,
3378            storage_fund_change: (true, 0),
3379        })
3380    }
3381
3382    #[test]
3383    fn withdraw_stake_operator_above_minimum_no_rewards() {
3384        withdraw_stake(WithdrawParams {
3385            minimum_nominator_stake: 10 * AI3,
3386            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3387            operator_reward: Zero::zero(),
3388            nominator_id: 0,
3389            withdraws: vec![(49 * AI3, Ok(()))],
3390            maybe_deposit: None,
3391            expected_withdraw: Some((48999999999999999980, false)),
3392            expected_nominator_count_reduced_by: 0,
3393            storage_fund_change: (true, 0),
3394        })
3395    }
3396
3397    #[test]
3398    fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
3399        withdraw_stake(WithdrawParams {
3400            minimum_nominator_stake: 10 * AI3,
3401            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3402            operator_reward: Zero::zero(),
3403            nominator_id: 0,
3404            withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
3405            maybe_deposit: None,
3406            expected_withdraw: Some((48999999999999999981, false)),
3407            expected_nominator_count_reduced_by: 0,
3408            storage_fund_change: (true, 0),
3409        })
3410    }
3411
3412    #[test]
3413    fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
3414        withdraw_stake(WithdrawParams {
3415            minimum_nominator_stake: 10 * AI3,
3416            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3417            operator_reward: Zero::zero(),
3418            nominator_id: 0,
3419            withdraws: vec![
3420                (29 * AI3, Ok(())),
3421                (20 * AI3, Ok(())),
3422                (20 * AI3, Err(StakingError::MinimumOperatorStake)),
3423            ],
3424            maybe_deposit: None,
3425            expected_withdraw: Some((48999999999999999981, false)),
3426            expected_nominator_count_reduced_by: 0,
3427            storage_fund_change: (true, 0),
3428        })
3429    }
3430
3431    #[test]
3432    fn withdraw_stake_nominator_below_minimum_with_rewards() {
3433        withdraw_stake(WithdrawParams {
3434            minimum_nominator_stake: 10 * AI3,
3435            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3436            operator_reward: 20 * AI3,
3437            nominator_id: 1,
3438            withdraws: vec![(45 * AI3, Ok(()))],
3439            // given nominator remaining stake goes below minimum
3440            // we withdraw everything, so for their 50 shares with reward,
3441            // price would be following
3442            maybe_deposit: None,
3443            expected_withdraw: Some((54761904761904761888, true)),
3444            expected_nominator_count_reduced_by: 1,
3445            storage_fund_change: (true, 0),
3446        })
3447    }
3448
3449    #[test]
3450    fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
3451        withdraw_stake(WithdrawParams {
3452            minimum_nominator_stake: 10 * AI3,
3453            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3454            operator_reward: 20 * AI3,
3455            nominator_id: 1,
3456            withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3457            // given nominator remaining stake goes below minimum
3458            // we withdraw everything, so for their 50 shares with reward,
3459            // price would be following
3460            maybe_deposit: None,
3461            expected_withdraw: Some((54761904761904761888, true)),
3462            expected_nominator_count_reduced_by: 1,
3463            storage_fund_change: (true, 0),
3464        })
3465    }
3466
3467    #[test]
3468    fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
3469        withdraw_stake(WithdrawParams {
3470            minimum_nominator_stake: 10 * AI3,
3471            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3472            operator_reward: 20 * AI3,
3473            nominator_id: 1,
3474            withdraws: vec![
3475                (25 * AI3, Ok(())),
3476                (20 * AI3, Ok(())),
3477                (20 * AI3, Err(StakingError::InsufficientShares)),
3478            ],
3479            // given nominator remaining stake goes below minimum
3480            // we withdraw everything, so for their 50 shares with reward,
3481            // price would be following
3482            maybe_deposit: None,
3483            expected_withdraw: Some((54761904761904761888, true)),
3484            expected_nominator_count_reduced_by: 1,
3485            storage_fund_change: (true, 0),
3486        })
3487    }
3488
3489    #[test]
3490    fn withdraw_stake_nominator_below_minimum_no_reward() {
3491        withdraw_stake(WithdrawParams {
3492            minimum_nominator_stake: 10 * AI3,
3493            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3494            operator_reward: Zero::zero(),
3495            nominator_id: 1,
3496            withdraws: vec![(45 * AI3, Ok(()))],
3497            maybe_deposit: None,
3498            expected_withdraw: Some((50 * AI3, true)),
3499            expected_nominator_count_reduced_by: 1,
3500            storage_fund_change: (true, 0),
3501        })
3502    }
3503
3504    #[test]
3505    fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
3506        withdraw_stake(WithdrawParams {
3507            minimum_nominator_stake: 10 * AI3,
3508            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3509            operator_reward: Zero::zero(),
3510            nominator_id: 1,
3511            withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3512            maybe_deposit: None,
3513            expected_withdraw: Some((50 * AI3, true)),
3514            expected_nominator_count_reduced_by: 1,
3515            storage_fund_change: (true, 0),
3516        })
3517    }
3518
3519    #[test]
3520    fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
3521        withdraw_stake(WithdrawParams {
3522            minimum_nominator_stake: 10 * AI3,
3523            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3524            operator_reward: Zero::zero(),
3525            nominator_id: 1,
3526            withdraws: vec![
3527                (25 * AI3, Ok(())),
3528                (20 * AI3, Ok(())),
3529                (20 * AI3, Err(StakingError::InsufficientShares)),
3530            ],
3531            maybe_deposit: None,
3532            expected_withdraw: Some((50 * AI3, true)),
3533            expected_nominator_count_reduced_by: 1,
3534            storage_fund_change: (true, 0),
3535        })
3536    }
3537
3538    #[test]
3539    fn withdraw_stake_nominator_above_minimum() {
3540        withdraw_stake(WithdrawParams {
3541            minimum_nominator_stake: 10 * AI3,
3542            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3543            operator_reward: 20 * AI3,
3544            nominator_id: 1,
3545            withdraws: vec![(40 * AI3, Ok(()))],
3546            maybe_deposit: None,
3547            expected_withdraw: Some((43809523809523809511, false)),
3548            expected_nominator_count_reduced_by: 0,
3549            storage_fund_change: (true, 0),
3550        })
3551    }
3552
3553    #[test]
3554    fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
3555        withdraw_stake(WithdrawParams {
3556            minimum_nominator_stake: 10 * AI3,
3557            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3558            operator_reward: 20 * AI3,
3559            nominator_id: 1,
3560            withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3561            maybe_deposit: None,
3562            expected_withdraw: Some((43809523809523809510, false)),
3563            expected_nominator_count_reduced_by: 0,
3564            storage_fund_change: (true, 0),
3565        })
3566    }
3567
3568    #[test]
3569    fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3570        withdraw_stake(WithdrawParams {
3571            minimum_nominator_stake: 10 * AI3,
3572            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3573            operator_reward: 20 * AI3,
3574            nominator_id: 1,
3575            withdraws: vec![
3576                (35 * AI3, Ok(())),
3577                (5 * AI3, Ok(())),
3578                (15 * AI3, Err(StakingError::InsufficientShares)),
3579            ],
3580            maybe_deposit: None,
3581            expected_withdraw: Some((43809523809523809510, false)),
3582            expected_nominator_count_reduced_by: 0,
3583            storage_fund_change: (true, 0),
3584        })
3585    }
3586
3587    #[test]
3588    fn withdraw_stake_nominator_above_minimum_no_rewards() {
3589        withdraw_stake(WithdrawParams {
3590            minimum_nominator_stake: 10 * AI3,
3591            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3592            operator_reward: Zero::zero(),
3593            nominator_id: 1,
3594            withdraws: vec![(39 * AI3, Ok(()))],
3595            maybe_deposit: None,
3596            expected_withdraw: Some((39 * AI3, false)),
3597            expected_nominator_count_reduced_by: 0,
3598            storage_fund_change: (true, 0),
3599        })
3600    }
3601
3602    #[test]
3603    fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3604        withdraw_stake(WithdrawParams {
3605            minimum_nominator_stake: 10 * AI3,
3606            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3607            operator_reward: Zero::zero(),
3608            nominator_id: 1,
3609            withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3610            maybe_deposit: None,
3611            expected_withdraw: Some((39999999899999999998, false)),
3612            expected_nominator_count_reduced_by: 0,
3613            storage_fund_change: (true, 0),
3614        })
3615    }
3616
3617    #[test]
3618    fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3619        withdraw_stake(WithdrawParams {
3620            minimum_nominator_stake: 10 * AI3,
3621            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3622            operator_reward: Zero::zero(),
3623            nominator_id: 1,
3624            withdraws: vec![
3625                (35 * AI3, Ok(())),
3626                (5 * AI3 - 100000000000, Ok(())),
3627                (15 * AI3, Err(StakingError::InsufficientShares)),
3628            ],
3629            maybe_deposit: None,
3630            expected_withdraw: Some((39999999899999999998, false)),
3631            expected_nominator_count_reduced_by: 0,
3632            storage_fund_change: (true, 0),
3633        })
3634    }
3635
3636    #[test]
3637    fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3638        withdraw_stake(WithdrawParams {
3639            minimum_nominator_stake: 10 * AI3,
3640            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3641            operator_reward: Zero::zero(),
3642            nominator_id: 1,
3643            withdraws: vec![
3644                (35 * AI3, Ok(())),
3645                (5 * AI3 - 100000000000, Ok(())),
3646                (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3647            ],
3648            maybe_deposit: Some(2 * AI3),
3649            expected_withdraw: Some((39999999899999999998, false)),
3650            expected_nominator_count_reduced_by: 0,
3651            storage_fund_change: (true, 0),
3652        })
3653    }
3654
3655    #[test]
3656    fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3657        withdraw_stake(WithdrawParams {
3658            minimum_nominator_stake: 10 * AI3,
3659            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3660            operator_reward: 20 * AI3,
3661            nominator_id: 1,
3662            withdraws: vec![
3663                (35 * AI3, Ok(())),
3664                (5 * AI3, Ok(())),
3665                (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3666            ],
3667            // given nominator remaining stake goes below minimum
3668            // we withdraw everything, so for their 50 shares with reward,
3669            // price would be following
3670            maybe_deposit: Some(2 * AI3),
3671            expected_withdraw: Some((43809523809523809510, false)),
3672            expected_nominator_count_reduced_by: 0,
3673            storage_fund_change: (true, 0),
3674        })
3675    }
3676
3677    #[test]
3678    fn withdraw_stake_nominator_zero_amount() {
3679        withdraw_stake(WithdrawParams {
3680            minimum_nominator_stake: 10 * AI3,
3681            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3682            operator_reward: Zero::zero(),
3683            nominator_id: 1,
3684            withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3685            maybe_deposit: None,
3686            expected_withdraw: None,
3687            expected_nominator_count_reduced_by: 0,
3688            storage_fund_change: (true, 0),
3689        })
3690    }
3691
3692    #[test]
3693    fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3694        withdraw_stake(WithdrawParams {
3695            minimum_nominator_stake: 10 * AI3,
3696            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3697            operator_reward: Zero::zero(),
3698            nominator_id: 1,
3699            withdraws: vec![(50 * AI3, Ok(()))],
3700            maybe_deposit: None,
3701            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3702            // storage fee profit i.e. 5 * AI3 with rounding dust deducted
3703            storage_fund_change: (true, 21),
3704            expected_withdraw: Some((54999999999999999985, true)),
3705            expected_nominator_count_reduced_by: 1,
3706        })
3707    }
3708
3709    #[test]
3710    fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3711        withdraw_stake(WithdrawParams {
3712            minimum_nominator_stake: 10 * AI3,
3713            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3714            operator_reward: Zero::zero(),
3715            nominator_id: 1,
3716            withdraws: vec![(50 * AI3, Ok(()))],
3717            maybe_deposit: None,
3718            // The storage fund decreased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3719            // storage fee deposit i.e. 5 * AI3 with rounding dust deducted
3720            storage_fund_change: (false, 21),
3721            expected_withdraw: Some((44999999999999999995, true)),
3722            expected_nominator_count_reduced_by: 1,
3723        })
3724    }
3725
3726    #[test]
3727    fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3728        withdraw_stake(WithdrawParams {
3729            minimum_nominator_stake: 10 * AI3,
3730            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3731            operator_reward: Zero::zero(),
3732            nominator_id: 1,
3733            withdraws: vec![(50 * AI3, Ok(()))],
3734            maybe_deposit: None,
3735            // The storage fund decreased 100% (i.e. 42 * AI3) thus the nominator loss 100%
3736            // storage fee deposit i.e. 10 * AI3
3737            storage_fund_change: (false, 42),
3738            expected_withdraw: Some((40 * AI3, true)),
3739            expected_nominator_count_reduced_by: 1,
3740        })
3741    }
3742
3743    #[test]
3744    fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3745        withdraw_stake(WithdrawParams {
3746            minimum_nominator_stake: 10 * AI3,
3747            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3748            operator_reward: Zero::zero(),
3749            nominator_id: 1,
3750            withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3751            maybe_deposit: None,
3752            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3753            // storage fee profit i.e. 5 * AI3 with rounding dust deducted, withdraw 60% of
3754            // the stake and the storage fee profit
3755            storage_fund_change: (true, 21),
3756            expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3757            expected_nominator_count_reduced_by: 0,
3758        })
3759    }
3760
3761    #[test]
3762    fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3763        withdraw_stake(WithdrawParams {
3764            minimum_nominator_stake: 10 * AI3,
3765            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3766            operator_reward: Zero::zero(),
3767            nominator_id: 1,
3768            withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3769            maybe_deposit: None,
3770            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3771            // storage fee i.e. 5 * AI3 with rounding dust deducted, withdraw 40% of
3772            // the stake and 40% of the storage fee loss are deducted
3773            storage_fund_change: (false, 21),
3774            expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3775            expected_nominator_count_reduced_by: 0,
3776        })
3777    }
3778
3779    #[test]
3780    fn unlock_multiple_withdrawals() {
3781        let domain_id = DomainId::new(0);
3782        let operator_account = 1;
3783        let operator_free_balance = 250 * AI3;
3784        let operator_stake = 200 * AI3;
3785        let pair = OperatorPair::from_seed(&[0; 32]);
3786        let nominator_account = 2;
3787        let nominator_free_balance = 150 * AI3;
3788        let nominator_stake = 100 * AI3;
3789
3790        let nominators = vec![
3791            (operator_account, (operator_free_balance, operator_stake)),
3792            (nominator_account, (nominator_free_balance, nominator_stake)),
3793        ];
3794
3795        let total_deposit = 300 * AI3;
3796        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3797        let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3798
3799        let mut ext = new_test_ext();
3800        ext.execute_with(|| {
3801            let (operator_id, _) = register_operator(
3802                domain_id,
3803                operator_account,
3804                operator_free_balance,
3805                operator_stake,
3806                10 * AI3,
3807                pair.public(),
3808                Default::default(),
3809                BTreeMap::from_iter(nominators),
3810            );
3811
3812            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3813            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3814            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3815
3816            let operator = Operators::<Test>::get(operator_id).unwrap();
3817            assert_eq!(operator.current_total_stake, init_total_stake);
3818            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3819            assert_eq!(
3820                operator.total_storage_fee_deposit,
3821                bundle_storage_fund::total_balance::<Test>(operator_id)
3822            );
3823
3824            // Guess that the number of shares will be approximately the same as the stake amount.
3825            let shares_per_withdraw = init_total_stake / 100;
3826            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3827
3828            // Request `WithdrawalLimit - 1` number of withdrawal
3829            for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3830                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3831                    .unwrap();
3832                do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3833            }
3834            // Increase the head domain number by 1
3835            HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3836
3837            // All withdrawals of a given nominator submitted in the same epoch will merge into one,
3838            // so we can submit as many as we want, even though the withdrawal limit is met.
3839            for _ in 0..5 {
3840                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3841                    .unwrap();
3842            }
3843            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3844
3845            // After the withdrawal limit is met, any new withdraw will be rejected in the next epoch
3846            assert_err!(
3847                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3848                StakingError::TooManyWithdrawals
3849            );
3850            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3851            Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3852                let withdrawal = maybe_withdrawal.as_mut().unwrap();
3853                do_convert_previous_epoch_withdrawal::<Test>(
3854                    operator_id,
3855                    withdrawal,
3856                    domain_stake_summary.current_epoch_index,
3857                )
3858                .unwrap();
3859                assert_eq!(
3860                    withdrawal.withdrawals.len() as u32,
3861                    <Test as crate::Config>::WithdrawalLimit::get()
3862                );
3863                Ok::<(), StakingError>(())
3864            })
3865            .unwrap();
3866
3867            // Make the first set of withdrawals pass the unlock period then unlock fund
3868            HeadDomainNumber::<Test>::set(
3869                domain_id,
3870                head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3871            );
3872            let total_balance = Balances::usable_balance(nominator_account);
3873            assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3874            assert_eq!(
3875                Balances::usable_balance(nominator_account) + 74, // `74` is a minor rounding dust
3876                total_balance
3877                    + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3878                        / 100
3879            );
3880            let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3881            assert_eq!(withdrawal.withdrawals.len(), 1);
3882
3883            // Make the second set of withdrawals pass the unlock period then unlock funds
3884            HeadDomainNumber::<Test>::set(
3885                domain_id,
3886                head_domain_number
3887                    + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3888                    + 1,
3889            );
3890            let total_balance = Balances::usable_balance(nominator_account);
3891            assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3892            assert_eq!(
3893                Balances::usable_balance(nominator_account) - 2, // `2` is a minor rounding dust
3894                total_balance + 5 * total_deposit / 100
3895            );
3896            assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3897        });
3898    }
3899
3900    #[test]
3901    fn slash_operator() {
3902        let domain_id = DomainId::new(0);
3903        let operator_account = 1;
3904        let operator_free_balance = 250 * AI3;
3905        let operator_stake = 200 * AI3;
3906        let operator_extra_deposit = 40 * AI3;
3907        let pair = OperatorPair::from_seed(&[0; 32]);
3908        let nominator_account = 2;
3909        let nominator_free_balance = 150 * AI3;
3910        let nominator_stake = 100 * AI3;
3911        let nominator_extra_deposit = 40 * AI3;
3912
3913        let nominators = vec![
3914            (operator_account, (operator_free_balance, operator_stake)),
3915            (nominator_account, (nominator_free_balance, nominator_stake)),
3916        ];
3917
3918        let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3919
3920        let deposits = vec![
3921            (operator_account, operator_extra_deposit),
3922            (nominator_account, nominator_extra_deposit),
3923        ];
3924
3925        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3926        let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3927
3928        let mut ext = new_test_ext();
3929        ext.execute_with(|| {
3930            let (operator_id, _) = register_operator(
3931                domain_id,
3932                operator_account,
3933                operator_free_balance,
3934                operator_stake,
3935                10 * AI3,
3936                pair.public(),
3937                Default::default(),
3938                BTreeMap::from_iter(nominators),
3939            );
3940
3941            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3942            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3943            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3944
3945            let operator = Operators::<Test>::get(operator_id).unwrap();
3946            assert_eq!(operator.current_total_stake, init_total_stake);
3947            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3948            assert_eq!(
3949                operator.total_storage_fee_deposit,
3950                bundle_storage_fund::total_balance::<Test>(operator_id)
3951            );
3952
3953            for unlock in &unlocking {
3954                do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3955            }
3956
3957            do_reward_operators::<Test>(
3958                domain_id,
3959                OperatorRewardSource::Dummy,
3960                vec![operator_id].into_iter(),
3961                20 * AI3,
3962            )
3963            .unwrap();
3964            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3965
3966            // Manually convert previous withdrawal in share to balance
3967            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3968            for id in [operator_account, nominator_account] {
3969                Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3970                    do_convert_previous_epoch_withdrawal::<Test>(
3971                        operator_id,
3972                        maybe_withdrawal.as_mut().unwrap(),
3973                        domain_stake_summary.current_epoch_index,
3974                    )
3975                })
3976                .unwrap();
3977            }
3978
3979            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
3980            // due to withdrawal of 20 shares
3981            let operator = Operators::<Test>::get(operator_id).unwrap();
3982            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3983            let operator_withdrawal =
3984                Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3985            let nominator_withdrawal =
3986                Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3987
3988            let total_deposit =
3989                domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3990            let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3991                + nominator_withdrawal.total_withdrawal_amount;
3992            let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3993                .storage_fee_refund
3994                + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3995            assert_eq!(293333333333333333336, total_deposit,);
3996            assert_eq!(21666666666666666664, total_stake_withdrawal);
3997            assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3998            assert_eq!(
3999                320 * AI3,
4000                total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
4001            );
4002            assert_eq!(
4003                operator.total_storage_fee_deposit,
4004                bundle_storage_fund::total_balance::<Test>(operator_id)
4005            );
4006
4007            for deposit in deposits {
4008                do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
4009            }
4010
4011            do_mark_operators_as_slashed::<Test>(
4012                vec![operator_id],
4013                SlashedReason::InvalidBundle(1),
4014            )
4015            .unwrap();
4016
4017            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4018            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4019
4020            let operator = Operators::<Test>::get(operator_id).unwrap();
4021            assert_eq!(
4022                *operator.status::<Test>(operator_id),
4023                OperatorStatus::Slashed
4024            );
4025
4026            let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4027            assert!(pending_slashes.contains(&operator_id));
4028
4029            assert_eq!(
4030                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4031                0
4032            );
4033
4034            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4035            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4036            assert_eq!(Operators::<Test>::get(operator_id), None);
4037            assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4038
4039            assert_eq!(
4040                Balances::total_balance(&operator_account),
4041                operator_free_balance - operator_stake
4042            );
4043            assert_eq!(
4044                Balances::total_balance(&nominator_account),
4045                nominator_free_balance - nominator_stake
4046            );
4047
4048            assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3);
4049            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4050        });
4051    }
4052
4053    #[test]
4054    fn slash_operator_with_more_than_max_nominators_to_slash() {
4055        let domain_id = DomainId::new(0);
4056        let operator_account = 1;
4057        let operator_free_balance = 250 * AI3;
4058        let operator_stake = 200 * AI3;
4059        let operator_extra_deposit = 40 * AI3;
4060        let operator_extra_withdraw = 5 * AI3;
4061        let pair = OperatorPair::from_seed(&[0; 32]);
4062
4063        let nominator_accounts: Vec<crate::tests::AccountId> = (2..22).collect();
4064        let nominator_free_balance = 150 * AI3;
4065        let nominator_stake = 100 * AI3;
4066        let nominator_extra_deposit = 40 * AI3;
4067        let nominator_extra_withdraw = 5 * AI3;
4068
4069        let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
4070        for nominator_account in nominator_accounts.clone() {
4071            nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
4072        }
4073
4074        let last_nominator_account = nominator_accounts.last().cloned().unwrap();
4075        let unlocking = vec![
4076            (operator_account, 10 * AI3),
4077            (last_nominator_account, 10 * AI3),
4078        ];
4079
4080        let deposits = vec![
4081            (operator_account, operator_extra_deposit),
4082            (last_nominator_account, nominator_extra_deposit),
4083        ];
4084        let withdrawals = vec![
4085            (operator_account, operator_extra_withdraw),
4086            (last_nominator_account, nominator_extra_withdraw),
4087        ];
4088
4089        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
4090            * (200 + (100 * nominator_accounts.len() as u128))
4091            * AI3;
4092        let init_total_storage_fund =
4093            STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
4094
4095        let mut ext = new_test_ext();
4096        ext.execute_with(|| {
4097            let (operator_id, _) = register_operator(
4098                domain_id,
4099                operator_account,
4100                operator_free_balance,
4101                operator_stake,
4102                10 * AI3,
4103                pair.public(),
4104                Default::default(),
4105                BTreeMap::from_iter(nominators),
4106            );
4107
4108            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4109            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4110            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4111
4112            let operator = Operators::<Test>::get(operator_id).unwrap();
4113            assert_eq!(operator.current_total_stake, init_total_stake);
4114            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
4115            assert_eq!(
4116                operator.total_storage_fee_deposit,
4117                bundle_storage_fund::total_balance::<Test>(operator_id)
4118            );
4119
4120            for unlock in &unlocking {
4121                do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
4122            }
4123
4124            do_reward_operators::<Test>(
4125                domain_id,
4126                OperatorRewardSource::Dummy,
4127                vec![operator_id].into_iter(),
4128                20 * AI3,
4129            )
4130            .unwrap();
4131            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4132
4133            // Manually convert previous withdrawal in share to balance
4134            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4135            for id in [operator_account, last_nominator_account] {
4136                Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
4137                    do_convert_previous_epoch_withdrawal::<Test>(
4138                        operator_id,
4139                        maybe_withdrawal.as_mut().unwrap(),
4140                        domain_stake_summary.current_epoch_index,
4141                    )
4142                })
4143                .unwrap();
4144            }
4145
4146            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
4147            // due to withdrawal of 20 shares
4148            let operator = Operators::<Test>::get(operator_id).unwrap();
4149            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4150            let operator_withdrawal =
4151                Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
4152            let nominator_withdrawal =
4153                Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
4154
4155            let total_deposit =
4156                domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
4157            let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
4158                + nominator_withdrawal.total_withdrawal_amount;
4159            let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
4160                .storage_fee_refund
4161                + nominator_withdrawal.withdrawals[0].storage_fee_refund;
4162            assert_eq!(2194772727272727272734, total_deposit,);
4163            assert_eq!(20227272727272727266, total_stake_withdrawal);
4164            assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
4165            assert_eq!(
4166                2220 * AI3,
4167                total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
4168            );
4169
4170            assert_eq!(
4171                operator.total_storage_fee_deposit,
4172                bundle_storage_fund::total_balance::<Test>(operator_id)
4173            );
4174
4175            for deposit in deposits {
4176                do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
4177            }
4178            for withdrawal in withdrawals {
4179                do_withdraw_stake::<Test>(
4180                    operator_id,
4181                    withdrawal.0,
4182                    // Guess that the number of shares will be approximately the same as the stake
4183                    // amount.
4184                    withdrawal.1,
4185                )
4186                .unwrap();
4187            }
4188
4189            do_mark_operators_as_slashed::<Test>(
4190                vec![operator_id],
4191                SlashedReason::InvalidBundle(1),
4192            )
4193            .unwrap();
4194
4195            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4196
4197            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4198            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4199
4200            let operator = Operators::<Test>::get(operator_id).unwrap();
4201            assert_eq!(
4202                *operator.status::<Test>(operator_id),
4203                OperatorStatus::Slashed
4204            );
4205
4206            let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4207            assert!(pending_slashes.contains(&operator_id));
4208
4209            assert_eq!(
4210                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4211                0
4212            );
4213
4214            // since we only slash 10 nominators a time but we have a total of 21 nominators,
4215            // do 3 iterations
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            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4219
4220            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4221            assert_eq!(Operators::<Test>::get(operator_id), None);
4222            assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4223
4224            assert_eq!(
4225                Balances::total_balance(&operator_account),
4226                operator_free_balance - operator_stake
4227            );
4228            for nominator_account in nominator_accounts {
4229                assert_eq!(
4230                    Balances::total_balance(&nominator_account),
4231                    nominator_free_balance - nominator_stake
4232                );
4233            }
4234
4235            assert_eq!(
4236                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4237                2220 * AI3
4238            );
4239            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4240        });
4241    }
4242
4243    #[test]
4244    fn slash_operators() {
4245        let domain_id = DomainId::new(0);
4246        let operator_free_balance = 250 * AI3;
4247        let operator_stake = 200 * AI3;
4248
4249        let operator_account_1 = 1;
4250        let operator_account_2 = 2;
4251        let operator_account_3 = 3;
4252
4253        let pair_1 = OperatorPair::from_seed(&[0; 32]);
4254        let pair_2 = OperatorPair::from_seed(&[1; 32]);
4255        let pair_3 = OperatorPair::from_seed(&[2; 32]);
4256
4257        let mut ext = new_test_ext();
4258        ext.execute_with(|| {
4259            let (operator_id_1, _) = register_operator(
4260                domain_id,
4261                operator_account_1,
4262                operator_free_balance,
4263                operator_stake,
4264                10 * AI3,
4265                pair_1.public(),
4266                Default::default(),
4267                Default::default(),
4268            );
4269
4270            let (operator_id_2, _) = register_operator(
4271                domain_id,
4272                operator_account_2,
4273                operator_free_balance,
4274                operator_stake,
4275                10 * AI3,
4276                pair_2.public(),
4277                Default::default(),
4278                Default::default(),
4279            );
4280
4281            let (operator_id_3, _) = register_operator(
4282                domain_id,
4283                operator_account_3,
4284                operator_free_balance,
4285                operator_stake,
4286                10 * AI3,
4287                pair_3.public(),
4288                Default::default(),
4289                Default::default(),
4290            );
4291
4292            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4293            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4294            assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
4295            assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
4296            assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
4297            assert_eq!(
4298                domain_stake_summary.current_total_stake,
4299                STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
4300            );
4301            for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4302                let operator = Operators::<Test>::get(operator_id).unwrap();
4303                assert_eq!(
4304                    operator.total_storage_fee_deposit,
4305                    STORAGE_FEE_RESERVE * operator_stake
4306                );
4307                assert_eq!(
4308                    operator.total_storage_fee_deposit,
4309                    bundle_storage_fund::total_balance::<Test>(operator_id)
4310                );
4311            }
4312
4313            // deactivated operators can be slashed
4314            let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id_3);
4315            assert_ok!(res);
4316            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4317
4318            let operator = Operators::<Test>::get(operator_id_3).unwrap();
4319            assert_eq!(
4320                *operator.status::<Test>(operator_id_3),
4321                OperatorStatus::Deactivated(7)
4322            );
4323
4324            do_mark_operators_as_slashed::<Test>(
4325                vec![operator_id_1],
4326                SlashedReason::InvalidBundle(1),
4327            )
4328            .unwrap();
4329            do_mark_operators_as_slashed::<Test>(
4330                vec![operator_id_2],
4331                SlashedReason::InvalidBundle(2),
4332            )
4333            .unwrap();
4334            do_mark_operators_as_slashed::<Test>(
4335                vec![operator_id_3],
4336                SlashedReason::InvalidBundle(3),
4337            )
4338            .unwrap();
4339
4340            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4341            assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
4342            assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
4343            assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
4344
4345            let operator = Operators::<Test>::get(operator_id_1).unwrap();
4346            assert_eq!(
4347                *operator.status::<Test>(operator_id_1),
4348                OperatorStatus::Slashed
4349            );
4350
4351            let operator = Operators::<Test>::get(operator_id_2).unwrap();
4352            assert_eq!(
4353                *operator.status::<Test>(operator_id_2),
4354                OperatorStatus::Slashed
4355            );
4356
4357            let operator = Operators::<Test>::get(operator_id_3).unwrap();
4358            assert_eq!(
4359                *operator.status::<Test>(operator_id_3),
4360                OperatorStatus::Slashed
4361            );
4362
4363            assert_eq!(
4364                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4365                0
4366            );
4367
4368            let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
4369            slashed_operators.into_iter().for_each(|_| {
4370                do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4371            });
4372
4373            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4374            assert_eq!(Operators::<Test>::get(operator_id_1), None);
4375            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
4376            assert_eq!(Operators::<Test>::get(operator_id_2), None);
4377            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
4378            assert_eq!(Operators::<Test>::get(operator_id_3), None);
4379            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
4380
4381            assert_eq!(
4382                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4383                600 * AI3
4384            );
4385            for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4386                assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4387            }
4388        });
4389    }
4390
4391    #[test]
4392    fn bundle_storage_fund_charged_and_refund_storage_fee() {
4393        let domain_id = DomainId::new(0);
4394        let operator_account = 1;
4395        let operator_free_balance = 150 * AI3;
4396        let operator_total_stake = 100 * AI3;
4397        let operator_stake = 80 * AI3;
4398        let operator_storage_fee_deposit = 20 * AI3;
4399        let pair = OperatorPair::from_seed(&[0; 32]);
4400        let nominator_account = 2;
4401
4402        let mut ext = new_test_ext();
4403        ext.execute_with(|| {
4404            let (operator_id, _) = register_operator(
4405                domain_id,
4406                operator_account,
4407                operator_free_balance,
4408                operator_total_stake,
4409                AI3,
4410                pair.public(),
4411                Default::default(),
4412                BTreeMap::default(),
4413            );
4414
4415            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4416            assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
4417
4418            let operator = Operators::<Test>::get(operator_id).unwrap();
4419            assert_eq!(operator.current_total_stake, operator_stake);
4420            assert_eq!(operator.current_total_shares, operator_stake);
4421            assert_eq!(
4422                operator.total_storage_fee_deposit,
4423                operator_storage_fee_deposit
4424            );
4425
4426            // Drain the bundle storage fund
4427            bundle_storage_fund::charge_bundle_storage_fee::<Test>(
4428                operator_id,
4429                // the transaction fee is one AI3 per byte thus div AI3 here
4430                (operator_storage_fee_deposit / AI3) as u32,
4431            )
4432            .unwrap();
4433            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4434            assert_err!(
4435                bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
4436                bundle_storage_fund::Error::BundleStorageFeePayment
4437            );
4438
4439            // The operator add more stake thus add deposit to the bundle storage fund
4440            do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
4441            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4442
4443            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4444            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4445
4446            // New nominator add deposit to the bundle storage fund
4447            Balances::set_balance(&nominator_account, 100 * AI3);
4448            do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4449            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4450
4451            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4452            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4453
4454            // Refund of the storage fee add deposit to the bundle storage fund
4455            bundle_storage_fund::refund_storage_fee::<Test>(
4456                10 * AI3,
4457                BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
4458            )
4459            .unwrap();
4460            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4461
4462            // The operator `operator_id + 1` not exist thus the refund storage fee added to treasury
4463            assert_eq!(
4464                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4465                9 * AI3
4466            );
4467
4468            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4469            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4470        });
4471    }
4472
4473    #[test]
4474    fn zero_amount_deposit_and_withdraw() {
4475        let domain_id = DomainId::new(0);
4476        let operator_account = 1;
4477        let operator_free_balance = 250 * AI3;
4478        let operator_stake = 200 * AI3;
4479        let pair = OperatorPair::from_seed(&[0; 32]);
4480        let nominator_account = 2;
4481        let nominator_free_balance = 150 * AI3;
4482        let nominator_stake = 100 * AI3;
4483
4484        let nominators = vec![
4485            (operator_account, (operator_free_balance, operator_stake)),
4486            (nominator_account, (nominator_free_balance, nominator_stake)),
4487        ];
4488
4489        let total_deposit = 300 * AI3;
4490        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4491
4492        let mut ext = new_test_ext();
4493        ext.execute_with(|| {
4494            let (operator_id, _) = register_operator(
4495                domain_id,
4496                operator_account,
4497                operator_free_balance,
4498                operator_stake,
4499                10 * AI3,
4500                pair.public(),
4501                Default::default(),
4502                BTreeMap::from_iter(nominators),
4503            );
4504
4505            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4506            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4507            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4508
4509            // Zero deposits should be rejected
4510            assert_err!(
4511                do_nominate_operator::<Test>(operator_id, nominator_account, 0),
4512                StakingError::ZeroDeposit
4513            );
4514
4515            // Zero withdraws should be rejected
4516            assert_err!(
4517                do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
4518                StakingError::ZeroWithdraw
4519            );
4520
4521            // Withdraw all
4522            do_withdraw_stake::<Test>(
4523                operator_id,
4524                nominator_account,
4525                // Assume shares are similar to the stake amount
4526                STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
4527            )
4528            .unwrap();
4529            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4530        });
4531    }
4532
4533    #[test]
4534    fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
4535        let domain_id = DomainId::new(0);
4536        let operator_account = 1;
4537        let operator_free_balance = 250 * AI3;
4538        let operator_stake = 200 * AI3;
4539        let pair = OperatorPair::from_seed(&[0; 32]);
4540        let nominator_account = 2;
4541        let nominator_free_balance = 150 * AI3;
4542        let nominator_stake = 100 * AI3;
4543
4544        let nominators = vec![
4545            (operator_account, (operator_free_balance, operator_stake)),
4546            (nominator_account, (nominator_free_balance, nominator_stake)),
4547        ];
4548
4549        let total_deposit = 300 * AI3;
4550        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4551
4552        let mut ext = new_test_ext();
4553        ext.execute_with(|| {
4554            let (operator_id, _) = register_operator(
4555                domain_id,
4556                operator_account,
4557                operator_free_balance,
4558                operator_stake,
4559                10 * AI3,
4560                pair.public(),
4561                Default::default(),
4562                BTreeMap::from_iter(nominators),
4563            );
4564
4565            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4566            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4567            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4568
4569            do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4570            // Assume shares will be approximately the same as the stake amount.
4571            do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4572
4573            // Completed current epoch
4574            let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4575            // Remove the epoch share price intentionally
4576            OperatorEpochSharePrice::<Test>::remove(
4577                operator_id,
4578                DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4579            );
4580
4581            // Both deposit and withdraw should fail due to the share price is missing unexpectedly
4582            assert_err!(
4583                do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4584                StakingError::MissingOperatorEpochSharePrice
4585            );
4586            assert_err!(
4587                do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4588                StakingError::MissingOperatorEpochSharePrice
4589            );
4590        });
4591    }
4592
4593    #[test]
4594    fn test_share_price_deposit() {
4595        let total_shares = 45 * AI3;
4596        let total_stake = 45 * AI3 + 37;
4597        let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4598
4599        // Each item in this list represents an individual deposit requested by a nominator
4600        let to_deposit_stakes = [
4601            5,
4602            7,
4603            9,
4604            11,
4605            17,
4606            23,
4607            934,
4608            24931,
4609            349083467,
4610            2 * AI3 + 32,
4611            52 * AI3 - 4729034,
4612            2732 * AI3 - 1720,
4613            1117 * AI3 + 1839832,
4614            31232 * AI3 - 987654321,
4615        ];
4616
4617        let mut deposited_share = 0;
4618        let mut deposited_stake = 0;
4619        for to_deposit_stake in to_deposit_stakes {
4620            let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4621
4622            // `deposited_stake` is sum of the stake deposited so far.
4623            deposited_stake += to_deposit_stake;
4624            // `deposited_share` is sum of the share that converted from `deposited_stake` so far,
4625            // this is also the share the nominator entitled to withdraw.
4626            deposited_share += to_deposit_share;
4627
4628            // Assuming an epoch transition happened
4629            //
4630            // `total_deposited_share` is the share converted from `operator.deposits_in_epoch`
4631            // and will be added to the `operator.current_total_shares`.
4632            let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4633
4634            // `total_deposited_share` must larger or equal to `deposited_share`, meaning the
4635            // arithmetic dust generated during stake-to-share convertion are leave to the pool
4636            // and can't withdraw/unlock, otherwise, `ShareOverflow` error will happen on `current_total_shares`
4637            // during withdraw/unlock.
4638            assert!(total_deposited_share >= deposited_share);
4639
4640            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4641            // lost during stake-to-share convertion.
4642            assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4643        }
4644    }
4645
4646    #[test]
4647    fn test_share_price_withdraw() {
4648        let total_shares = 123 * AI3;
4649        let total_stake = 123 * AI3 + 13;
4650        let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4651
4652        // Each item in this list represents an individual withdrawal requested by a nominator
4653        let to_withdraw_shares = [
4654            1,
4655            3,
4656            7,
4657            13,
4658            17,
4659            123,
4660            43553,
4661            546393039,
4662            15 * AI3 + 1342,
4663            2 * AI3 - 423,
4664            31 * AI3 - 1321,
4665            42 * AI3 + 4564234,
4666            7 * AI3 - 987654321,
4667            3 * AI3 + 987654321123879,
4668        ];
4669
4670        let mut withdrawn_share = 0;
4671        let mut withdrawn_stake = 0;
4672        for to_withdraw_share in to_withdraw_shares {
4673            let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4674
4675            // `withdrawn_share` is sum of the share withdrawn so far.
4676            withdrawn_share += to_withdraw_share;
4677            // `withdrawn_stake` is sum of the stake that converted from `withdrawn_share` so far,
4678            // this is also the stake the nominator entitled to release/mint during unlock.
4679            withdrawn_stake += to_withdraw_stake;
4680
4681            // Assuming an epoch transition happened
4682            //
4683            // `total_withdrawn_stake` is the stake converted from `operator.withdrawals_in_epoch`
4684            // and will be removed to the `operator.current_total_stake`.
4685            let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4686
4687            // `total_withdrawn_stake` must larger or equal to `withdrawn_stake`, meaning the
4688            // arithmetic dust generated during share-to-stake convertion are leave to the pool,
4689            // otherwise, the nominator will be able to mint reward out of thin air during unlock.
4690            assert!(total_withdrawn_stake >= withdrawn_stake);
4691
4692            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4693            // lost during share-to-stake convertion.
4694            assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4695        }
4696    }
4697
4698    #[test]
4699    fn test_share_price_unlock() {
4700        let mut total_shares = 20 * AI3;
4701        let mut total_stake = 20 * AI3 + 12;
4702
4703        // Each item in this list represents a nominator unlock after the operator de-registered.
4704        //
4705        // The following is simulating how `do_unlock_nominator` work, `shares-to-stake` must return a
4706        // rouding down result, otherwise, `BalanceOverflow` error will happen on `current_total_stake`
4707        // during `do_unlock_nominator`.
4708        for to_unlock_share in [
4709            AI3 + 123,
4710            2 * AI3 - 456,
4711            3 * AI3 - 789,
4712            4 * AI3 - 123 + 456,
4713            7 * AI3 + 789 - 987654321,
4714            3 * AI3 + 987654321,
4715        ] {
4716            let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4717
4718            let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4719
4720            total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4721            total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4722        }
4723        assert_eq!(total_shares, 0);
4724    }
4725}