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, DepositOnHold, DeregisteredOperators, DomainBlockNumberFor,
15    DomainHashingFor, Event, ExecutionReceiptOf, HoldIdentifier, InvalidBundleAuthors, NominatorId,
16    OperatorEpochSharePrice, OperatorHighestSlot, Pallet, ReceiptHashFor, SlashedReason,
17};
18use frame_support::traits::fungible::{Inspect, MutateHold};
19use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
20use frame_support::{PalletError, StorageDoubleMap, ensure};
21use frame_system::pallet_prelude::BlockNumberFor;
22use parity_scale_codec::{Decode, Encode};
23use scale_info::TypeInfo;
24use sp_core::{Get, sr25519};
25use sp_domains::{DomainId, EpochIndex, OperatorId, OperatorPublicKey, OperatorRewardSource};
26use sp_runtime::traits::{CheckedAdd, CheckedSub, Zero};
27use sp_runtime::{PerThing, Percent, Perquintill, Saturating};
28use sp_std::collections::btree_map::BTreeMap;
29use sp_std::collections::btree_set::BTreeSet;
30use sp_std::collections::vec_deque::VecDeque;
31use sp_std::vec::IntoIter;
32
33/// A nominators deposit.
34#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
35pub(crate) struct Deposit<Share: Copy, Balance: Copy> {
36    pub(crate) known: KnownDeposit<Share, Balance>,
37    pub(crate) pending: Option<PendingDeposit<Balance>>,
38}
39
40/// A share price is parts per billion of shares/ai3.
41/// Note: Shares must always be equal to or lower than ai3, and both shares and ai3 can't be zero.
42#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
43pub struct SharePrice(pub Perquintill);
44
45impl SharePrice {
46    /// Creates a new instance of share price from shares and stake.
47    /// Returns an error if there are more shares than stake or either value is zero.
48    pub(crate) fn new<T: Config>(
49        total_shares: T::Share,
50        total_stake: BalanceOf<T>,
51    ) -> Result<Self, Error> {
52        if total_shares > total_stake.into() {
53            // Invalid share price, can't be greater than one.
54            Err(Error::ShareOverflow)
55        } else if total_stake.is_zero() || total_shares.is_zero() {
56            // If there are no shares or no stake, we can't construct a zero share price.
57            Err(Error::ZeroSharePrice)
58        } else {
59            Ok(SharePrice(Perquintill::from_rational(
60                total_shares.into(),
61                total_stake,
62            )))
63        }
64    }
65
66    /// Converts stake to shares based on the share price.
67    /// Always rounding down i.e. may return less share due to arithmetic dust.
68    pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
69        self.0.mul_floor(stake).into()
70    }
71
72    /// Converts shares to stake based on the share price.
73    /// Always rounding down i.e. may return less stake due to arithmetic dust.
74    pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
75        // NOTE: `stakes = shares / share_price = shares / (total_shares / total_stake)`
76        // every `div` operation come with an arithmetic dust, to return a rounding down stakes,
77        // we want the first `div` rounding down (i.e. `saturating_reciprocal_mul_floor`) and
78        // the second `div` rounding up (i.e. `plus_epsilon`).
79        self.0
80            // Within the `SharePrice::new`, `Perquintill::from_rational` is internally rouding down,
81            // `plus_epsilon` essentially return a rounding up share price.
82            .plus_epsilon()
83            .saturating_reciprocal_mul_floor(shares.into())
84    }
85
86    /// Return a 1:1 share price
87    pub(crate) fn one() -> Self {
88        Self(Perquintill::one())
89    }
90}
91
92/// Unique epoch identifier across all domains. A combination of Domain and its epoch.
93#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
94pub struct DomainEpoch(DomainId, EpochIndex);
95
96impl DomainEpoch {
97    pub(crate) fn deconstruct(self) -> (DomainId, EpochIndex) {
98        (self.0, self.1)
99    }
100}
101
102impl From<(DomainId, EpochIndex)> for DomainEpoch {
103    fn from((domain_id, epoch_idx): (DomainId, EpochIndex)) -> Self {
104        Self(domain_id, epoch_idx)
105    }
106}
107
108pub struct NewDeposit<Balance> {
109    pub(crate) staking: Balance,
110    pub(crate) storage_fee_deposit: Balance,
111}
112
113/// A nominator's shares against their deposits to given operator pool.
114#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq, Default)]
115pub(crate) struct KnownDeposit<Share: Copy, Balance: Copy> {
116    pub(crate) shares: Share,
117    pub(crate) storage_fee_deposit: Balance,
118}
119
120/// A nominators pending deposit in AI3 that needs to be converted to shares once domain epoch is complete.
121#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
122pub(crate) struct PendingDeposit<Balance: Copy> {
123    pub(crate) effective_domain_epoch: DomainEpoch,
124    pub(crate) amount: Balance,
125    pub(crate) storage_fee_deposit: Balance,
126}
127
128impl<Balance: Copy + CheckedAdd> PendingDeposit<Balance> {
129    fn total(&self) -> Result<Balance, Error> {
130        self.amount
131            .checked_add(&self.storage_fee_deposit)
132            .ok_or(Error::BalanceOverflow)
133    }
134}
135
136/// A nominator's withdrawal from a given operator pool.
137#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
138pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
139    /// Total withdrawal amount requested by the nominator that are in unlocking state excluding withdrawal
140    /// in shares and the storage fee
141    pub(crate) total_withdrawal_amount: Balance,
142    /// Total amount of storage fee on withdraw (including withdrawal in shares)
143    pub(crate) total_storage_fee_withdrawal: Balance,
144    /// Individual withdrawal amounts with their unlocking block for a given domain
145    pub(crate) withdrawals: VecDeque<WithdrawalInBalance<DomainBlockNumber, Balance>>,
146    /// Withdrawal that was initiated by nominator and not converted to balance due to
147    /// unfinished domain epoch.
148    pub(crate) withdrawal_in_shares: Option<WithdrawalInShares<DomainBlockNumber, Share, Balance>>,
149}
150
151#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
152pub(crate) struct WithdrawalInBalance<DomainBlockNumber, Balance> {
153    pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
154    pub(crate) amount_to_unlock: Balance,
155    pub(crate) storage_fee_refund: Balance,
156}
157
158#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
159pub(crate) struct WithdrawalInShares<DomainBlockNumber, Share, Balance> {
160    pub(crate) domain_epoch: DomainEpoch,
161    pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
162    pub(crate) shares: Share,
163    pub(crate) storage_fee_refund: Balance,
164}
165
166#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
167pub struct OperatorDeregisteredInfo<DomainBlockNumber> {
168    pub domain_epoch: DomainEpoch,
169    pub unlock_at_confirmed_domain_block_number: DomainBlockNumber,
170}
171
172impl<DomainBlockNumber> From<(DomainId, EpochIndex, DomainBlockNumber)>
173    for OperatorDeregisteredInfo<DomainBlockNumber>
174{
175    fn from(value: (DomainId, EpochIndex, DomainBlockNumber)) -> Self {
176        OperatorDeregisteredInfo {
177            domain_epoch: (value.0, value.1).into(),
178            unlock_at_confirmed_domain_block_number: value.2,
179        }
180    }
181}
182
183/// Type that represents an operator status.
184#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
185pub enum OperatorStatus<DomainBlockNumber, ReceiptHash> {
186    #[codec(index = 0)]
187    Registered,
188    /// De-registered at given domain epoch.
189    #[codec(index = 1)]
190    Deregistered(OperatorDeregisteredInfo<DomainBlockNumber>),
191    #[codec(index = 2)]
192    Slashed,
193    #[codec(index = 3)]
194    PendingSlash,
195    #[codec(index = 4)]
196    InvalidBundle(ReceiptHash),
197}
198
199/// Type that represents an operator details.
200#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
201pub struct Operator<Balance, Share, DomainBlockNumber, ReceiptHash> {
202    pub signing_key: OperatorPublicKey,
203    pub current_domain_id: DomainId,
204    pub next_domain_id: DomainId,
205    pub minimum_nominator_stake: Balance,
206    pub nomination_tax: Percent,
207    /// Total active stake of combined nominators under this operator.
208    pub current_total_stake: Balance,
209    /// Total shares of all the nominators under this operator.
210    pub current_total_shares: Share,
211    /// The status of the operator, it may be stale due to the `OperatorStatus::PendingSlash` is
212    /// not assigned to this field directly, thus MUST use the `status()` method to query the status
213    /// instead.
214    partial_status: OperatorStatus<DomainBlockNumber, ReceiptHash>,
215    /// Total deposits during the previous epoch
216    pub deposits_in_epoch: Balance,
217    /// Total withdrew shares during the previous epoch
218    pub withdrawals_in_epoch: Share,
219    /// Total balance deposited to the bundle storage fund
220    pub total_storage_fee_deposit: Balance,
221}
222
223impl<Balance, Share, DomainBlockNumber, ReceiptHash>
224    Operator<Balance, Share, DomainBlockNumber, ReceiptHash>
225{
226    pub fn status<T: Config>(
227        &self,
228        operator_id: OperatorId,
229    ) -> &OperatorStatus<DomainBlockNumber, ReceiptHash> {
230        if matches!(self.partial_status, OperatorStatus::Slashed) {
231            &OperatorStatus::Slashed
232        } else if Pallet::<T>::is_operator_pending_to_slash(self.current_domain_id, operator_id) {
233            &OperatorStatus::PendingSlash
234        } else {
235            &self.partial_status
236        }
237    }
238
239    pub fn update_status(&mut self, new_status: OperatorStatus<DomainBlockNumber, ReceiptHash>) {
240        self.partial_status = new_status;
241    }
242}
243
244#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
245pub struct StakingSummary<OperatorId, Balance> {
246    /// Current epoch index for the domain.
247    pub current_epoch_index: EpochIndex,
248    /// Total active stake for the current epoch.
249    pub current_total_stake: Balance,
250    /// Current operators for this epoch
251    pub current_operators: BTreeMap<OperatorId, Balance>,
252    /// Operators for the next epoch.
253    pub next_operators: BTreeSet<OperatorId>,
254    /// Operator's current Epoch rewards
255    pub current_epoch_rewards: BTreeMap<OperatorId, Balance>,
256}
257
258#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
259pub struct OperatorConfig<Balance> {
260    pub signing_key: OperatorPublicKey,
261    pub minimum_nominator_stake: Balance,
262    pub nomination_tax: Percent,
263}
264
265#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
266pub enum Error {
267    MaximumOperatorId,
268    DomainNotInitialized,
269    PendingOperatorSwitch,
270    InsufficientBalance,
271    InsufficientShares,
272    ZeroWithdraw,
273    BalanceFreeze,
274    MinimumOperatorStake,
275    UnknownOperator,
276    MinimumNominatorStake,
277    BalanceOverflow,
278    BalanceUnderflow,
279    NotOperatorOwner,
280    OperatorNotRegistered,
281    UnknownNominator,
282    MissingOperatorOwner,
283    MintBalance,
284    BlockNumberOverflow,
285    RemoveLock,
286    EpochOverflow,
287    ShareUnderflow,
288    ShareOverflow,
289    TooManyPendingStakingOperation,
290    OperatorNotAllowed,
291    InvalidOperatorSigningKey,
292    MissingOperatorEpochSharePrice,
293    MissingWithdrawal,
294    EpochNotComplete,
295    UnlockPeriodNotComplete,
296    OperatorNotDeregistered,
297    BundleStorageFund(bundle_storage_fund::Error),
298    UnconfirmedER,
299    TooManyWithdrawals,
300    ZeroDeposit,
301    ZeroSharePrice,
302}
303
304// Increase `PendingStakingOperationCount` by one and check if the `MaxPendingStakingOperation`
305// limit is exceeded
306fn note_pending_staking_operation<T: Config>(domain_id: DomainId) -> Result<(), Error> {
307    let pending_op_count = PendingStakingOperationCount::<T>::get(domain_id);
308
309    ensure!(
310        pending_op_count < T::MaxPendingStakingOperation::get(),
311        Error::TooManyPendingStakingOperation
312    );
313
314    PendingStakingOperationCount::<T>::set(domain_id, pending_op_count.saturating_add(1));
315
316    Ok(())
317}
318
319pub fn do_register_operator<T: Config>(
320    operator_owner: T::AccountId,
321    domain_id: DomainId,
322    amount: BalanceOf<T>,
323    config: OperatorConfig<BalanceOf<T>>,
324) -> Result<(OperatorId, EpochIndex), Error> {
325    note_pending_staking_operation::<T>(domain_id)?;
326
327    DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_domain_stake_summary| {
328        ensure!(
329            config.signing_key != OperatorPublicKey::from(sr25519::Public::default()),
330            Error::InvalidOperatorSigningKey
331        );
332
333        ensure!(
334            config.minimum_nominator_stake >= T::MinNominatorStake::get(),
335            Error::MinimumNominatorStake
336        );
337
338        let domain_obj = DomainRegistry::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
339        ensure!(
340            domain_obj
341                .domain_config
342                .operator_allow_list
343                .is_operator_allowed(&operator_owner),
344            Error::OperatorNotAllowed
345        );
346
347        let operator_id = NextOperatorId::<T>::get();
348        let next_operator_id = operator_id.checked_add(1).ok_or(Error::MaximumOperatorId)?;
349        NextOperatorId::<T>::set(next_operator_id);
350
351        OperatorIdOwner::<T>::insert(operator_id, operator_owner.clone());
352
353        // reserve stake balance
354        ensure!(
355            amount >= T::MinOperatorStake::get(),
356            Error::MinimumOperatorStake
357        );
358
359        let new_deposit =
360            deposit_reserve_for_storage_fund::<T>(operator_id, &operator_owner, amount)
361                .map_err(Error::BundleStorageFund)?;
362
363        hold_deposit::<T>(&operator_owner, operator_id, new_deposit.staking)?;
364
365        let domain_stake_summary = maybe_domain_stake_summary
366            .as_mut()
367            .ok_or(Error::DomainNotInitialized)?;
368
369        let OperatorConfig {
370            signing_key,
371            minimum_nominator_stake,
372            nomination_tax,
373        } = config;
374
375        // When the operator just registered, the operator owner is the first and only nominator
376        // thus it is safe to finalize the operator owner's deposit here by:
377        // - Adding the first share price, which is 1:1 since there is no reward
378        // - Adding this deposit to the operator's `current_total_shares` and `current_total_shares`
379        //
380        // NOTE: this is needed so we can ensure the operator's `current_total_shares` and `current_total_shares`
381        // will never be zero after it is registered and before all nominators is unlocked, thus we
382        // will never construct a zero share price.
383        let first_share_price = SharePrice::one();
384        let operator = Operator {
385            signing_key: signing_key.clone(),
386            current_domain_id: domain_id,
387            next_domain_id: domain_id,
388            minimum_nominator_stake,
389            nomination_tax,
390            current_total_stake: new_deposit.staking,
391            current_total_shares: first_share_price.stake_to_shares::<T>(new_deposit.staking),
392            partial_status: OperatorStatus::Registered,
393            // sum total deposits added during this epoch.
394            deposits_in_epoch: Zero::zero(),
395            withdrawals_in_epoch: Zero::zero(),
396            total_storage_fee_deposit: new_deposit.storage_fee_deposit,
397        };
398        Operators::<T>::insert(operator_id, operator);
399        OperatorEpochSharePrice::<T>::insert(
400            operator_id,
401            DomainEpoch::from((domain_id, domain_stake_summary.current_epoch_index)),
402            first_share_price,
403        );
404
405        // update stake summary to include new operator for next epoch
406        domain_stake_summary.next_operators.insert(operator_id);
407        // update pending transfers
408        let current_domain_epoch = (domain_id, domain_stake_summary.current_epoch_index).into();
409        do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
410            operator_id,
411            operator_owner,
412            current_domain_epoch,
413            new_deposit,
414            None,
415        )?;
416
417        Ok((operator_id, domain_stake_summary.current_epoch_index))
418    })
419}
420
421/// Calculates shares for any pending deposit for previous epoch using the epoch share price and
422/// then create a new pending deposit in the current epoch.
423/// If there is a pending deposit for the current epoch, then simply increment the amount.
424/// Returns updated deposit info
425pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T: Config>(
426    operator_id: OperatorId,
427    nominator_id: NominatorId<T>,
428    current_domain_epoch: DomainEpoch,
429    new_deposit: NewDeposit<BalanceOf<T>>,
430    required_minimum_nominator_stake: Option<BalanceOf<T>>,
431) -> Result<(), Error> {
432    Deposits::<T>::try_mutate(operator_id, nominator_id, |maybe_deposit| {
433        let mut deposit = maybe_deposit.take().unwrap_or_default();
434        do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, current_domain_epoch.1)?;
435
436        // add or create new pending deposit
437        let pending_deposit = match deposit.pending {
438            None => PendingDeposit {
439                effective_domain_epoch: current_domain_epoch,
440                amount: new_deposit.staking,
441                storage_fee_deposit: new_deposit.storage_fee_deposit,
442            },
443            Some(pending_deposit) => PendingDeposit {
444                effective_domain_epoch: current_domain_epoch,
445                amount: pending_deposit
446                    .amount
447                    .checked_add(&new_deposit.staking)
448                    .ok_or(Error::BalanceOverflow)?,
449                storage_fee_deposit: pending_deposit
450                    .storage_fee_deposit
451                    .checked_add(&new_deposit.storage_fee_deposit)
452                    .ok_or(Error::BalanceOverflow)?,
453            },
454        };
455
456        if deposit.known.shares.is_zero()
457            && let Some(minimum_nominator_stake) = required_minimum_nominator_stake
458        {
459            ensure!(
460                pending_deposit.total()? >= minimum_nominator_stake,
461                Error::MinimumNominatorStake
462            );
463        }
464
465        deposit.pending = Some(pending_deposit);
466        *maybe_deposit = Some(deposit);
467        Ok(())
468    })
469}
470
471pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
472    operator_id: OperatorId,
473    deposit: &mut Deposit<T::Share, BalanceOf<T>>,
474    current_domain_epoch_index: EpochIndex,
475) -> Result<(), Error> {
476    // if it is one of the previous domain epoch, then calculate shares for the epoch and update known deposit
477    let epoch_share_price = match deposit.pending {
478        None => return Ok(()),
479        Some(pending_deposit) => {
480            match OperatorEpochSharePrice::<T>::get(
481                operator_id,
482                pending_deposit.effective_domain_epoch,
483            ) {
484                Some(p) => p,
485                None => {
486                    ensure!(
487                        pending_deposit.effective_domain_epoch.1 >= current_domain_epoch_index,
488                        Error::MissingOperatorEpochSharePrice
489                    );
490                    return Ok(());
491                }
492            }
493        }
494    };
495
496    if let Some(PendingDeposit {
497        amount,
498        storage_fee_deposit,
499        ..
500    }) = deposit.pending.take()
501    {
502        let new_shares = epoch_share_price.stake_to_shares::<T>(amount);
503        deposit.known.shares = deposit
504            .known
505            .shares
506            .checked_add(&new_shares)
507            .ok_or(Error::ShareOverflow)?;
508        deposit.known.storage_fee_deposit = deposit
509            .known
510            .storage_fee_deposit
511            .checked_add(&storage_fee_deposit)
512            .ok_or(Error::BalanceOverflow)?;
513    }
514
515    Ok(())
516}
517
518/// Converts any epoch withdrawals into balance using the operator epoch share price.
519///
520/// If there is withdrawal happened in the current epoch (thus share price is unavailable),
521/// this will be no-op. If there is withdrawal happened in the previous epoch and the share
522/// price is unavailable, `MissingOperatorEpochSharePrice` error will be return.
523pub(crate) fn do_convert_previous_epoch_withdrawal<T: Config>(
524    operator_id: OperatorId,
525    withdrawal: &mut Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
526    current_domain_epoch_index: EpochIndex,
527) -> Result<(), Error> {
528    let epoch_share_price = match withdrawal.withdrawal_in_shares.as_ref() {
529        None => return Ok(()),
530        Some(withdraw) => {
531            // `withdraw.domain_epoch` is not end yet so the share price won't be available
532            if withdraw.domain_epoch.1 >= current_domain_epoch_index {
533                return Ok(());
534            }
535
536            match OperatorEpochSharePrice::<T>::get(operator_id, withdraw.domain_epoch) {
537                Some(p) => p,
538                None => return Err(Error::MissingOperatorEpochSharePrice),
539            }
540        }
541    };
542
543    if let Some(WithdrawalInShares {
544        unlock_at_confirmed_domain_block_number,
545        shares,
546        storage_fee_refund,
547        domain_epoch: _,
548    }) = withdrawal.withdrawal_in_shares.take()
549    {
550        let withdrawal_amount = epoch_share_price.shares_to_stake::<T>(shares);
551        withdrawal.total_withdrawal_amount = withdrawal
552            .total_withdrawal_amount
553            .checked_add(&withdrawal_amount)
554            .ok_or(Error::BalanceOverflow)?;
555
556        let withdraw_in_balance = WithdrawalInBalance {
557            unlock_at_confirmed_domain_block_number,
558            amount_to_unlock: withdrawal_amount,
559            storage_fee_refund,
560        };
561        withdrawal.withdrawals.push_back(withdraw_in_balance);
562    }
563
564    Ok(())
565}
566
567pub(crate) fn do_nominate_operator<T: Config>(
568    operator_id: OperatorId,
569    nominator_id: T::AccountId,
570    amount: BalanceOf<T>,
571) -> Result<(), Error> {
572    ensure!(!amount.is_zero(), Error::ZeroDeposit);
573
574    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
575        let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
576
577        ensure!(
578            *operator.status::<T>(operator_id) == OperatorStatus::Registered,
579            Error::OperatorNotRegistered
580        );
581
582        // If this is the first staking request of this operator `note_pending_staking_operation` for it
583        if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
584            note_pending_staking_operation::<T>(operator.current_domain_id)?;
585        }
586
587        let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
588            .ok_or(Error::DomainNotInitialized)?;
589
590        // Reserve for the bundle storage fund
591        let new_deposit = deposit_reserve_for_storage_fund::<T>(operator_id, &nominator_id, amount)
592            .map_err(Error::BundleStorageFund)?;
593
594        hold_deposit::<T>(&nominator_id, operator_id, new_deposit.staking)?;
595        Pallet::<T>::deposit_event(Event::OperatorNominated {
596            operator_id,
597            nominator_id: nominator_id.clone(),
598            amount: new_deposit.staking,
599        });
600
601        // increment total deposit for operator pool within this epoch
602        operator.deposits_in_epoch = operator
603            .deposits_in_epoch
604            .checked_add(&new_deposit.staking)
605            .ok_or(Error::BalanceOverflow)?;
606
607        // Increase total storage fee deposit as there is new deposit to the storage fund
608        operator.total_storage_fee_deposit = operator
609            .total_storage_fee_deposit
610            .checked_add(&new_deposit.storage_fee_deposit)
611            .ok_or(Error::BalanceOverflow)?;
612
613        let current_domain_epoch = (
614            operator.current_domain_id,
615            domain_stake_summary.current_epoch_index,
616        )
617            .into();
618
619        do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
620            operator_id,
621            nominator_id,
622            current_domain_epoch,
623            new_deposit,
624            Some(operator.minimum_nominator_stake),
625        )?;
626
627        Ok(())
628    })
629}
630
631pub(crate) fn hold_deposit<T: Config>(
632    who: &T::AccountId,
633    operator_id: OperatorId,
634    amount: BalanceOf<T>,
635) -> Result<(), Error> {
636    // ensure there is enough free balance to lock
637    ensure!(
638        T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite) >= amount,
639        Error::InsufficientBalance
640    );
641
642    DepositOnHold::<T>::try_mutate((operator_id, who), |deposit_on_hold| {
643        *deposit_on_hold = deposit_on_hold
644            .checked_add(&amount)
645            .ok_or(Error::BalanceOverflow)?;
646        Ok(())
647    })?;
648
649    let pending_deposit_hold_id = T::HoldIdentifier::staking_staked();
650    T::Currency::hold(&pending_deposit_hold_id, who, amount).map_err(|_| Error::BalanceFreeze)?;
651
652    Ok(())
653}
654
655pub(crate) fn do_deregister_operator<T: Config>(
656    operator_owner: T::AccountId,
657    operator_id: OperatorId,
658) -> Result<(), Error> {
659    ensure!(
660        OperatorIdOwner::<T>::get(operator_id) == Some(operator_owner),
661        Error::NotOperatorOwner
662    );
663
664    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
665        let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
666
667        ensure!(
668            *operator.status::<T>(operator_id) == OperatorStatus::Registered,
669            Error::OperatorNotRegistered
670        );
671
672        DomainStakingSummary::<T>::try_mutate(
673            operator.current_domain_id,
674            |maybe_domain_stake_summary| {
675                let stake_summary = maybe_domain_stake_summary
676                    .as_mut()
677                    .ok_or(Error::DomainNotInitialized)?;
678
679                let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
680                let unlock_operator_at_domain_block_number = head_domain_number
681                    .checked_add(&T::StakeWithdrawalLockingPeriod::get())
682                    .ok_or(Error::BlockNumberOverflow)?;
683                let operator_deregister_info = (
684                    operator.current_domain_id,
685                    stake_summary.current_epoch_index,
686                    unlock_operator_at_domain_block_number,
687                )
688                    .into();
689
690                operator.update_status(OperatorStatus::Deregistered(operator_deregister_info));
691
692                stake_summary.next_operators.remove(&operator_id);
693
694                DeregisteredOperators::<T>::mutate(operator.current_domain_id, |operators| {
695                    operators.insert(operator_id)
696                });
697                Ok(())
698            },
699        )
700    })
701}
702
703/// A helper function used to calculate the share price at this instant
704/// Returns an error if there are more shares than stake, or if either value is zero.
705pub(crate) fn current_share_price<T: Config>(
706    operator_id: OperatorId,
707    operator: &Operator<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
708    domain_stake_summary: &StakingSummary<OperatorId, BalanceOf<T>>,
709) -> Result<SharePrice, Error> {
710    // Total stake including any reward within this epoch.
711    let total_stake = domain_stake_summary
712        .current_epoch_rewards
713        .get(&operator_id)
714        .and_then(|rewards| {
715            let operator_tax = operator.nomination_tax.mul_floor(*rewards);
716            operator
717                .current_total_stake
718                .checked_add(rewards)?
719                // deduct operator tax
720                .checked_sub(&operator_tax)
721        })
722        .unwrap_or(operator.current_total_stake);
723
724    SharePrice::new::<T>(operator.current_total_shares, total_stake)
725}
726
727/// Withdraw some or all of the stake, using an amount of shares.
728///
729/// Withdrawal validity depends on the current share price and number of shares, so requests can
730/// pass the initial checks, but fail because the most recent share amount is lower than expected.
731///
732/// Absolute stake amount and percentage withdrawals can be handled in the frontend.
733/// Full stake withdrawals are handled by withdrawing everything, if the remaining number of shares
734/// is less than the minimum nominator stake, and the nominator is not the operator.
735pub(crate) fn do_withdraw_stake<T: Config>(
736    operator_id: OperatorId,
737    nominator_id: NominatorId<T>,
738    to_withdraw: T::Share,
739) -> Result<(), Error> {
740    // Some withdraws are always zero, others require calculations to check if they are zero.
741    // So this check is redundant, but saves us some work if the request will always be rejected.
742    ensure!(!to_withdraw.is_zero(), Error::ZeroWithdraw);
743
744    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
745        let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
746        ensure!(
747            *operator.status::<T>(operator_id) == OperatorStatus::Registered,
748            Error::OperatorNotRegistered
749        );
750
751        // If this is the first staking request of this operator `note_pending_staking_operation` for it
752        if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
753            note_pending_staking_operation::<T>(operator.current_domain_id)?;
754        }
755
756        // calculate shares for any previous epoch
757        let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
758            .ok_or(Error::DomainNotInitialized)?;
759        let domain_current_epoch = (
760            operator.current_domain_id,
761            domain_stake_summary.current_epoch_index,
762        )
763            .into();
764
765        let known_shares =
766            Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
767                let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
768                do_convert_previous_epoch_deposits::<T>(
769                    operator_id,
770                    deposit,
771                    domain_stake_summary.current_epoch_index,
772                )?;
773                Ok(deposit.known.shares)
774            })?;
775
776        Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
777            if let Some(withdrawal) = maybe_withdrawal {
778                do_convert_previous_epoch_withdrawal::<T>(
779                    operator_id,
780                    withdrawal,
781                    domain_stake_summary.current_epoch_index,
782                )?;
783                if withdrawal.withdrawals.len() as u32 >= T::WithdrawalLimit::get() {
784                    return Err(Error::TooManyWithdrawals);
785                }
786            }
787            Ok(())
788        })?;
789
790        let operator_owner =
791            OperatorIdOwner::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
792
793        let is_operator_owner = operator_owner == nominator_id;
794
795        Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
796            let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
797
798            let (remaining_shares, shares_withdrew) = {
799                let remaining_shares = known_shares
800                    .checked_sub(&to_withdraw)
801                    .ok_or(Error::InsufficientShares)?;
802
803                // short circuit to check if remaining shares can be zero
804                if remaining_shares.is_zero() {
805                    if is_operator_owner {
806                        return Err(Error::MinimumOperatorStake);
807                    }
808
809                    (remaining_shares, to_withdraw)
810                } else {
811                    let share_price =
812                        current_share_price::<T>(operator_id, operator, &domain_stake_summary)?;
813
814                    let remaining_storage_fee =
815                        Perquintill::from_rational(remaining_shares.into(), known_shares.into())
816                            .mul_floor(deposit.known.storage_fee_deposit);
817
818                    let remaining_stake = share_price
819                        .shares_to_stake::<T>(remaining_shares)
820                        .checked_add(&remaining_storage_fee)
821                        .ok_or(Error::BalanceOverflow)?;
822
823                    // ensure the remaining share value is at least the defined MinOperatorStake if
824                    // a nominator is the operator pool owner
825                    if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
826                        return Err(Error::MinimumOperatorStake);
827                    }
828
829                    // if not an owner, if remaining balance < MinNominatorStake, then withdraw all shares.
830                    if !is_operator_owner && remaining_stake.lt(&operator.minimum_nominator_stake) {
831                        (T::Share::zero(), known_shares)
832                    } else {
833                        (remaining_shares, to_withdraw)
834                    }
835                }
836            };
837
838            // Withdraw storage fund, the `withdraw_storage_fee` amount of fund will be transferred
839            // and hold on the nominator account
840            let storage_fee_to_withdraw =
841                Perquintill::from_rational(shares_withdrew.into(), known_shares.into())
842                    .mul_floor(deposit.known.storage_fee_deposit);
843
844            let withdraw_storage_fee = {
845                let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
846                    operator_id,
847                    operator.total_storage_fee_deposit,
848                );
849                bundle_storage_fund::withdraw_and_hold::<T>(
850                    operator_id,
851                    &nominator_id,
852                    storage_fund_redeem_price.redeem(storage_fee_to_withdraw),
853                )
854                .map_err(Error::BundleStorageFund)?
855            };
856
857            deposit.known.storage_fee_deposit = deposit
858                .known
859                .storage_fee_deposit
860                .checked_sub(&storage_fee_to_withdraw)
861                .ok_or(Error::BalanceOverflow)?;
862
863            operator.total_storage_fee_deposit = operator
864                .total_storage_fee_deposit
865                .checked_sub(&storage_fee_to_withdraw)
866                .ok_or(Error::BalanceOverflow)?;
867
868            // update operator pool to note withdrew shares in the epoch
869            operator.withdrawals_in_epoch = operator
870                .withdrawals_in_epoch
871                .checked_add(&shares_withdrew)
872                .ok_or(Error::ShareOverflow)?;
873
874            deposit.known.shares = remaining_shares;
875            if remaining_shares.is_zero()
876                && let Some(pending_deposit) = deposit.pending
877            {
878                // if there is a pending deposit, then ensure
879                // the new deposit is atleast minimum nominator stake
880                ensure!(
881                    pending_deposit.total()? >= operator.minimum_nominator_stake,
882                    Error::MinimumNominatorStake
883                );
884            }
885
886            let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
887            let unlock_at_confirmed_domain_block_number = head_domain_number
888                .checked_add(&T::StakeWithdrawalLockingPeriod::get())
889                .ok_or(Error::BlockNumberOverflow)?;
890
891            Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
892                let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
893                // if this is some, then the withdrawal was initiated in this current epoch due to conversion
894                // of previous epoch withdrawals from shares to balances above. So just update it instead
895                let new_withdrawal_in_shares = match withdrawal.withdrawal_in_shares.take() {
896                    Some(WithdrawalInShares {
897                        shares,
898                        storage_fee_refund,
899                        ..
900                    }) => WithdrawalInShares {
901                        domain_epoch: domain_current_epoch,
902                        shares: shares
903                            .checked_add(&shares_withdrew)
904                            .ok_or(Error::ShareOverflow)?,
905                        unlock_at_confirmed_domain_block_number,
906                        storage_fee_refund: storage_fee_refund
907                            .checked_add(&withdraw_storage_fee)
908                            .ok_or(Error::BalanceOverflow)?,
909                    },
910                    None => WithdrawalInShares {
911                        domain_epoch: domain_current_epoch,
912                        unlock_at_confirmed_domain_block_number,
913                        shares: shares_withdrew,
914                        storage_fee_refund: withdraw_storage_fee,
915                    },
916                };
917                withdrawal.withdrawal_in_shares = Some(new_withdrawal_in_shares);
918                withdrawal.total_storage_fee_withdrawal = withdrawal
919                    .total_storage_fee_withdrawal
920                    .checked_add(&withdraw_storage_fee)
921                    .ok_or(Error::BalanceOverflow)?;
922
923                *maybe_withdrawal = Some(withdrawal);
924                Ok(())
925            })
926        })
927    })
928}
929
930/// Unlocks any withdraws that are ready to be unlocked.
931///
932/// Return the number of withdrawals being unlocked
933pub(crate) fn do_unlock_funds<T: Config>(
934    operator_id: OperatorId,
935    nominator_id: NominatorId<T>,
936) -> Result<u32, Error> {
937    let operator = Operators::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
938    ensure!(
939        *operator.status::<T>(operator_id) == OperatorStatus::Registered,
940        Error::OperatorNotRegistered
941    );
942
943    let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
944        .ok_or(Error::DomainNotInitialized)?
945        .current_epoch_index;
946
947    Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
948        let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
949        do_convert_previous_epoch_withdrawal::<T>(
950            operator_id,
951            withdrawal,
952            current_domain_epoch_index,
953        )?;
954
955        ensure!(!withdrawal.withdrawals.is_empty(), Error::MissingWithdrawal);
956
957        let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
958
959        let mut withdrawal_count = 0;
960        let mut total_unlocked_amount = BalanceOf::<T>::zero();
961        let mut total_storage_fee_refund = BalanceOf::<T>::zero();
962        loop {
963            if withdrawal
964                .withdrawals
965                .front()
966                .map(|w| w.unlock_at_confirmed_domain_block_number > head_domain_number)
967                .unwrap_or(true)
968            {
969                break;
970            }
971
972            let WithdrawalInBalance {
973                amount_to_unlock,
974                storage_fee_refund,
975                ..
976            } = withdrawal
977                .withdrawals
978                .pop_front()
979                .expect("Must not empty as checked above; qed");
980
981            total_unlocked_amount = total_unlocked_amount
982                .checked_add(&amount_to_unlock)
983                .ok_or(Error::BalanceOverflow)?;
984
985            total_storage_fee_refund = total_storage_fee_refund
986                .checked_add(&storage_fee_refund)
987                .ok_or(Error::BalanceOverflow)?;
988
989            withdrawal_count += 1;
990        }
991
992        // There is withdrawal but none being processed meaning the first withdrawal's unlock period has
993        // not completed yet
994        ensure!(
995            !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
996            Error::UnlockPeriodNotComplete
997        );
998
999        // deduct the amount unlocked from total
1000        withdrawal.total_withdrawal_amount = withdrawal
1001            .total_withdrawal_amount
1002            .checked_sub(&total_unlocked_amount)
1003            .ok_or(Error::BalanceUnderflow)?;
1004
1005        withdrawal.total_storage_fee_withdrawal = withdrawal
1006            .total_storage_fee_withdrawal
1007            .checked_sub(&total_storage_fee_refund)
1008            .ok_or(Error::BalanceUnderflow)?;
1009
1010        // If the amount to release is more than currently locked,
1011        // mint the diff and release the rest
1012        let (amount_to_mint, amount_to_release) = DepositOnHold::<T>::try_mutate(
1013            (operator_id, nominator_id.clone()),
1014            |deposit_on_hold| {
1015                let amount_to_release = total_unlocked_amount.min(*deposit_on_hold);
1016                let amount_to_mint = total_unlocked_amount.saturating_sub(*deposit_on_hold);
1017
1018                *deposit_on_hold = deposit_on_hold.saturating_sub(amount_to_release);
1019
1020                Ok((amount_to_mint, amount_to_release))
1021            },
1022        )?;
1023
1024        // Mint any gains
1025        if !amount_to_mint.is_zero() {
1026            mint_funds::<T>(&nominator_id, amount_to_mint)?;
1027        }
1028        // Release staking fund
1029        if !amount_to_release.is_zero() {
1030            let staked_hold_id = T::HoldIdentifier::staking_staked();
1031            T::Currency::release(
1032                &staked_hold_id,
1033                &nominator_id,
1034                amount_to_release,
1035                Precision::Exact,
1036            )
1037            .map_err(|_| Error::RemoveLock)?;
1038        }
1039
1040        Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1041            operator_id,
1042            nominator_id: nominator_id.clone(),
1043            unlocked_amount: total_unlocked_amount,
1044        });
1045
1046        // Release storage fund
1047        let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1048        T::Currency::release(
1049            &storage_fund_hold_id,
1050            &nominator_id,
1051            total_storage_fee_refund,
1052            Precision::Exact,
1053        )
1054        .map_err(|_| Error::RemoveLock)?;
1055
1056        Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1057            operator_id,
1058            nominator_id: nominator_id.clone(),
1059            storage_fee: total_storage_fee_refund,
1060        });
1061
1062        // if there are no withdrawals, then delete the storage as well
1063        if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1064            *maybe_withdrawal = None;
1065            // if there is no deposit or pending deposits, then clean up the deposit state as well
1066            Deposits::<T>::mutate_exists(operator_id, nominator_id.clone(), |maybe_deposit| {
1067                if let Some(deposit) = maybe_deposit
1068                    && deposit.known.shares.is_zero()
1069                    && deposit.pending.is_none()
1070                {
1071                    *maybe_deposit = None;
1072
1073                    DepositOnHold::<T>::mutate_exists(
1074                        (operator_id, nominator_id),
1075                        |maybe_deposit_on_hold| {
1076                            if let Some(deposit_on_hold) = maybe_deposit_on_hold
1077                                && deposit_on_hold.is_zero()
1078                            {
1079                                *maybe_deposit_on_hold = None
1080                            }
1081                        },
1082                    );
1083                }
1084            });
1085        }
1086
1087        Ok(withdrawal_count)
1088    })
1089}
1090
1091/// Unlocks an already de-registered operator's nominator given unlock wait period is complete.
1092pub(crate) fn do_unlock_nominator<T: Config>(
1093    operator_id: OperatorId,
1094    nominator_id: NominatorId<T>,
1095) -> Result<(), Error> {
1096    Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
1097        // take the operator so this operator info is removed once we unlock the operator.
1098        let mut operator = maybe_operator.take().ok_or(Error::UnknownOperator)?;
1099        let OperatorDeregisteredInfo {
1100            domain_epoch,
1101            unlock_at_confirmed_domain_block_number,
1102        } = match operator.status::<T>(operator_id) {
1103            OperatorStatus::Deregistered(operator_deregistered_info) => operator_deregistered_info,
1104            _ => return Err(Error::OperatorNotDeregistered),
1105        };
1106
1107        let (domain_id, _) = domain_epoch.deconstruct();
1108        let head_domain_number = HeadDomainNumber::<T>::get(domain_id);
1109        ensure!(
1110            *unlock_at_confirmed_domain_block_number <= head_domain_number,
1111            Error::UnlockPeriodNotComplete
1112        );
1113
1114        let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1115            .ok_or(Error::DomainNotInitialized)?
1116            .current_epoch_index;
1117
1118        let mut total_shares = operator.current_total_shares;
1119        let mut total_stake = operator.current_total_stake;
1120        let share_price = SharePrice::new::<T>(total_shares, total_stake)?;
1121
1122        let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
1123        let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1124            operator_id,
1125            total_storage_fee_deposit,
1126        );
1127        let mut deposit = Deposits::<T>::take(operator_id, nominator_id.clone())
1128            .ok_or(Error::UnknownNominator)?;
1129
1130        // convert any deposits from the previous epoch to shares.
1131        // share prices will always be present because
1132        // - if there are any deposits before operator de-registered, we ensure to create a
1133        //   share price for them at the time of epoch transition.
1134        // - if the operator got rewarded after the being de-registered and due to nomination tax
1135        //   operator self deposits said tax amount, we calculate share price at the time of epoch transition.
1136        do_convert_previous_epoch_deposits::<T>(
1137            operator_id,
1138            &mut deposit,
1139            current_domain_epoch_index,
1140        )?;
1141
1142        // if there are any withdrawals from this operator, account for them
1143        // if the withdrawals has share price noted, then convert them to AI3
1144        let (
1145            amount_ready_to_withdraw,
1146            total_storage_fee_withdrawal,
1147            shares_withdrew_in_current_epoch,
1148        ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
1149            .map(|mut withdrawal| {
1150                // convert any withdrawals from the previous epoch to stake.
1151                // share prices will always be present because
1152                // - if there are any withdrawals before operator de-registered, we ensure to create a
1153                //   share price for them at the time of epoch transition.
1154                do_convert_previous_epoch_withdrawal::<T>(
1155                    operator_id,
1156                    &mut withdrawal,
1157                    current_domain_epoch_index,
1158                )?;
1159                Ok((
1160                    withdrawal.total_withdrawal_amount,
1161                    withdrawal.total_storage_fee_withdrawal,
1162                    withdrawal
1163                        .withdrawal_in_shares
1164                        .map(|WithdrawalInShares { shares, .. }| shares)
1165                        .unwrap_or_default(),
1166                ))
1167            })
1168            .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
1169
1170        // include all the known shares and shares that were withdrawn in the current epoch
1171        let nominator_shares = deposit
1172            .known
1173            .shares
1174            .checked_add(&shares_withdrew_in_current_epoch)
1175            .ok_or(Error::ShareOverflow)?;
1176        total_shares = total_shares
1177            .checked_sub(&nominator_shares)
1178            .ok_or(Error::ShareOverflow)?;
1179
1180        // current staked amount
1181        let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
1182        total_stake = total_stake
1183            .checked_sub(&nominator_staked_amount)
1184            .ok_or(Error::BalanceOverflow)?;
1185
1186        // amount deposited by this nominator before operator de-registered.
1187        let amount_deposited_in_epoch = deposit
1188            .pending
1189            .map(|pending_deposit| pending_deposit.amount)
1190            .unwrap_or_default();
1191
1192        let total_amount_to_unlock = nominator_staked_amount
1193            .checked_add(&amount_ready_to_withdraw)
1194            .and_then(|amount| amount.checked_add(&amount_deposited_in_epoch))
1195            .ok_or(Error::BalanceOverflow)?;
1196
1197        // Remove the lock and mint any gains
1198        let current_locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
1199        if let Some(amount_to_mint) = total_amount_to_unlock.checked_sub(&current_locked_amount) {
1200            mint_funds::<T>(&nominator_id, amount_to_mint)?;
1201        }
1202        if !current_locked_amount.is_zero() {
1203            let staked_hold_id = T::HoldIdentifier::staking_staked();
1204            T::Currency::release(
1205                &staked_hold_id,
1206                &nominator_id,
1207                current_locked_amount,
1208                Precision::Exact,
1209            )
1210            .map_err(|_| Error::RemoveLock)?;
1211        }
1212
1213        Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1214            operator_id,
1215            nominator_id: nominator_id.clone(),
1216            unlocked_amount: total_amount_to_unlock,
1217        });
1218
1219        // Withdraw all storage fee for the nominator
1220        let nominator_total_storage_fee_deposit = deposit
1221            .pending
1222            .map(|pending_deposit| pending_deposit.storage_fee_deposit)
1223            .unwrap_or(Zero::zero())
1224            .checked_add(&deposit.known.storage_fee_deposit)
1225            .ok_or(Error::BalanceOverflow)?;
1226
1227        bundle_storage_fund::withdraw_to::<T>(
1228            operator_id,
1229            &nominator_id,
1230            storage_fund_redeem_price.redeem(nominator_total_storage_fee_deposit),
1231        )
1232        .map_err(Error::BundleStorageFund)?;
1233
1234        // Release all storage fee on withdraw of the nominator
1235        let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1236        T::Currency::release(
1237            &storage_fund_hold_id,
1238            &nominator_id,
1239            total_storage_fee_withdrawal,
1240            Precision::Exact,
1241        )
1242        .map_err(|_| Error::RemoveLock)?;
1243
1244        Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1245            operator_id,
1246            nominator_id: nominator_id.clone(),
1247            storage_fee: total_storage_fee_withdrawal,
1248        });
1249
1250        // reduce total storage fee deposit with nominator total fee deposit
1251        total_storage_fee_deposit =
1252            total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1253
1254        // The operator state is safe to cleanup if there is no entry in `Deposits` and `Withdrawals`
1255        // which means all nominator (including the operator owner) have unlocked their stake.
1256        let cleanup_operator = !Deposits::<T>::contains_prefix(operator_id)
1257            && !Withdrawals::<T>::contains_prefix(operator_id);
1258
1259        if cleanup_operator {
1260            do_cleanup_operator::<T>(operator_id, total_stake)?
1261        } else {
1262            // set update total shares, total stake and total storage fee deposit for operator
1263            operator.current_total_shares = total_shares;
1264            operator.current_total_stake = total_stake;
1265            operator.total_storage_fee_deposit = total_storage_fee_deposit;
1266
1267            *maybe_operator = Some(operator);
1268        }
1269
1270        Ok(())
1271    })
1272}
1273
1274/// Removes all operator storages and mints the total stake back to treasury.
1275pub(crate) fn do_cleanup_operator<T: Config>(
1276    operator_id: OperatorId,
1277    total_stake: BalanceOf<T>,
1278) -> Result<(), Error> {
1279    // transfer any remaining storage fund to treasury
1280    bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1281        .map_err(Error::BundleStorageFund)?;
1282
1283    // transfer any remaining amount to treasury
1284    mint_into_treasury::<T>(total_stake)?;
1285
1286    // remove OperatorOwner Details
1287    OperatorIdOwner::<T>::remove(operator_id);
1288
1289    // remove `OperatorHighestSlot`
1290    OperatorHighestSlot::<T>::remove(operator_id);
1291
1292    // remove operator epoch share prices
1293    let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1294
1295    Ok(())
1296}
1297
1298/// Distribute the reward to the operators equally and drop any dust to treasury.
1299pub(crate) fn do_reward_operators<T: Config>(
1300    domain_id: DomainId,
1301    source: OperatorRewardSource<BlockNumberFor<T>>,
1302    operators: IntoIter<OperatorId>,
1303    rewards: BalanceOf<T>,
1304) -> Result<(), Error> {
1305    if rewards.is_zero() {
1306        return Ok(());
1307    }
1308    DomainStakingSummary::<T>::mutate(domain_id, |maybe_stake_summary| {
1309        let stake_summary = maybe_stake_summary
1310            .as_mut()
1311            .ok_or(Error::DomainNotInitialized)?;
1312
1313        let total_count = operators.len() as u64;
1314        // calculate the operator weights based on the number of times they are repeated in the original list.
1315        let operator_weights = operators.into_iter().fold(
1316            BTreeMap::<OperatorId, u64>::new(),
1317            |mut acc, operator_id| {
1318                acc.entry(operator_id)
1319                    .and_modify(|weight| *weight += 1)
1320                    .or_insert(1);
1321                acc
1322            },
1323        );
1324
1325        let mut allocated_rewards = BalanceOf::<T>::zero();
1326        for (operator_id, weight) in operator_weights {
1327            let operator_reward = {
1328                let distribution = Perquintill::from_rational(weight, total_count);
1329                distribution.mul_floor(rewards)
1330            };
1331
1332            stake_summary
1333                .current_epoch_rewards
1334                .entry(operator_id)
1335                .and_modify(|rewards| *rewards = rewards.saturating_add(operator_reward))
1336                .or_insert(operator_reward);
1337
1338            Pallet::<T>::deposit_event(Event::OperatorRewarded {
1339                source: source.clone(),
1340                operator_id,
1341                reward: operator_reward,
1342            });
1343
1344            allocated_rewards = allocated_rewards
1345                .checked_add(&operator_reward)
1346                .ok_or(Error::BalanceOverflow)?;
1347        }
1348
1349        // mint remaining funds to treasury
1350        mint_into_treasury::<T>(
1351            rewards
1352                .checked_sub(&allocated_rewards)
1353                .ok_or(Error::BalanceUnderflow)?,
1354        )
1355    })
1356}
1357
1358/// Freezes the slashed operators and moves the operator to be removed once the domain they are
1359/// operating finishes the epoch.
1360pub(crate) fn do_mark_operators_as_slashed<T: Config>(
1361    operator_ids: impl AsRef<[OperatorId]>,
1362    slash_reason: SlashedReason<DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
1363) -> Result<(), Error> {
1364    for operator_id in operator_ids.as_ref() {
1365        Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1366            let operator = match maybe_operator.as_mut() {
1367                // If the operator is already slashed and removed due to fraud proof, when the operator
1368                // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1369                // not find the operator here thus just return.
1370                None => return Ok(()),
1371                Some(operator) => operator,
1372            };
1373            let mut pending_slashes =
1374                PendingSlashes::<T>::get(operator.current_domain_id).unwrap_or_default();
1375
1376            if pending_slashes.contains(operator_id) {
1377                return Ok(());
1378            }
1379
1380            DomainStakingSummary::<T>::try_mutate(
1381                operator.current_domain_id,
1382                |maybe_domain_stake_summary| {
1383                    let stake_summary = maybe_domain_stake_summary
1384                        .as_mut()
1385                        .ok_or(Error::DomainNotInitialized)?;
1386
1387                    // slash and remove operator from next and current epoch set
1388                    operator.update_status(OperatorStatus::Slashed);
1389
1390                    // ensure to reduce the total stake if operator is actually present in the
1391                    // current_operator set
1392                    if stake_summary
1393                        .current_operators
1394                        .remove(operator_id)
1395                        .is_some()
1396                    {
1397                        stake_summary.current_total_stake = stake_summary
1398                            .current_total_stake
1399                            .checked_sub(&operator.current_total_stake)
1400                            .ok_or(Error::BalanceUnderflow)?;
1401                    }
1402                    stake_summary.next_operators.remove(operator_id);
1403                    pending_slashes.insert(*operator_id);
1404                    PendingSlashes::<T>::insert(operator.current_domain_id, pending_slashes);
1405                    Pallet::<T>::deposit_event(Event::OperatorSlashed {
1406                        operator_id: *operator_id,
1407                        reason: slash_reason.clone(),
1408                    });
1409                    Ok(())
1410                },
1411            )
1412        })?
1413    }
1414
1415    Ok(())
1416}
1417
1418/// Mark all the invalid bundle authors from this ER and remove them from operator set.
1419pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1420    domain_id: DomainId,
1421    er: &ExecutionReceiptOf<T>,
1422) -> Result<(), Error> {
1423    let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1424    let er_hash = er.hash::<DomainHashingFor<T>>();
1425    let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1426    let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1427    let mut stake_summary =
1428        DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1429
1430    for operator_id in invalid_bundle_authors {
1431        if pending_slashes.contains(&operator_id) {
1432            continue;
1433        }
1434
1435        mark_invalid_bundle_author::<T>(
1436            operator_id,
1437            er_hash,
1438            &mut stake_summary,
1439            &mut invalid_bundle_authors_in_epoch,
1440        )?;
1441    }
1442
1443    DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1444    InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1445    Ok(())
1446}
1447
1448pub(crate) fn mark_invalid_bundle_author<T: Config>(
1449    operator_id: OperatorId,
1450    er_hash: ReceiptHashFor<T>,
1451    stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1452    invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1453) -> Result<(), Error> {
1454    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1455        let operator = match maybe_operator.as_mut() {
1456            // If the operator is already slashed and removed due to fraud proof, when the operator
1457            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1458            // not find the operator here thus just return.
1459            None => return Ok(()),
1460            Some(operator) => operator,
1461        };
1462
1463        // operator must be in registered status.
1464        // for other states, we anyway do not allow bundle submission.
1465        if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1466            return Ok(());
1467        }
1468
1469        // slash and remove operator from next and current epoch set
1470        operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1471        invalid_bundle_authors.insert(operator_id);
1472        if stake_summary
1473            .current_operators
1474            .remove(&operator_id)
1475            .is_some()
1476        {
1477            stake_summary.current_total_stake = stake_summary
1478                .current_total_stake
1479                .checked_sub(&operator.current_total_stake)
1480                .ok_or(Error::BalanceUnderflow)?;
1481        }
1482        stake_summary.next_operators.remove(&operator_id);
1483        Ok(())
1484    })
1485}
1486
1487/// Unmark all the invalid bundle authors from this ER that were marked invalid.
1488/// Assumed the ER is invalid and add the marked operators as registered and add them
1489/// back to next operator set.
1490pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1491    domain_id: DomainId,
1492    er: &ExecutionReceiptOf<T>,
1493) -> Result<(), Error> {
1494    let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1495    let er_hash = er.hash::<DomainHashingFor<T>>();
1496    let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1497    let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1498    let mut stake_summary =
1499        DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1500
1501    for operator_id in invalid_bundle_authors {
1502        if pending_slashes.contains(&operator_id)
1503            || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1504        {
1505            continue;
1506        }
1507
1508        unmark_invalid_bundle_author::<T>(
1509            operator_id,
1510            er_hash,
1511            &mut stake_summary,
1512            &mut invalid_bundle_authors_in_epoch,
1513        )?;
1514    }
1515
1516    DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1517    InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1518    Ok(())
1519}
1520
1521fn unmark_invalid_bundle_author<T: Config>(
1522    operator_id: OperatorId,
1523    er_hash: ReceiptHashFor<T>,
1524    stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1525    invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1526) -> Result<(), Error> {
1527    Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1528        let operator = match maybe_operator.as_mut() {
1529            // If the operator is already slashed and removed due to fraud proof, when the operator
1530            // is slash again due to invalid bundle, which happen after the ER is confirmed, we can
1531            // not find the operator here thus just return.
1532            None => return Ok(()),
1533            Some(operator) => operator,
1534        };
1535
1536        // operator must be in invalid bundle state with the exact er
1537        if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1538            return Ok(());
1539        }
1540
1541        // add operator to next set
1542        operator.update_status(OperatorStatus::Registered);
1543        invalid_bundle_authors.remove(&operator_id);
1544        stake_summary.next_operators.insert(operator_id);
1545        Ok(())
1546    })
1547}
1548
1549#[cfg(test)]
1550pub(crate) mod tests {
1551    use crate::domain_registry::{DomainConfig, DomainObject};
1552    use crate::pallet::{
1553        Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1554        NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1555    };
1556    use crate::staking::{
1557        DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorStatus, SharePrice,
1558        StakingSummary, do_convert_previous_epoch_withdrawal, do_mark_operators_as_slashed,
1559        do_nominate_operator, do_reward_operators, do_unlock_funds, do_withdraw_stake,
1560    };
1561    use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1562    use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext};
1563    use crate::{
1564        BalanceOf, Error, MAX_NOMINATORS_TO_SLASH, NominatorId, OperatorEpochSharePrice,
1565        SlashedReason, bundle_storage_fund,
1566    };
1567    use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
1568    use frame_support::traits::Currency;
1569    use frame_support::traits::fungible::Mutate;
1570    use frame_support::weights::Weight;
1571    use frame_support::{assert_err, assert_ok};
1572    use prop_test::prelude::*;
1573    use prop_test::proptest::test_runner::TestCaseResult;
1574    use sp_core::{Pair, sr25519};
1575    use sp_domains::{
1576        DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1577        OperatorRewardSource,
1578    };
1579    use sp_runtime::traits::Zero;
1580    use sp_runtime::{PerThing, Percent, Perquintill};
1581    use std::collections::{BTreeMap, BTreeSet};
1582    use std::ops::RangeInclusive;
1583    use std::vec;
1584    use subspace_runtime_primitives::AI3;
1585
1586    type Balances = pallet_balances::Pallet<Test>;
1587    type Domains = crate::Pallet<Test>;
1588
1589    const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1590
1591    #[allow(clippy::too_many_arguments)]
1592    pub(crate) fn register_operator(
1593        domain_id: DomainId,
1594        operator_account: <Test as frame_system::Config>::AccountId,
1595        operator_free_balance: BalanceOf<Test>,
1596        operator_stake: BalanceOf<Test>,
1597        minimum_nominator_stake: BalanceOf<Test>,
1598        signing_key: OperatorPublicKey,
1599        nomination_tax: Percent,
1600        mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1601    ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1602        nominators.insert(operator_account, (operator_free_balance, operator_stake));
1603        for nominator in &nominators {
1604            Balances::set_balance(nominator.0, nominator.1.0);
1605            assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1606        }
1607        nominators.remove(&operator_account);
1608
1609        if !DomainRegistry::<Test>::contains_key(domain_id) {
1610            let domain_config = DomainConfig {
1611                domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1612                runtime_id: 0,
1613                max_bundle_size: u32::MAX,
1614                max_bundle_weight: Weight::MAX,
1615                bundle_slot_probability: (0, 0),
1616                operator_allow_list: OperatorAllowList::Anyone,
1617                initial_balances: Default::default(),
1618            };
1619
1620            let domain_obj = DomainObject {
1621                owner_account_id: 0,
1622                created_at: 0,
1623                genesis_receipt_hash: Default::default(),
1624                domain_config,
1625                domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1626                domain_instantiation_deposit: Default::default(),
1627            };
1628
1629            DomainRegistry::<Test>::insert(domain_id, domain_obj);
1630        }
1631
1632        if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1633            DomainStakingSummary::<Test>::insert(
1634                domain_id,
1635                StakingSummary {
1636                    current_epoch_index: 0,
1637                    current_total_stake: 0,
1638                    current_operators: BTreeMap::new(),
1639                    next_operators: BTreeSet::new(),
1640                    current_epoch_rewards: BTreeMap::new(),
1641                },
1642            );
1643        }
1644
1645        let operator_config = OperatorConfig {
1646            signing_key,
1647            minimum_nominator_stake,
1648            nomination_tax,
1649        };
1650
1651        let res = Domains::register_operator(
1652            RuntimeOrigin::signed(operator_account),
1653            domain_id,
1654            operator_stake,
1655            operator_config.clone(),
1656        );
1657        assert_ok!(res);
1658
1659        let operator_id = NextOperatorId::<Test>::get() - 1;
1660        for nominator in nominators {
1661            if nominator.1.1.is_zero() {
1662                continue;
1663            }
1664
1665            let res = Domains::nominate_operator(
1666                RuntimeOrigin::signed(nominator.0),
1667                operator_id,
1668                nominator.1.1,
1669            );
1670            assert_ok!(res);
1671            assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1672        }
1673
1674        (operator_id, operator_config)
1675    }
1676
1677    #[test]
1678    fn test_register_operator_invalid_signing_key() {
1679        let domain_id = DomainId::new(0);
1680        let operator_account = 1;
1681
1682        let mut ext = new_test_ext();
1683        ext.execute_with(|| {
1684            let operator_config = OperatorConfig {
1685                signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1686                minimum_nominator_stake: Default::default(),
1687                nomination_tax: Default::default(),
1688            };
1689
1690            let res = Domains::register_operator(
1691                RuntimeOrigin::signed(operator_account),
1692                domain_id,
1693                Default::default(),
1694                operator_config,
1695            );
1696            assert_err!(
1697                res,
1698                Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1699            );
1700        });
1701    }
1702
1703    #[test]
1704    fn test_register_operator_minimum_nominator_stake() {
1705        let domain_id = DomainId::new(0);
1706        let operator_account = 1;
1707        let pair = OperatorPair::from_seed(&[0; 32]);
1708
1709        let mut ext = new_test_ext();
1710        ext.execute_with(|| {
1711            let operator_config = OperatorConfig {
1712                signing_key: pair.public(),
1713                minimum_nominator_stake: Default::default(),
1714                nomination_tax: Default::default(),
1715            };
1716
1717            let res = Domains::register_operator(
1718                RuntimeOrigin::signed(operator_account),
1719                domain_id,
1720                Default::default(),
1721                operator_config,
1722            );
1723            assert_err!(
1724                res,
1725                Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1726            );
1727        });
1728    }
1729
1730    #[test]
1731    fn test_register_operator() {
1732        let domain_id = DomainId::new(0);
1733        let operator_account = 1;
1734        let operator_free_balance = 2500 * AI3;
1735        let operator_total_stake = 1000 * AI3;
1736        let operator_stake = 800 * AI3;
1737        let operator_storage_fee_deposit = 200 * AI3;
1738        let pair = OperatorPair::from_seed(&[0; 32]);
1739
1740        let mut ext = new_test_ext();
1741        ext.execute_with(|| {
1742            let (operator_id, mut operator_config) = register_operator(
1743                domain_id,
1744                operator_account,
1745                operator_free_balance,
1746                operator_total_stake,
1747                AI3,
1748                pair.public(),
1749                Default::default(),
1750                BTreeMap::new(),
1751            );
1752
1753            assert_eq!(NextOperatorId::<Test>::get(), 1);
1754            // operator_id should be 0 and be registered
1755            assert_eq!(
1756                OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1757                operator_account
1758            );
1759            assert_eq!(
1760                Operators::<Test>::get(operator_id).unwrap(),
1761                Operator {
1762                    signing_key: pair.public(),
1763                    current_domain_id: domain_id,
1764                    next_domain_id: domain_id,
1765                    minimum_nominator_stake: AI3,
1766                    nomination_tax: Default::default(),
1767                    current_total_stake: operator_stake,
1768                    current_total_shares: operator_stake,
1769                    partial_status: OperatorStatus::Registered,
1770                    deposits_in_epoch: 0,
1771                    withdrawals_in_epoch: 0,
1772                    total_storage_fee_deposit: operator_storage_fee_deposit,
1773                }
1774            );
1775
1776            let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1777            assert!(stake_summary.next_operators.contains(&operator_id));
1778            assert_eq!(stake_summary.current_total_stake, operator_stake);
1779
1780            assert_eq!(
1781                Balances::usable_balance(operator_account),
1782                operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1783            );
1784
1785            // registering with same operator key is allowed
1786            let res = Domains::register_operator(
1787                RuntimeOrigin::signed(operator_account),
1788                domain_id,
1789                operator_stake,
1790                operator_config.clone(),
1791            );
1792            assert_ok!(res);
1793
1794            // cannot use the locked funds to register a new operator
1795            let new_pair = OperatorPair::from_seed(&[1; 32]);
1796            operator_config.signing_key = new_pair.public();
1797            let res = Domains::register_operator(
1798                RuntimeOrigin::signed(operator_account),
1799                domain_id,
1800                operator_stake,
1801                operator_config,
1802            );
1803            assert_err!(
1804                res,
1805                Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1806            );
1807        });
1808    }
1809
1810    #[test]
1811    fn nominate_operator() {
1812        let domain_id = DomainId::new(0);
1813        let operator_account = 1;
1814        let operator_free_balance = 1500 * AI3;
1815        let operator_total_stake = 1000 * AI3;
1816        let operator_stake = 800 * AI3;
1817        let operator_storage_fee_deposit = 200 * AI3;
1818        let pair = OperatorPair::from_seed(&[0; 32]);
1819
1820        let nominator_account = 2;
1821        let nominator_free_balance = 150 * AI3;
1822        let nominator_total_stake = 100 * AI3;
1823        let nominator_stake = 80 * AI3;
1824        let nominator_storage_fee_deposit = 20 * AI3;
1825
1826        let mut ext = new_test_ext();
1827        ext.execute_with(|| {
1828            let (operator_id, _) = register_operator(
1829                domain_id,
1830                operator_account,
1831                operator_free_balance,
1832                operator_total_stake,
1833                10 * AI3,
1834                pair.public(),
1835                Default::default(),
1836                BTreeMap::from_iter(vec![(
1837                    nominator_account,
1838                    (nominator_free_balance, nominator_total_stake),
1839                )]),
1840            );
1841
1842            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1843            assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
1844
1845            let operator = Operators::<Test>::get(operator_id).unwrap();
1846            assert_eq!(operator.current_total_stake, operator_stake);
1847            assert_eq!(operator.current_total_shares, operator_stake);
1848            assert_eq!(
1849                operator.total_storage_fee_deposit,
1850                operator_storage_fee_deposit + nominator_storage_fee_deposit
1851            );
1852            assert_eq!(operator.deposits_in_epoch, nominator_stake);
1853
1854            let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1855                .unwrap()
1856                .pending
1857                .unwrap();
1858            assert_eq!(pending_deposit.amount, nominator_stake);
1859            assert_eq!(
1860                pending_deposit.storage_fee_deposit,
1861                nominator_storage_fee_deposit
1862            );
1863            assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
1864
1865            assert_eq!(
1866                Balances::usable_balance(nominator_account),
1867                nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
1868            );
1869
1870            // another transfer with an existing transfer in place should lead to single
1871            let additional_nomination_total_stake = 40 * AI3;
1872            let additional_nomination_stake = 32 * AI3;
1873            let additional_nomination_storage_fee_deposit = 8 * AI3;
1874            let res = Domains::nominate_operator(
1875                RuntimeOrigin::signed(nominator_account),
1876                operator_id,
1877                additional_nomination_total_stake,
1878            );
1879            assert_ok!(res);
1880            let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1881                .unwrap()
1882                .pending
1883                .unwrap();
1884            assert_eq!(
1885                pending_deposit.amount,
1886                nominator_stake + additional_nomination_stake
1887            );
1888            assert_eq!(
1889                pending_deposit.storage_fee_deposit,
1890                nominator_storage_fee_deposit + additional_nomination_storage_fee_deposit
1891            );
1892
1893            let operator = Operators::<Test>::get(operator_id).unwrap();
1894            assert_eq!(operator.current_total_stake, operator_stake);
1895            assert_eq!(
1896                operator.deposits_in_epoch,
1897                nominator_stake + additional_nomination_stake
1898            );
1899            assert_eq!(
1900                operator.total_storage_fee_deposit,
1901                operator_storage_fee_deposit
1902                    + nominator_storage_fee_deposit
1903                    + additional_nomination_storage_fee_deposit
1904            );
1905
1906            // do epoch transition
1907            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
1908
1909            let operator = Operators::<Test>::get(operator_id).unwrap();
1910            assert_eq!(
1911                operator.current_total_stake,
1912                operator_stake + nominator_stake + additional_nomination_stake
1913            );
1914
1915            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1916            assert_eq!(
1917                domain_staking_summary.current_total_stake,
1918                operator_stake + nominator_stake + additional_nomination_stake
1919            );
1920        });
1921    }
1922
1923    #[test]
1924    fn operator_deregistration() {
1925        let domain_id = DomainId::new(0);
1926        let operator_account = 1;
1927        let operator_stake = 200 * AI3;
1928        let operator_free_balance = 250 * AI3;
1929        let pair = OperatorPair::from_seed(&[0; 32]);
1930        let mut ext = new_test_ext();
1931        ext.execute_with(|| {
1932            let (operator_id, _) = register_operator(
1933                domain_id,
1934                operator_account,
1935                operator_free_balance,
1936                operator_stake,
1937                AI3,
1938                pair.public(),
1939                Default::default(),
1940                BTreeMap::new(),
1941            );
1942
1943            let res =
1944                Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
1945            assert_ok!(res);
1946
1947            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1948            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
1949
1950            let operator = Operators::<Test>::get(operator_id).unwrap();
1951            assert_eq!(
1952                *operator.status::<Test>(operator_id),
1953                OperatorStatus::Deregistered(
1954                    (
1955                        domain_id,
1956                        domain_stake_summary.current_epoch_index,
1957                        // since the Withdrawals locking period is 5 and confirmed domain block is 0
1958                        5
1959                    )
1960                        .into()
1961                )
1962            );
1963
1964            // operator nomination will not work since the operator is already de-registered
1965            let new_domain_id = DomainId::new(1);
1966            let domain_config = DomainConfig {
1967                domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1968                runtime_id: 0,
1969                max_bundle_size: u32::MAX,
1970                max_bundle_weight: Weight::MAX,
1971                bundle_slot_probability: (0, 0),
1972                operator_allow_list: OperatorAllowList::Anyone,
1973                initial_balances: Default::default(),
1974            };
1975
1976            let domain_obj = DomainObject {
1977                owner_account_id: 0,
1978                created_at: 0,
1979                genesis_receipt_hash: Default::default(),
1980                domain_config,
1981                domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1982                domain_instantiation_deposit: Default::default(),
1983            };
1984
1985            DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
1986            DomainStakingSummary::<Test>::insert(
1987                new_domain_id,
1988                StakingSummary {
1989                    current_epoch_index: 0,
1990                    current_total_stake: 0,
1991                    current_operators: BTreeMap::new(),
1992                    next_operators: BTreeSet::new(),
1993                    current_epoch_rewards: BTreeMap::new(),
1994                },
1995            );
1996
1997            // nominations will not work since the is frozen
1998            let nominator_account = 100;
1999            let nominator_stake = 100 * AI3;
2000            let res = Domains::nominate_operator(
2001                RuntimeOrigin::signed(nominator_account),
2002                operator_id,
2003                nominator_stake,
2004            );
2005            assert_err!(
2006                res,
2007                Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2008            );
2009        });
2010    }
2011
2012    type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2013
2014    /// Expected withdrawal amount.
2015    /// Bool indicates to include existential deposit while asserting the final balance
2016    /// since ED is not holded back from usable balance when there are no holds on the account.
2017    type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2018
2019    /// The storage fund change in AI3, `true` means increase of the storage fund, `false` means decrease.
2020    type StorageFundChange = (bool, u32);
2021
2022    pub(crate) type Share = <Test as Config>::Share;
2023
2024    struct WithdrawParams {
2025        /// The minimum valid nominator stake.
2026        minimum_nominator_stake: BalanceOf<Test>,
2027        /// The nominator IDs and their stakes.
2028        /// Account 0 is the operator and its stake.
2029        nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2030        /// The operator reward.
2031        operator_reward: BalanceOf<Test>,
2032        /// The nominator ID to withdraw from.
2033        nominator_id: NominatorId<Test>,
2034        /// The withdraw attempts to be made, in order, with their expected results.
2035        withdraws: WithdrawWithResult,
2036        /// The deposit to be made when nominating the operator, if any.
2037        maybe_deposit: Option<BalanceOf<Test>>,
2038        /// The expected withdraw amount for `nominator_id`.
2039        /// Includes the existential deposit if `true`.
2040        expected_withdraw: ExpectedWithdrawAmount,
2041        /// The expected reduction in the number of nominators.
2042        expected_nominator_count_reduced_by: u32,
2043        /// The storage fund change, increase if `true`.
2044        storage_fund_change: StorageFundChange,
2045    }
2046
2047    /// Withdraw stake in a non-proptest.
2048    fn withdraw_stake(params: WithdrawParams) {
2049        withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2050    }
2051
2052    /// Withdraw stake in a proptest.
2053    fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2054        withdraw_stake_inner(params, true)
2055    }
2056
2057    /// Inner function for withdrawing stake.
2058    fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2059        let WithdrawParams {
2060            minimum_nominator_stake,
2061            nominators,
2062            operator_reward,
2063            nominator_id,
2064            withdraws,
2065            maybe_deposit,
2066            expected_withdraw,
2067            expected_nominator_count_reduced_by,
2068            storage_fund_change,
2069        } = params;
2070        let domain_id = DomainId::new(0);
2071        let operator_account = 0;
2072        let pair = OperatorPair::from_seed(&[0; 32]);
2073        let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2074            + operator_reward
2075            + maybe_deposit.unwrap_or(0);
2076
2077        let mut nominators = BTreeMap::from_iter(
2078            nominators
2079                .into_iter()
2080                .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2081                .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2082        );
2083
2084        let mut ext = new_test_ext();
2085        ext.execute_with(|| {
2086            let (operator_free_balance, operator_stake) =
2087                nominators.remove(&operator_account).unwrap();
2088            let (operator_id, _) = register_operator(
2089                domain_id,
2090                operator_account,
2091                operator_free_balance,
2092                operator_stake,
2093                minimum_nominator_stake,
2094                pair.public(),
2095                Default::default(),
2096                nominators,
2097            );
2098
2099            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2100
2101            if !operator_reward.is_zero() {
2102                do_reward_operators::<Test>(
2103                    domain_id,
2104                    OperatorRewardSource::Dummy,
2105                    vec![operator_id].into_iter(),
2106                    operator_reward,
2107                )
2108                .unwrap();
2109            }
2110
2111            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2112
2113            if let Some(deposit_amount) = maybe_deposit {
2114                Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2115                let res = Domains::nominate_operator(
2116                    RuntimeOrigin::signed(nominator_id),
2117                    operator_id,
2118                    deposit_amount,
2119                );
2120                assert_ok!(res);
2121            }
2122
2123            let operator = Operators::<Test>::get(operator_id).unwrap();
2124            let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2125            if is_storage_fund_increased {
2126                bundle_storage_fund::refund_storage_fee::<Test>(
2127                    storage_fund_change_amount as u128 * AI3,
2128                    BTreeMap::from_iter([(operator_id, 1)]),
2129                )
2130                .unwrap();
2131                assert_eq!(
2132                    operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2133                    bundle_storage_fund::total_balance::<Test>(operator_id)
2134                );
2135                total_balance += storage_fund_change_amount as u128 * AI3;
2136            } else {
2137                bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2138                    operator_id,
2139                    storage_fund_change_amount,
2140                )
2141                .unwrap();
2142                assert_eq!(
2143                    operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2144                    bundle_storage_fund::total_balance::<Test>(operator_id)
2145                );
2146                total_balance -= storage_fund_change_amount as u128 * AI3;
2147            }
2148
2149            for (withdraw, expected_result) in withdraws {
2150                let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2151                let res = Domains::withdraw_stake(
2152                    RuntimeOrigin::signed(nominator_id),
2153                    operator_id,
2154                    withdraw_share_amount,
2155                );
2156                if is_proptest {
2157                    prop_assert_eq!(
2158                        res,
2159                        expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2160                        "unexpected withdraw_stake result",
2161                    );
2162                } else {
2163                    assert_eq!(
2164                        res,
2165                        expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2166                        "unexpected withdraw_stake result",
2167                    );
2168                }
2169            }
2170
2171            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2172
2173            if let Some((withdraw, include_ed)) = expected_withdraw {
2174                let previous_usable_balance = Balances::usable_balance(nominator_id);
2175
2176                // Update `HeadDomainNumber` to ensure unlock success
2177                HeadDomainNumber::<Test>::set(
2178                    domain_id,
2179                    head_domain_number
2180                        + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2181                );
2182                assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2183
2184                let expected_balance = if include_ed {
2185                    total_balance += crate::tests::ExistentialDeposit::get();
2186                    previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get()
2187                } else {
2188                    previous_usable_balance + withdraw
2189                };
2190
2191                if is_proptest {
2192                    prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2193                } else {
2194                    assert_eq!(
2195                        Balances::usable_balance(nominator_id),
2196                        expected_balance,
2197                        "usable balance is not equal to expected balance:\n{} !=\n{}",
2198                        Balances::usable_balance(nominator_id),
2199                        expected_balance,
2200                    );
2201                }
2202
2203                // ensure there are no withdrawals left
2204                assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2205            }
2206
2207            // if the nominator count reduced, then there should be no storage for deposits as well
2208            // TODO: assert this matches the change in the number of nominators
2209            if expected_nominator_count_reduced_by > 0 {
2210                if is_proptest {
2211                    prop_assert!(
2212                        Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2213                        "deposit exists for nominator: {nominator_id}",
2214                    );
2215                    prop_assert!(
2216                        !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2217                        "deposit on hold exists for nominator: {nominator_id}",
2218                    );
2219                } else {
2220                    assert!(
2221                        Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2222                        "deposit exists for nominator: {nominator_id}",
2223                    );
2224                    assert!(
2225                        !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2226                        "deposit on hold exists for nominator: {nominator_id}",
2227                    );
2228                }
2229            }
2230
2231            // The total balance is distributed in different places but never changed.
2232            let operator = Operators::<Test>::get(operator_id).unwrap();
2233            if is_proptest {
2234                prop_assert_approx!(
2235                    Balances::usable_balance(nominator_id)
2236                        + operator.current_total_stake
2237                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2238                    total_balance,
2239                    "\n{} + {} + {} =",
2240                    Balances::usable_balance(nominator_id),
2241                    operator.current_total_stake,
2242                    bundle_storage_fund::total_balance::<Test>(operator_id),
2243                );
2244            } else {
2245                assert_eq!(
2246                    Balances::usable_balance(nominator_id)
2247                        + operator.current_total_stake
2248                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2249                    total_balance,
2250                    "initial total balance is not equal to final total balance:\n\
2251                    {} + {} + {} =\n{} !=\n{}",
2252                    Balances::usable_balance(nominator_id),
2253                    operator.current_total_stake,
2254                    bundle_storage_fund::total_balance::<Test>(operator_id),
2255                    Balances::usable_balance(nominator_id)
2256                        + operator.current_total_stake
2257                        + bundle_storage_fund::total_balance::<Test>(operator_id),
2258                    total_balance,
2259                );
2260            }
2261
2262            Ok(())
2263        })
2264    }
2265
2266    /// Do an approximate amount comparison in a proptest.
2267    /// Takes actual and expected amounts, and an optional extra format string and arguments.
2268    ///
2269    /// The user should never get more, and they shouldn't get significantly less.
2270    /// Accounts for both absolute and relative rounding errors.
2271    macro_rules! prop_assert_approx {
2272            ($actual:expr, $expected:expr $(,)?) => {
2273                prop_assert_approx!($actual, $expected, "");
2274            };
2275
2276            ($actual:expr, $expected:expr, $fmt:expr) => {
2277                prop_assert_approx!($actual, $expected, $fmt,);
2278            };
2279
2280            ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2281                let actual = $actual;
2282                let expected = $expected;
2283                let extra = format!($fmt, $($args)*);
2284                prop_test::proptest::prop_assert!(
2285                    actual <= expected,
2286                    "extra minting: actual amount is greater than expected amount:{}{}\
2287                    \n{} >\
2288                    \n{}",
2289                    if extra.is_empty() { "" } else { "\n" },
2290                    extra,
2291                    actual,
2292                    expected,
2293                );
2294                let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2295                    .mul_floor(expected)
2296                    .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2297                prop_test::proptest::prop_assert!(
2298                    actual >= expected_rounded_down,
2299                    "excess rounding losses: actual amount is less than expected amount \
2300                    rounded down:{}{}\
2301                    \n{} <\
2302                    \n{} (from\
2303                    \n{})",
2304                    if extra.is_empty() { "" } else { "\n" },
2305                    extra,
2306                    actual,
2307                    expected_rounded_down,
2308                    expected,
2309                );
2310            }};
2311        }
2312
2313    // Export the macro for use in other modules (and earlier in this file).
2314    pub(crate) use prop_assert_approx;
2315
2316    /// Rounding down factor for property tests to account for arithmetic precision errors.
2317    /// This factor is used to allow for small rounding errors in calculations.
2318    // Perquintill::from_parts(...).left_from_one(), as a constant.
2319    pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2320        Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2321
2322    /// Absolute rounding error tolerance for property tests.
2323    /// This constant defines the maximum acceptable absolute rounding error in calculations.
2324    pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2325
2326    /// The maximum balance we test for in property tests.
2327    /// This balance should be just below the maximum possible issuance.
2328    ///
2329    /// Limiting the balances avoids arithmetic errors in the withdraw_stake test function, and
2330    /// RemoveLock (likely converted from BalanceOverflow) in the staking functions.
2331    //
2332    // TODO: fix the code so we can get closer to 2^128
2333    pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2334
2335    /// The minimum operator stake we test for in property tests.
2336    // TODO: edit the test harness so we can go as low as MinOperatorStake + 1
2337    pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2338
2339    /// The minimum nominator stake we test for in property tests.
2340    pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2341        <Test as Config>::MinNominatorStake::get() + 1;
2342
2343    /// The range of operator stakes we test for in property tests.
2344    pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2345        MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2346
2347    /// The range of nominator stakes we test for in property tests.
2348    pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2349        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2350
2351    /// The range of operator or nominator deposits we test for in property tests.
2352    // TODO: edit the test harness so we can go as low as zero
2353    pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2354        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2355
2356    /// The range of operator rewards we test for in property tests.
2357    pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2358
2359    /// The range of operator free balances we test for in property tests.
2360    pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2361        MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2362
2363    // Using too many random parameters and prop_assume()s can reduce test coverage.
2364    // Try to limit the number of parameters to 3.
2365
2366    /// Property test for withdrawing an operator's excess stake.
2367    /// Their balance should be almost the same before and after.
2368    #[test]
2369    fn prop_withdraw_excess_operator_stake() {
2370        prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2371            let mut excess_stake =
2372                operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2373
2374            // Account for both absolute and relative rounding errors.
2375            excess_stake = Perquintill::from_parts(1)
2376                .left_from_one()
2377                .mul_ceil(excess_stake);
2378            excess_stake -= 1;
2379
2380            prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2381
2382            let expected_withdraw = (excess_stake, false);
2383
2384            withdraw_stake_prop(WithdrawParams {
2385                minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2386                nominators: vec![(0, operator_stake)],
2387                operator_reward: 0,
2388                nominator_id: 0,
2389                withdraws: vec![(excess_stake, Ok(()))],
2390                maybe_deposit: None,
2391                expected_withdraw: Some(expected_withdraw),
2392                expected_nominator_count_reduced_by: 0,
2393                storage_fund_change: (true, 0),
2394            })
2395        });
2396    }
2397
2398    /// Property test for withdrawing all a nominator's excess stake.
2399    /// Their balance should be almost the same before and after.
2400    #[test]
2401    fn prop_withdraw_excess_operator_stake_with_nominator() {
2402        prop_test!(
2403            &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2404            |(operator_stake, nominator_stake)| {
2405                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2406                prop_assert!(
2407                    [operator_stake, nominator_stake]
2408                        .into_iter()
2409                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2410                        .is_some()
2411                );
2412
2413                let expected_withdraw = (nominator_stake, true);
2414
2415                let excess_stake = expected_withdraw
2416                    .0
2417                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
2418
2419                prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2420
2421                withdraw_stake_prop(WithdrawParams {
2422                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2423                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
2424                    operator_reward: 0,
2425                    nominator_id: 1,
2426                    withdraws: vec![(excess_stake, Ok(()))],
2427                    maybe_deposit: None,
2428                    expected_withdraw: Some(expected_withdraw),
2429                    expected_nominator_count_reduced_by: 1,
2430                    storage_fund_change: (true, 0),
2431                })
2432            }
2433        );
2434    }
2435
2436    /// Property test for withdrawing an operator's excess stake with a deposit.
2437    /// Their balance should be almost the same before and after.
2438    #[test]
2439    fn prop_withdraw_excess_operator_stake_with_deposit() {
2440        prop_test!(
2441            &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2442            |(mut operator_stake, maybe_deposit)| {
2443                operator_stake = operator_stake.saturating_add(maybe_deposit);
2444
2445                prop_assert!(
2446                    operator_stake.saturating_sub(maybe_deposit)
2447                        >= <Test as Config>::MinOperatorStake::get(),
2448                    "would cause MinimumOperatorStake error"
2449                );
2450
2451                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2452                prop_assert!(
2453                    [operator_stake, maybe_deposit]
2454                        .into_iter()
2455                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2456                        .is_some()
2457                );
2458
2459                let expected_withdraw = (
2460                    // TODO: work out how to avoid this multiplication on WithdrawParams.withdraws
2461                    STORAGE_FEE_RESERVE
2462                        .left_from_one()
2463                        .mul_ceil(operator_stake)
2464                        .saturating_sub(maybe_deposit),
2465                    true,
2466                );
2467
2468                let excess_stake = expected_withdraw
2469                    .0
2470                    .saturating_sub(<Test as Config>::MinOperatorStake::get());
2471
2472                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2473
2474                // Avoid ZeroDeposit errors
2475                let maybe_deposit = if maybe_deposit == 0 {
2476                    None
2477                } else {
2478                    Some(maybe_deposit)
2479                };
2480
2481                withdraw_stake_prop(WithdrawParams {
2482                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2483                    nominators: vec![(0, operator_stake)],
2484                    operator_reward: 0,
2485                    nominator_id: 0,
2486                    withdraws: vec![(excess_stake, Ok(()))],
2487                    maybe_deposit,
2488                    expected_withdraw: Some(expected_withdraw),
2489                    expected_nominator_count_reduced_by: 0,
2490                    storage_fund_change: (true, 0),
2491                })
2492            }
2493        );
2494    }
2495
2496    /// Property test for withdrawing an operator's excess stake with a deposit and fixed
2497    /// operator stake.
2498    /// Their balance should be almost the same before and after.
2499    #[test]
2500    fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
2501        prop_test!(
2502            &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2503            |(mut nominator_stake, maybe_deposit)| {
2504                nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2505
2506                prop_assert!(
2507                    nominator_stake.saturating_sub(maybe_deposit)
2508                        >= <Test as Config>::MinNominatorStake::get(),
2509                    "would cause MinimumNominatorStake error"
2510                );
2511
2512                // MinimumOperatorStake error
2513                let operator_stake = <Test as Config>::MinOperatorStake::get();
2514
2515                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2516                prop_assert!(
2517                    [operator_stake, nominator_stake, maybe_deposit]
2518                        .into_iter()
2519                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2520                        .is_some()
2521                );
2522
2523                let expected_withdraw = (
2524                    STORAGE_FEE_RESERVE
2525                        .left_from_one()
2526                        .mul_ceil(nominator_stake)
2527                        .saturating_sub(maybe_deposit),
2528                    true,
2529                );
2530
2531                let excess_stake = expected_withdraw
2532                    .0
2533                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
2534
2535                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2536
2537                // Avoid ZeroDeposit errors
2538                let maybe_deposit = if maybe_deposit == 0 {
2539                    None
2540                } else {
2541                    Some(maybe_deposit)
2542                };
2543
2544                withdraw_stake_prop(WithdrawParams {
2545                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2546                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
2547                    operator_reward: 0,
2548                    nominator_id: 1,
2549                    withdraws: vec![(excess_stake, Ok(()))],
2550                    maybe_deposit,
2551                    expected_withdraw: Some(expected_withdraw),
2552                    expected_nominator_count_reduced_by: 0,
2553                    storage_fund_change: (true, 0),
2554                })
2555            }
2556        );
2557    }
2558
2559    /// Property test for withdrawing a nominator's excess stake with a deposit.
2560    /// Their balance should be almost the same before and after.
2561    #[test]
2562    fn prop_withdraw_excess_nominator_stake_with_deposit() {
2563        prop_test!(
2564            &(
2565                PROP_OPERATOR_STAKE_RANGE,
2566                PROP_NOMINATOR_STAKE_RANGE,
2567                PROP_DEPOSIT_RANGE,
2568            ),
2569            |(operator_stake, mut nominator_stake, maybe_deposit)| {
2570                nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2571
2572                prop_assert!(
2573                    nominator_stake.saturating_sub(maybe_deposit)
2574                        >= <Test as Config>::MinNominatorStake::get(),
2575                    "would cause MinimumNominatorStake error"
2576                );
2577
2578                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2579                prop_assert!(
2580                    [operator_stake, nominator_stake, maybe_deposit]
2581                        .into_iter()
2582                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2583                        .is_some()
2584                );
2585
2586                // Some deposit gets left as storage fees.
2587                let expected_withdraw = (
2588                    STORAGE_FEE_RESERVE
2589                        .left_from_one()
2590                        .mul_ceil(nominator_stake)
2591                        .saturating_sub(maybe_deposit),
2592                    true,
2593                );
2594
2595                let excess_stake = expected_withdraw
2596                    .0
2597                    .saturating_sub(<Test as Config>::MinNominatorStake::get());
2598
2599                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2600
2601                // Avoid ZeroDeposit errors
2602                let maybe_deposit = if maybe_deposit == 0 {
2603                    None
2604                } else {
2605                    Some(maybe_deposit)
2606                };
2607
2608                withdraw_stake_prop(WithdrawParams {
2609                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2610                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
2611                    operator_reward: 0,
2612                    nominator_id: 1,
2613                    withdraws: vec![(excess_stake, Ok(()))],
2614                    maybe_deposit,
2615                    expected_withdraw: Some(expected_withdraw),
2616                    expected_nominator_count_reduced_by: 0,
2617                    storage_fund_change: (true, 0),
2618                })
2619            }
2620        );
2621    }
2622
2623    /// Property test for withdrawing an operator's excess stake with a nominator and a reward.
2624    /// The total balance should be increased by part of the reward afterwards.
2625    #[test]
2626    fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
2627        prop_test!(
2628            &(
2629                PROP_OPERATOR_STAKE_RANGE,
2630                PROP_NOMINATOR_STAKE_RANGE,
2631                PROP_REWARD_RANGE,
2632            ),
2633            |(operator_stake, nominator_stake, operator_reward)| {
2634                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2635                prop_assert!(
2636                    [operator_stake, nominator_stake, operator_reward]
2637                        .into_iter()
2638                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2639                        .is_some()
2640                );
2641
2642                // Withdraw is stake plus reward split.
2643                let total_stake = operator_stake.saturating_add(nominator_stake);
2644                let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
2645                    .mul_floor(operator_reward);
2646
2647                // Shares are approximately equal to the original stake.
2648                // TODO: fix the tests so we can go as low as MinOperatorStake
2649                let excess_stake =
2650                    operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
2651
2652                let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
2653
2654                prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2655
2656                withdraw_stake_prop(WithdrawParams {
2657                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2658                    nominators: vec![(0, operator_stake), (1, nominator_stake)],
2659                    operator_reward,
2660                    nominator_id: 0,
2661                    withdraws: vec![(excess_stake, Ok(()))],
2662                    maybe_deposit: None,
2663                    expected_withdraw: Some(expected_withdraw),
2664                    expected_nominator_count_reduced_by: 0,
2665                    storage_fund_change: (true, 0),
2666                })
2667            }
2668        );
2669    }
2670
2671    /// Property test for withdrawing an operator's excess stake with a deposit and a reward.
2672    /// Their balance should be almost the same before and after.
2673    #[test]
2674    fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
2675        prop_test!(
2676            &(
2677                PROP_OPERATOR_STAKE_RANGE,
2678                PROP_DEPOSIT_RANGE,
2679                PROP_REWARD_RANGE,
2680            ),
2681            |(mut operator_stake, maybe_deposit, operator_reward)| {
2682                operator_stake = operator_stake.saturating_add(maybe_deposit);
2683
2684                prop_assert!(
2685                    operator_stake.saturating_sub(maybe_deposit)
2686                        >= <Test as Config>::MinOperatorStake::get(),
2687                    "would cause MinimumOperatorStake error"
2688                );
2689
2690                // Total balances can't overflow: arithmetic overflow error in withdraw_stake test function
2691                prop_assert!(
2692                    [operator_stake, maybe_deposit, operator_reward]
2693                        .into_iter()
2694                        .try_fold(0_u128, |acc, value| acc.checked_add(value))
2695                        .is_some()
2696                );
2697
2698                // Shares are approximately equal to the original stake.
2699                // TODO: fix the tests so we can go as low as MinOperatorStake
2700                let reserve = 2 * <Test as Config>::MinOperatorStake::get();
2701                let excess_stake = operator_stake.saturating_sub(reserve);
2702
2703                // Withdraw is stake plus reward, but some deposit gets left as storage fees.
2704                let expected_withdraw = (
2705                    operator_stake
2706                        .saturating_add(operator_reward)
2707                        .saturating_sub(reserve),
2708                    true,
2709                );
2710
2711                prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2712
2713                // Avoid ZeroDeposit errors
2714                let maybe_deposit = if maybe_deposit == 0 {
2715                    None
2716                } else {
2717                    Some(maybe_deposit)
2718                };
2719
2720                withdraw_stake_prop(WithdrawParams {
2721                    minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2722                    nominators: vec![(0, operator_stake)],
2723                    operator_reward,
2724                    nominator_id: 0,
2725                    withdraws: vec![(excess_stake, Ok(()))],
2726                    maybe_deposit,
2727                    expected_withdraw: Some(expected_withdraw),
2728                    expected_nominator_count_reduced_by: 0,
2729                    storage_fund_change: (true, 0),
2730                })
2731            }
2732        );
2733    }
2734
2735    #[test]
2736    fn withdraw_stake_operator_all() {
2737        withdraw_stake(WithdrawParams {
2738            minimum_nominator_stake: 10 * AI3,
2739            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2740            operator_reward: 20 * AI3,
2741            nominator_id: 0,
2742            withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
2743            maybe_deposit: None,
2744            expected_withdraw: None,
2745            expected_nominator_count_reduced_by: 0,
2746            storage_fund_change: (true, 0),
2747        })
2748    }
2749
2750    #[test]
2751    fn withdraw_stake_operator_below_minimum() {
2752        withdraw_stake(WithdrawParams {
2753            minimum_nominator_stake: 10 * AI3,
2754            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2755            operator_reward: 20 * AI3,
2756            nominator_id: 0,
2757            withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
2758            maybe_deposit: None,
2759            expected_withdraw: None,
2760            expected_nominator_count_reduced_by: 0,
2761            storage_fund_change: (true, 0),
2762        })
2763    }
2764
2765    #[test]
2766    fn withdraw_stake_operator_below_minimum_no_rewards() {
2767        withdraw_stake(WithdrawParams {
2768            minimum_nominator_stake: 10 * AI3,
2769            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2770            operator_reward: Zero::zero(),
2771            nominator_id: 0,
2772            withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
2773            maybe_deposit: None,
2774            expected_withdraw: None,
2775            expected_nominator_count_reduced_by: 0,
2776            storage_fund_change: (true, 0),
2777        })
2778    }
2779
2780    #[test]
2781    fn withdraw_stake_operator_above_minimum() {
2782        withdraw_stake(WithdrawParams {
2783            minimum_nominator_stake: 10 * AI3,
2784            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2785            operator_reward: 20 * AI3,
2786            nominator_id: 0,
2787            withdraws: vec![(58 * AI3, Ok(()))],
2788            // given the reward, operator will get 164.28 AI3
2789            // taking 58 shares will give this following approximate amount.
2790            maybe_deposit: None,
2791            expected_withdraw: Some((63523809523809523770, false)),
2792            expected_nominator_count_reduced_by: 0,
2793            storage_fund_change: (true, 0),
2794        })
2795    }
2796
2797    #[test]
2798    fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
2799        withdraw_stake(WithdrawParams {
2800            minimum_nominator_stake: 10 * AI3,
2801            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2802            operator_reward: 20 * AI3,
2803            nominator_id: 0,
2804            withdraws: vec![
2805                (58 * AI3, Ok(())),
2806                (5 * AI3, Err(StakingError::MinimumOperatorStake)),
2807            ],
2808            maybe_deposit: None,
2809            expected_withdraw: Some((63523809523809523770, false)),
2810            expected_nominator_count_reduced_by: 0,
2811            storage_fund_change: (true, 0),
2812        })
2813    }
2814
2815    #[test]
2816    fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
2817        withdraw_stake(WithdrawParams {
2818            minimum_nominator_stake: 10 * AI3,
2819            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2820            operator_reward: 20 * AI3,
2821            nominator_id: 0,
2822            withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
2823            maybe_deposit: None,
2824            expected_withdraw: Some((63523809523809523769, false)),
2825            expected_nominator_count_reduced_by: 0,
2826            storage_fund_change: (true, 0),
2827        })
2828    }
2829
2830    #[test]
2831    fn withdraw_stake_operator_above_minimum_no_rewards() {
2832        withdraw_stake(WithdrawParams {
2833            minimum_nominator_stake: 10 * AI3,
2834            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2835            operator_reward: Zero::zero(),
2836            nominator_id: 0,
2837            withdraws: vec![(49 * AI3, Ok(()))],
2838            maybe_deposit: None,
2839            expected_withdraw: Some((48999999999999999980, false)),
2840            expected_nominator_count_reduced_by: 0,
2841            storage_fund_change: (true, 0),
2842        })
2843    }
2844
2845    #[test]
2846    fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
2847        withdraw_stake(WithdrawParams {
2848            minimum_nominator_stake: 10 * AI3,
2849            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2850            operator_reward: Zero::zero(),
2851            nominator_id: 0,
2852            withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
2853            maybe_deposit: None,
2854            expected_withdraw: Some((48999999999999999981, false)),
2855            expected_nominator_count_reduced_by: 0,
2856            storage_fund_change: (true, 0),
2857        })
2858    }
2859
2860    #[test]
2861    fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
2862        withdraw_stake(WithdrawParams {
2863            minimum_nominator_stake: 10 * AI3,
2864            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2865            operator_reward: Zero::zero(),
2866            nominator_id: 0,
2867            withdraws: vec![
2868                (29 * AI3, Ok(())),
2869                (20 * AI3, Ok(())),
2870                (20 * AI3, Err(StakingError::MinimumOperatorStake)),
2871            ],
2872            maybe_deposit: None,
2873            expected_withdraw: Some((48999999999999999981, false)),
2874            expected_nominator_count_reduced_by: 0,
2875            storage_fund_change: (true, 0),
2876        })
2877    }
2878
2879    #[test]
2880    fn withdraw_stake_nominator_below_minimum_with_rewards() {
2881        withdraw_stake(WithdrawParams {
2882            minimum_nominator_stake: 10 * AI3,
2883            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2884            operator_reward: 20 * AI3,
2885            nominator_id: 1,
2886            withdraws: vec![(45 * AI3, Ok(()))],
2887            // given nominator remaining stake goes below minimum
2888            // we withdraw everything, so for their 50 shares with reward,
2889            // price would be following
2890            maybe_deposit: None,
2891            expected_withdraw: Some((54761904761904761888, true)),
2892            expected_nominator_count_reduced_by: 1,
2893            storage_fund_change: (true, 0),
2894        })
2895    }
2896
2897    #[test]
2898    fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
2899        withdraw_stake(WithdrawParams {
2900            minimum_nominator_stake: 10 * AI3,
2901            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2902            operator_reward: 20 * AI3,
2903            nominator_id: 1,
2904            withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2905            // given nominator remaining stake goes below minimum
2906            // we withdraw everything, so for their 50 shares with reward,
2907            // price would be following
2908            maybe_deposit: None,
2909            expected_withdraw: Some((54761904761904761888, true)),
2910            expected_nominator_count_reduced_by: 1,
2911            storage_fund_change: (true, 0),
2912        })
2913    }
2914
2915    #[test]
2916    fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
2917        withdraw_stake(WithdrawParams {
2918            minimum_nominator_stake: 10 * AI3,
2919            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2920            operator_reward: 20 * AI3,
2921            nominator_id: 1,
2922            withdraws: vec![
2923                (25 * AI3, Ok(())),
2924                (20 * AI3, Ok(())),
2925                (20 * AI3, Err(StakingError::InsufficientShares)),
2926            ],
2927            // given nominator remaining stake goes below minimum
2928            // we withdraw everything, so for their 50 shares with reward,
2929            // price would be following
2930            maybe_deposit: None,
2931            expected_withdraw: Some((54761904761904761888, true)),
2932            expected_nominator_count_reduced_by: 1,
2933            storage_fund_change: (true, 0),
2934        })
2935    }
2936
2937    #[test]
2938    fn withdraw_stake_nominator_below_minimum_no_reward() {
2939        withdraw_stake(WithdrawParams {
2940            minimum_nominator_stake: 10 * AI3,
2941            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2942            operator_reward: Zero::zero(),
2943            nominator_id: 1,
2944            withdraws: vec![(45 * AI3, Ok(()))],
2945            maybe_deposit: None,
2946            expected_withdraw: Some((50 * AI3, true)),
2947            expected_nominator_count_reduced_by: 1,
2948            storage_fund_change: (true, 0),
2949        })
2950    }
2951
2952    #[test]
2953    fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
2954        withdraw_stake(WithdrawParams {
2955            minimum_nominator_stake: 10 * AI3,
2956            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2957            operator_reward: Zero::zero(),
2958            nominator_id: 1,
2959            withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2960            maybe_deposit: None,
2961            expected_withdraw: Some((50 * AI3, true)),
2962            expected_nominator_count_reduced_by: 1,
2963            storage_fund_change: (true, 0),
2964        })
2965    }
2966
2967    #[test]
2968    fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
2969        withdraw_stake(WithdrawParams {
2970            minimum_nominator_stake: 10 * AI3,
2971            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2972            operator_reward: Zero::zero(),
2973            nominator_id: 1,
2974            withdraws: vec![
2975                (25 * AI3, Ok(())),
2976                (20 * AI3, Ok(())),
2977                (20 * AI3, Err(StakingError::InsufficientShares)),
2978            ],
2979            maybe_deposit: None,
2980            expected_withdraw: Some((50 * AI3, true)),
2981            expected_nominator_count_reduced_by: 1,
2982            storage_fund_change: (true, 0),
2983        })
2984    }
2985
2986    #[test]
2987    fn withdraw_stake_nominator_above_minimum() {
2988        withdraw_stake(WithdrawParams {
2989            minimum_nominator_stake: 10 * AI3,
2990            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2991            operator_reward: 20 * AI3,
2992            nominator_id: 1,
2993            withdraws: vec![(40 * AI3, Ok(()))],
2994            maybe_deposit: None,
2995            expected_withdraw: Some((43809523809523809511, false)),
2996            expected_nominator_count_reduced_by: 0,
2997            storage_fund_change: (true, 0),
2998        })
2999    }
3000
3001    #[test]
3002    fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
3003        withdraw_stake(WithdrawParams {
3004            minimum_nominator_stake: 10 * AI3,
3005            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3006            operator_reward: 20 * AI3,
3007            nominator_id: 1,
3008            withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3009            maybe_deposit: None,
3010            expected_withdraw: Some((43809523809523809510, false)),
3011            expected_nominator_count_reduced_by: 0,
3012            storage_fund_change: (true, 0),
3013        })
3014    }
3015
3016    #[test]
3017    fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3018        withdraw_stake(WithdrawParams {
3019            minimum_nominator_stake: 10 * AI3,
3020            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3021            operator_reward: 20 * AI3,
3022            nominator_id: 1,
3023            withdraws: vec![
3024                (35 * AI3, Ok(())),
3025                (5 * AI3, Ok(())),
3026                (15 * AI3, Err(StakingError::InsufficientShares)),
3027            ],
3028            maybe_deposit: None,
3029            expected_withdraw: Some((43809523809523809510, false)),
3030            expected_nominator_count_reduced_by: 0,
3031            storage_fund_change: (true, 0),
3032        })
3033    }
3034
3035    #[test]
3036    fn withdraw_stake_nominator_above_minimum_no_rewards() {
3037        withdraw_stake(WithdrawParams {
3038            minimum_nominator_stake: 10 * AI3,
3039            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3040            operator_reward: Zero::zero(),
3041            nominator_id: 1,
3042            withdraws: vec![(39 * AI3, Ok(()))],
3043            maybe_deposit: None,
3044            expected_withdraw: Some((39 * AI3, false)),
3045            expected_nominator_count_reduced_by: 0,
3046            storage_fund_change: (true, 0),
3047        })
3048    }
3049
3050    #[test]
3051    fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3052        withdraw_stake(WithdrawParams {
3053            minimum_nominator_stake: 10 * AI3,
3054            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3055            operator_reward: Zero::zero(),
3056            nominator_id: 1,
3057            withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3058            maybe_deposit: None,
3059            expected_withdraw: Some((39999999899999999998, false)),
3060            expected_nominator_count_reduced_by: 0,
3061            storage_fund_change: (true, 0),
3062        })
3063    }
3064
3065    #[test]
3066    fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3067        withdraw_stake(WithdrawParams {
3068            minimum_nominator_stake: 10 * AI3,
3069            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3070            operator_reward: Zero::zero(),
3071            nominator_id: 1,
3072            withdraws: vec![
3073                (35 * AI3, Ok(())),
3074                (5 * AI3 - 100000000000, Ok(())),
3075                (15 * AI3, Err(StakingError::InsufficientShares)),
3076            ],
3077            maybe_deposit: None,
3078            expected_withdraw: Some((39999999899999999998, false)),
3079            expected_nominator_count_reduced_by: 0,
3080            storage_fund_change: (true, 0),
3081        })
3082    }
3083
3084    #[test]
3085    fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3086        withdraw_stake(WithdrawParams {
3087            minimum_nominator_stake: 10 * AI3,
3088            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3089            operator_reward: Zero::zero(),
3090            nominator_id: 1,
3091            withdraws: vec![
3092                (35 * AI3, Ok(())),
3093                (5 * AI3 - 100000000000, Ok(())),
3094                (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3095            ],
3096            maybe_deposit: Some(2 * AI3),
3097            expected_withdraw: Some((39999999899999999998, false)),
3098            expected_nominator_count_reduced_by: 0,
3099            storage_fund_change: (true, 0),
3100        })
3101    }
3102
3103    #[test]
3104    fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3105        withdraw_stake(WithdrawParams {
3106            minimum_nominator_stake: 10 * AI3,
3107            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3108            operator_reward: 20 * AI3,
3109            nominator_id: 1,
3110            withdraws: vec![
3111                (35 * AI3, Ok(())),
3112                (5 * AI3, Ok(())),
3113                (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3114            ],
3115            // given nominator remaining stake goes below minimum
3116            // we withdraw everything, so for their 50 shares with reward,
3117            // price would be following
3118            maybe_deposit: Some(2 * AI3),
3119            expected_withdraw: Some((43809523809523809510, false)),
3120            expected_nominator_count_reduced_by: 0,
3121            storage_fund_change: (true, 0),
3122        })
3123    }
3124
3125    #[test]
3126    fn withdraw_stake_nominator_zero_amount() {
3127        withdraw_stake(WithdrawParams {
3128            minimum_nominator_stake: 10 * AI3,
3129            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3130            operator_reward: Zero::zero(),
3131            nominator_id: 1,
3132            withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3133            maybe_deposit: None,
3134            expected_withdraw: None,
3135            expected_nominator_count_reduced_by: 0,
3136            storage_fund_change: (true, 0),
3137        })
3138    }
3139
3140    #[test]
3141    fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3142        withdraw_stake(WithdrawParams {
3143            minimum_nominator_stake: 10 * AI3,
3144            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3145            operator_reward: Zero::zero(),
3146            nominator_id: 1,
3147            withdraws: vec![(50 * AI3, Ok(()))],
3148            maybe_deposit: None,
3149            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3150            // storage fee profit i.e. 5 * AI3 with rounding dust deducted
3151            storage_fund_change: (true, 21),
3152            expected_withdraw: Some((54999999999999999985, true)),
3153            expected_nominator_count_reduced_by: 1,
3154        })
3155    }
3156
3157    #[test]
3158    fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3159        withdraw_stake(WithdrawParams {
3160            minimum_nominator_stake: 10 * AI3,
3161            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3162            operator_reward: Zero::zero(),
3163            nominator_id: 1,
3164            withdraws: vec![(50 * AI3, Ok(()))],
3165            maybe_deposit: None,
3166            // The storage fund decreased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3167            // storage fee deposit i.e. 5 * AI3 with rounding dust deducted
3168            storage_fund_change: (false, 21),
3169            expected_withdraw: Some((44999999999999999995, true)),
3170            expected_nominator_count_reduced_by: 1,
3171        })
3172    }
3173
3174    #[test]
3175    fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3176        withdraw_stake(WithdrawParams {
3177            minimum_nominator_stake: 10 * AI3,
3178            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3179            operator_reward: Zero::zero(),
3180            nominator_id: 1,
3181            withdraws: vec![(50 * AI3, Ok(()))],
3182            maybe_deposit: None,
3183            // The storage fund decreased 100% (i.e. 42 * AI3) thus the nominator loss 100%
3184            // storage fee deposit i.e. 10 * AI3
3185            storage_fund_change: (false, 42),
3186            expected_withdraw: Some((40 * AI3, true)),
3187            expected_nominator_count_reduced_by: 1,
3188        })
3189    }
3190
3191    #[test]
3192    fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3193        withdraw_stake(WithdrawParams {
3194            minimum_nominator_stake: 10 * AI3,
3195            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3196            operator_reward: Zero::zero(),
3197            nominator_id: 1,
3198            withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3199            maybe_deposit: None,
3200            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator make 50%
3201            // storage fee profit i.e. 5 * AI3 with rounding dust deducted, withdraw 60% of
3202            // the stake and the storage fee profit
3203            storage_fund_change: (true, 21),
3204            expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3205            expected_nominator_count_reduced_by: 0,
3206        })
3207    }
3208
3209    #[test]
3210    fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3211        withdraw_stake(WithdrawParams {
3212            minimum_nominator_stake: 10 * AI3,
3213            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3214            operator_reward: Zero::zero(),
3215            nominator_id: 1,
3216            withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3217            maybe_deposit: None,
3218            // The storage fund increased 50% (i.e. 21 * AI3) thus the nominator loss 50%
3219            // storage fee i.e. 5 * AI3 with rounding dust deducted, withdraw 40% of
3220            // the stake and 40% of the storage fee loss are deducted
3221            storage_fund_change: (false, 21),
3222            expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3223            expected_nominator_count_reduced_by: 0,
3224        })
3225    }
3226
3227    #[test]
3228    fn unlock_multiple_withdrawals() {
3229        let domain_id = DomainId::new(0);
3230        let operator_account = 1;
3231        let operator_free_balance = 250 * AI3;
3232        let operator_stake = 200 * AI3;
3233        let pair = OperatorPair::from_seed(&[0; 32]);
3234        let nominator_account = 2;
3235        let nominator_free_balance = 150 * AI3;
3236        let nominator_stake = 100 * AI3;
3237
3238        let nominators = vec![
3239            (operator_account, (operator_free_balance, operator_stake)),
3240            (nominator_account, (nominator_free_balance, nominator_stake)),
3241        ];
3242
3243        let total_deposit = 300 * AI3;
3244        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3245        let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3246
3247        let mut ext = new_test_ext();
3248        ext.execute_with(|| {
3249            let (operator_id, _) = register_operator(
3250                domain_id,
3251                operator_account,
3252                operator_free_balance,
3253                operator_stake,
3254                10 * AI3,
3255                pair.public(),
3256                Default::default(),
3257                BTreeMap::from_iter(nominators),
3258            );
3259
3260            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3261            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3262            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3263
3264            let operator = Operators::<Test>::get(operator_id).unwrap();
3265            assert_eq!(operator.current_total_stake, init_total_stake);
3266            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3267            assert_eq!(
3268                operator.total_storage_fee_deposit,
3269                bundle_storage_fund::total_balance::<Test>(operator_id)
3270            );
3271
3272            // Guess that the number of shares will be approximately the same as the stake amount.
3273            let shares_per_withdraw = init_total_stake / 100;
3274            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3275
3276            // Request `WithdrawalLimit - 1` number of withdrawal
3277            for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3278                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3279                    .unwrap();
3280                do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3281            }
3282            // Increase the head domain number by 1
3283            HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3284
3285            // All withdrawals of a given nominator submitted in the same epoch will merge into one,
3286            // so we can submit as many as we want, even though the withdrawal limit is met.
3287            for _ in 0..5 {
3288                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3289                    .unwrap();
3290            }
3291            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3292
3293            // After the withdrawal limit is met, any new withdraw will be rejected in the next epoch
3294            assert_err!(
3295                do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3296                StakingError::TooManyWithdrawals
3297            );
3298            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3299            Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3300                let withdrawal = maybe_withdrawal.as_mut().unwrap();
3301                do_convert_previous_epoch_withdrawal::<Test>(
3302                    operator_id,
3303                    withdrawal,
3304                    domain_stake_summary.current_epoch_index,
3305                )
3306                .unwrap();
3307                assert_eq!(
3308                    withdrawal.withdrawals.len() as u32,
3309                    <Test as crate::Config>::WithdrawalLimit::get()
3310                );
3311                Ok::<(), StakingError>(())
3312            })
3313            .unwrap();
3314
3315            // Make the first set of withdrawals pass the unlock period then unlock fund
3316            HeadDomainNumber::<Test>::set(
3317                domain_id,
3318                head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3319            );
3320            let total_balance = Balances::usable_balance(nominator_account);
3321            assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3322            assert_eq!(
3323                Balances::usable_balance(nominator_account) + 74, // `74` is a minor rounding dust
3324                total_balance
3325                    + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3326                        / 100
3327            );
3328            let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3329            assert_eq!(withdrawal.withdrawals.len(), 1);
3330
3331            // Make the second set of withdrawals pass the unlock period then unlock funds
3332            HeadDomainNumber::<Test>::set(
3333                domain_id,
3334                head_domain_number
3335                    + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3336                    + 1,
3337            );
3338            let total_balance = Balances::usable_balance(nominator_account);
3339            assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3340            assert_eq!(
3341                Balances::usable_balance(nominator_account) - 2, // `2` is a minor rounding dust
3342                total_balance + 5 * total_deposit / 100
3343            );
3344            assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3345        });
3346    }
3347
3348    #[test]
3349    fn slash_operator() {
3350        let domain_id = DomainId::new(0);
3351        let operator_account = 1;
3352        let operator_free_balance = 250 * AI3;
3353        let operator_stake = 200 * AI3;
3354        let operator_extra_deposit = 40 * AI3;
3355        let pair = OperatorPair::from_seed(&[0; 32]);
3356        let nominator_account = 2;
3357        let nominator_free_balance = 150 * AI3;
3358        let nominator_stake = 100 * AI3;
3359        let nominator_extra_deposit = 40 * AI3;
3360
3361        let nominators = vec![
3362            (operator_account, (operator_free_balance, operator_stake)),
3363            (nominator_account, (nominator_free_balance, nominator_stake)),
3364        ];
3365
3366        let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3367
3368        let deposits = vec![
3369            (operator_account, operator_extra_deposit),
3370            (nominator_account, nominator_extra_deposit),
3371        ];
3372
3373        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3374        let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3375
3376        let mut ext = new_test_ext();
3377        ext.execute_with(|| {
3378            let (operator_id, _) = register_operator(
3379                domain_id,
3380                operator_account,
3381                operator_free_balance,
3382                operator_stake,
3383                10 * AI3,
3384                pair.public(),
3385                Default::default(),
3386                BTreeMap::from_iter(nominators),
3387            );
3388
3389            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3390            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3391            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3392
3393            let operator = Operators::<Test>::get(operator_id).unwrap();
3394            assert_eq!(operator.current_total_stake, init_total_stake);
3395            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3396            assert_eq!(
3397                operator.total_storage_fee_deposit,
3398                bundle_storage_fund::total_balance::<Test>(operator_id)
3399            );
3400
3401            for unlock in &unlocking {
3402                do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3403            }
3404
3405            do_reward_operators::<Test>(
3406                domain_id,
3407                OperatorRewardSource::Dummy,
3408                vec![operator_id].into_iter(),
3409                20 * AI3,
3410            )
3411            .unwrap();
3412            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3413
3414            // Manually convert previous withdrawal in share to balance
3415            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3416            for id in [operator_account, nominator_account] {
3417                Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3418                    do_convert_previous_epoch_withdrawal::<Test>(
3419                        operator_id,
3420                        maybe_withdrawal.as_mut().unwrap(),
3421                        domain_stake_summary.current_epoch_index,
3422                    )
3423                })
3424                .unwrap();
3425            }
3426
3427            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
3428            // due to withdrawal of 20 shares
3429            let operator = Operators::<Test>::get(operator_id).unwrap();
3430            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3431            let operator_withdrawal =
3432                Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3433            let nominator_withdrawal =
3434                Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3435
3436            let total_deposit =
3437                domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3438            let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3439                + nominator_withdrawal.total_withdrawal_amount;
3440            let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3441                .storage_fee_refund
3442                + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3443            assert_eq!(293333333333333333336, total_deposit,);
3444            assert_eq!(21666666666666666664, total_stake_withdrawal);
3445            assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3446            assert_eq!(
3447                320 * AI3,
3448                total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3449            );
3450            assert_eq!(
3451                operator.total_storage_fee_deposit,
3452                bundle_storage_fund::total_balance::<Test>(operator_id)
3453            );
3454
3455            for deposit in deposits {
3456                do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3457            }
3458
3459            do_mark_operators_as_slashed::<Test>(
3460                vec![operator_id],
3461                SlashedReason::InvalidBundle(1),
3462            )
3463            .unwrap();
3464
3465            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3466            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3467
3468            let operator = Operators::<Test>::get(operator_id).unwrap();
3469            assert_eq!(
3470                *operator.status::<Test>(operator_id),
3471                OperatorStatus::Slashed
3472            );
3473
3474            let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3475            assert!(pending_slashes.contains(&operator_id));
3476
3477            assert_eq!(
3478                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3479                0
3480            );
3481
3482            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3483            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3484            assert_eq!(Operators::<Test>::get(operator_id), None);
3485            assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3486
3487            assert_eq!(
3488                Balances::total_balance(&operator_account),
3489                operator_free_balance - operator_stake
3490            );
3491            assert_eq!(
3492                Balances::total_balance(&nominator_account),
3493                nominator_free_balance - nominator_stake
3494            );
3495
3496            assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3);
3497            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3498        });
3499    }
3500
3501    #[test]
3502    fn slash_operator_with_more_than_max_nominators_to_slash() {
3503        let domain_id = DomainId::new(0);
3504        let operator_account = 1;
3505        let operator_free_balance = 250 * AI3;
3506        let operator_stake = 200 * AI3;
3507        let operator_extra_deposit = 40 * AI3;
3508        let operator_extra_withdraw = 5 * AI3;
3509        let pair = OperatorPair::from_seed(&[0; 32]);
3510
3511        let nominator_accounts: Vec<crate::tests::AccountId> = (2..22).collect();
3512        let nominator_free_balance = 150 * AI3;
3513        let nominator_stake = 100 * AI3;
3514        let nominator_extra_deposit = 40 * AI3;
3515        let nominator_extra_withdraw = 5 * AI3;
3516
3517        let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
3518        for nominator_account in nominator_accounts.clone() {
3519            nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
3520        }
3521
3522        let last_nominator_account = nominator_accounts.last().cloned().unwrap();
3523        let unlocking = vec![
3524            (operator_account, 10 * AI3),
3525            (last_nominator_account, 10 * AI3),
3526        ];
3527
3528        let deposits = vec![
3529            (operator_account, operator_extra_deposit),
3530            (last_nominator_account, nominator_extra_deposit),
3531        ];
3532        let withdrawals = vec![
3533            (operator_account, operator_extra_withdraw),
3534            (last_nominator_account, nominator_extra_withdraw),
3535        ];
3536
3537        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
3538            * (200 + (100 * nominator_accounts.len() as u128))
3539            * AI3;
3540        let init_total_storage_fund =
3541            STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
3542
3543        let mut ext = new_test_ext();
3544        ext.execute_with(|| {
3545            let (operator_id, _) = register_operator(
3546                domain_id,
3547                operator_account,
3548                operator_free_balance,
3549                operator_stake,
3550                10 * AI3,
3551                pair.public(),
3552                Default::default(),
3553                BTreeMap::from_iter(nominators),
3554            );
3555
3556            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3557            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3558            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3559
3560            let operator = Operators::<Test>::get(operator_id).unwrap();
3561            assert_eq!(operator.current_total_stake, init_total_stake);
3562            assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3563            assert_eq!(
3564                operator.total_storage_fee_deposit,
3565                bundle_storage_fund::total_balance::<Test>(operator_id)
3566            );
3567
3568            for unlock in &unlocking {
3569                do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3570            }
3571
3572            do_reward_operators::<Test>(
3573                domain_id,
3574                OperatorRewardSource::Dummy,
3575                vec![operator_id].into_iter(),
3576                20 * AI3,
3577            )
3578            .unwrap();
3579            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3580
3581            // Manually convert previous withdrawal in share to balance
3582            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3583            for id in [operator_account, last_nominator_account] {
3584                Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3585                    do_convert_previous_epoch_withdrawal::<Test>(
3586                        operator_id,
3587                        maybe_withdrawal.as_mut().unwrap(),
3588                        domain_stake_summary.current_epoch_index,
3589                    )
3590                })
3591                .unwrap();
3592            }
3593
3594            // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced
3595            // due to withdrawal of 20 shares
3596            let operator = Operators::<Test>::get(operator_id).unwrap();
3597            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3598            let operator_withdrawal =
3599                Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3600            let nominator_withdrawal =
3601                Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
3602
3603            let total_deposit =
3604                domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3605            let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3606                + nominator_withdrawal.total_withdrawal_amount;
3607            let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3608                .storage_fee_refund
3609                + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3610            assert_eq!(2194772727272727272734, total_deposit,);
3611            assert_eq!(20227272727272727266, total_stake_withdrawal);
3612            assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3613            assert_eq!(
3614                2220 * AI3,
3615                total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3616            );
3617
3618            assert_eq!(
3619                operator.total_storage_fee_deposit,
3620                bundle_storage_fund::total_balance::<Test>(operator_id)
3621            );
3622
3623            for deposit in deposits {
3624                do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3625            }
3626            for withdrawal in withdrawals {
3627                do_withdraw_stake::<Test>(
3628                    operator_id,
3629                    withdrawal.0,
3630                    // Guess that the number of shares will be approximately the same as the stake
3631                    // amount.
3632                    withdrawal.1,
3633                )
3634                .unwrap();
3635            }
3636
3637            do_mark_operators_as_slashed::<Test>(
3638                vec![operator_id],
3639                SlashedReason::InvalidBundle(1),
3640            )
3641            .unwrap();
3642
3643            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3644
3645            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3646            assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3647
3648            let operator = Operators::<Test>::get(operator_id).unwrap();
3649            assert_eq!(
3650                *operator.status::<Test>(operator_id),
3651                OperatorStatus::Slashed
3652            );
3653
3654            let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3655            assert!(pending_slashes.contains(&operator_id));
3656
3657            assert_eq!(
3658                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3659                0
3660            );
3661
3662            // since we only slash 10 nominators a time but we have a total of 21 nominators,
3663            // do 3 iterations
3664            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3665            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3666            do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3667
3668            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3669            assert_eq!(Operators::<Test>::get(operator_id), None);
3670            assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3671
3672            assert_eq!(
3673                Balances::total_balance(&operator_account),
3674                operator_free_balance - operator_stake
3675            );
3676            for nominator_account in nominator_accounts {
3677                assert_eq!(
3678                    Balances::total_balance(&nominator_account),
3679                    nominator_free_balance - nominator_stake
3680                );
3681            }
3682
3683            assert_eq!(
3684                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3685                2220 * AI3
3686            );
3687            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3688        });
3689    }
3690
3691    #[test]
3692    fn slash_operators() {
3693        let domain_id = DomainId::new(0);
3694        let operator_free_balance = 250 * AI3;
3695        let operator_stake = 200 * AI3;
3696
3697        let operator_account_1 = 1;
3698        let operator_account_2 = 2;
3699        let operator_account_3 = 3;
3700
3701        let pair_1 = OperatorPair::from_seed(&[0; 32]);
3702        let pair_2 = OperatorPair::from_seed(&[1; 32]);
3703        let pair_3 = OperatorPair::from_seed(&[2; 32]);
3704
3705        let mut ext = new_test_ext();
3706        ext.execute_with(|| {
3707            let (operator_id_1, _) = register_operator(
3708                domain_id,
3709                operator_account_1,
3710                operator_free_balance,
3711                operator_stake,
3712                10 * AI3,
3713                pair_1.public(),
3714                Default::default(),
3715                Default::default(),
3716            );
3717
3718            let (operator_id_2, _) = register_operator(
3719                domain_id,
3720                operator_account_2,
3721                operator_free_balance,
3722                operator_stake,
3723                10 * AI3,
3724                pair_2.public(),
3725                Default::default(),
3726                Default::default(),
3727            );
3728
3729            let (operator_id_3, _) = register_operator(
3730                domain_id,
3731                operator_account_3,
3732                operator_free_balance,
3733                operator_stake,
3734                10 * AI3,
3735                pair_3.public(),
3736                Default::default(),
3737                Default::default(),
3738            );
3739
3740            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3741            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3742            assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
3743            assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
3744            assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
3745            assert_eq!(
3746                domain_stake_summary.current_total_stake,
3747                STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
3748            );
3749            for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3750                let operator = Operators::<Test>::get(operator_id).unwrap();
3751                assert_eq!(
3752                    operator.total_storage_fee_deposit,
3753                    STORAGE_FEE_RESERVE * operator_stake
3754                );
3755                assert_eq!(
3756                    operator.total_storage_fee_deposit,
3757                    bundle_storage_fund::total_balance::<Test>(operator_id)
3758                );
3759            }
3760
3761            do_mark_operators_as_slashed::<Test>(
3762                vec![operator_id_1],
3763                SlashedReason::InvalidBundle(1),
3764            )
3765            .unwrap();
3766            do_mark_operators_as_slashed::<Test>(
3767                vec![operator_id_2],
3768                SlashedReason::InvalidBundle(2),
3769            )
3770            .unwrap();
3771            do_mark_operators_as_slashed::<Test>(
3772                vec![operator_id_3],
3773                SlashedReason::InvalidBundle(3),
3774            )
3775            .unwrap();
3776
3777            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3778            assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
3779            assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
3780            assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
3781
3782            let operator = Operators::<Test>::get(operator_id_1).unwrap();
3783            assert_eq!(
3784                *operator.status::<Test>(operator_id_1),
3785                OperatorStatus::Slashed
3786            );
3787
3788            let operator = Operators::<Test>::get(operator_id_2).unwrap();
3789            assert_eq!(
3790                *operator.status::<Test>(operator_id_2),
3791                OperatorStatus::Slashed
3792            );
3793
3794            let operator = Operators::<Test>::get(operator_id_3).unwrap();
3795            assert_eq!(
3796                *operator.status::<Test>(operator_id_3),
3797                OperatorStatus::Slashed
3798            );
3799
3800            assert_eq!(
3801                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3802                0
3803            );
3804
3805            let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
3806            slashed_operators.into_iter().for_each(|_| {
3807                do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3808            });
3809
3810            assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3811            assert_eq!(Operators::<Test>::get(operator_id_1), None);
3812            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
3813            assert_eq!(Operators::<Test>::get(operator_id_2), None);
3814            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
3815            assert_eq!(Operators::<Test>::get(operator_id_3), None);
3816            assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
3817
3818            assert_eq!(
3819                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3820                600 * AI3
3821            );
3822            for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3823                assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3824            }
3825        });
3826    }
3827
3828    #[test]
3829    fn bundle_storage_fund_charged_and_refund_storage_fee() {
3830        let domain_id = DomainId::new(0);
3831        let operator_account = 1;
3832        let operator_free_balance = 150 * AI3;
3833        let operator_total_stake = 100 * AI3;
3834        let operator_stake = 80 * AI3;
3835        let operator_storage_fee_deposit = 20 * AI3;
3836        let pair = OperatorPair::from_seed(&[0; 32]);
3837        let nominator_account = 2;
3838
3839        let mut ext = new_test_ext();
3840        ext.execute_with(|| {
3841            let (operator_id, _) = register_operator(
3842                domain_id,
3843                operator_account,
3844                operator_free_balance,
3845                operator_total_stake,
3846                AI3,
3847                pair.public(),
3848                Default::default(),
3849                BTreeMap::default(),
3850            );
3851
3852            let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3853            assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
3854
3855            let operator = Operators::<Test>::get(operator_id).unwrap();
3856            assert_eq!(operator.current_total_stake, operator_stake);
3857            assert_eq!(operator.current_total_shares, operator_stake);
3858            assert_eq!(
3859                operator.total_storage_fee_deposit,
3860                operator_storage_fee_deposit
3861            );
3862
3863            // Drain the bundle storage fund
3864            bundle_storage_fund::charge_bundle_storage_fee::<Test>(
3865                operator_id,
3866                // the transaction fee is one AI3 per byte thus div AI3 here
3867                (operator_storage_fee_deposit / AI3) as u32,
3868            )
3869            .unwrap();
3870            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3871            assert_err!(
3872                bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
3873                bundle_storage_fund::Error::BundleStorageFeePayment
3874            );
3875
3876            // The operator add more stake thus add deposit to the bundle storage fund
3877            do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
3878            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3879
3880            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3881            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3882
3883            // New nominator add deposit to the bundle storage fund
3884            Balances::set_balance(&nominator_account, 100 * AI3);
3885            do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
3886            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3887
3888            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3889            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3890
3891            // Refund of the storage fee add deposit to the bundle storage fund
3892            bundle_storage_fund::refund_storage_fee::<Test>(
3893                10 * AI3,
3894                BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
3895            )
3896            .unwrap();
3897            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3898
3899            // The operator `operator_id + 1` not exist thus the refund storage fee added to treasury
3900            assert_eq!(
3901                Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3902                9 * AI3
3903            );
3904
3905            bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3906            assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3907        });
3908    }
3909
3910    #[test]
3911    fn zero_amount_deposit_and_withdraw() {
3912        let domain_id = DomainId::new(0);
3913        let operator_account = 1;
3914        let operator_free_balance = 250 * AI3;
3915        let operator_stake = 200 * AI3;
3916        let pair = OperatorPair::from_seed(&[0; 32]);
3917        let nominator_account = 2;
3918        let nominator_free_balance = 150 * AI3;
3919        let nominator_stake = 100 * AI3;
3920
3921        let nominators = vec![
3922            (operator_account, (operator_free_balance, operator_stake)),
3923            (nominator_account, (nominator_free_balance, nominator_stake)),
3924        ];
3925
3926        let total_deposit = 300 * AI3;
3927        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3928
3929        let mut ext = new_test_ext();
3930        ext.execute_with(|| {
3931            let (operator_id, _) = register_operator(
3932                domain_id,
3933                operator_account,
3934                operator_free_balance,
3935                operator_stake,
3936                10 * AI3,
3937                pair.public(),
3938                Default::default(),
3939                BTreeMap::from_iter(nominators),
3940            );
3941
3942            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3943            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3944            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3945
3946            // Zero deposits should be rejected
3947            assert_err!(
3948                do_nominate_operator::<Test>(operator_id, nominator_account, 0),
3949                StakingError::ZeroDeposit
3950            );
3951
3952            // Zero withdraws should be rejected
3953            assert_err!(
3954                do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
3955                StakingError::ZeroWithdraw
3956            );
3957
3958            // Withdraw all
3959            do_withdraw_stake::<Test>(
3960                operator_id,
3961                nominator_account,
3962                // Assume shares are similar to the stake amount
3963                STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
3964            )
3965            .unwrap();
3966            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3967        });
3968    }
3969
3970    #[test]
3971    fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
3972        let domain_id = DomainId::new(0);
3973        let operator_account = 1;
3974        let operator_free_balance = 250 * AI3;
3975        let operator_stake = 200 * AI3;
3976        let pair = OperatorPair::from_seed(&[0; 32]);
3977        let nominator_account = 2;
3978        let nominator_free_balance = 150 * AI3;
3979        let nominator_stake = 100 * AI3;
3980
3981        let nominators = vec![
3982            (operator_account, (operator_free_balance, operator_stake)),
3983            (nominator_account, (nominator_free_balance, nominator_stake)),
3984        ];
3985
3986        let total_deposit = 300 * AI3;
3987        let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3988
3989        let mut ext = new_test_ext();
3990        ext.execute_with(|| {
3991            let (operator_id, _) = register_operator(
3992                domain_id,
3993                operator_account,
3994                operator_free_balance,
3995                operator_stake,
3996                10 * AI3,
3997                pair.public(),
3998                Default::default(),
3999                BTreeMap::from_iter(nominators),
4000            );
4001
4002            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4003            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4004            assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4005
4006            do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4007            // Assume shares will be approximately the same as the stake amount.
4008            do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4009
4010            // Completed current epoch
4011            let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4012            // Remove the epoch share price intentionally
4013            OperatorEpochSharePrice::<Test>::remove(
4014                operator_id,
4015                DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4016            );
4017
4018            // Both deposit and withdraw should fail due to the share price is missing unexpectedly
4019            assert_err!(
4020                do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4021                StakingError::MissingOperatorEpochSharePrice
4022            );
4023            assert_err!(
4024                do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4025                StakingError::MissingOperatorEpochSharePrice
4026            );
4027        });
4028    }
4029
4030    #[test]
4031    fn test_share_price_deposit() {
4032        let total_shares = 45 * AI3;
4033        let total_stake = 45 * AI3 + 37;
4034        let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4035
4036        // Each item in this list represents an individual deposit requested by a nominator
4037        let to_deposit_stakes = [
4038            5,
4039            7,
4040            9,
4041            11,
4042            17,
4043            23,
4044            934,
4045            24931,
4046            349083467,
4047            2 * AI3 + 32,
4048            52 * AI3 - 4729034,
4049            2732 * AI3 - 1720,
4050            1117 * AI3 + 1839832,
4051            31232 * AI3 - 987654321,
4052        ];
4053
4054        let mut deposited_share = 0;
4055        let mut deposited_stake = 0;
4056        for to_deposit_stake in to_deposit_stakes {
4057            let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4058
4059            // `deposited_stake` is sum of the stake deposited so far.
4060            deposited_stake += to_deposit_stake;
4061            // `deposited_share` is sum of the share that converted from `deposited_stake` so far,
4062            // this is also the share the nominator entitled to withdraw.
4063            deposited_share += to_deposit_share;
4064
4065            // Assuming an epoch transition happened
4066            //
4067            // `total_deposited_share` is the share converted from `operator.deposits_in_epoch`
4068            // and will be added to the `operator.current_total_shares`.
4069            let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4070
4071            // `total_deposited_share` must larger or equal to `deposited_share`, meaning the
4072            // arithmetic dust generated during stake-to-share convertion are leave to the pool
4073            // and can't withdraw/unlock, otherwise, `ShareOverflow` error will happen on `current_total_shares`
4074            // during withdraw/unlock.
4075            assert!(total_deposited_share >= deposited_share);
4076
4077            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4078            // lost during stake-to-share convertion.
4079            assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4080        }
4081    }
4082
4083    #[test]
4084    fn test_share_price_withdraw() {
4085        let total_shares = 123 * AI3;
4086        let total_stake = 123 * AI3 + 13;
4087        let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4088
4089        // Each item in this list represents an individual withdrawal requested by a nominator
4090        let to_withdraw_shares = [
4091            1,
4092            3,
4093            7,
4094            13,
4095            17,
4096            123,
4097            43553,
4098            546393039,
4099            15 * AI3 + 1342,
4100            2 * AI3 - 423,
4101            31 * AI3 - 1321,
4102            42 * AI3 + 4564234,
4103            7 * AI3 - 987654321,
4104            3 * AI3 + 987654321123879,
4105        ];
4106
4107        let mut withdrawn_share = 0;
4108        let mut withdrawn_stake = 0;
4109        for to_withdraw_share in to_withdraw_shares {
4110            let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4111
4112            // `withdrawn_share` is sum of the share withdrawn so far.
4113            withdrawn_share += to_withdraw_share;
4114            // `withdrawn_stake` is sum of the stake that converted from `withdrawn_share` so far,
4115            // this is also the stake the nominator entitled to release/mint during unlock.
4116            withdrawn_stake += to_withdraw_stake;
4117
4118            // Assuming an epoch transition happened
4119            //
4120            // `total_withdrawn_stake` is the stake converted from `operator.withdrawals_in_epoch`
4121            // and will be removed to the `operator.current_total_stake`.
4122            let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4123
4124            // `total_withdrawn_stake` must larger or equal to `withdrawn_stake`, meaning the
4125            // arithmetic dust generated during share-to-stake convertion are leave to the pool,
4126            // otherwise, the nominator will be able to mint reward out of thin air during unlock.
4127            assert!(total_withdrawn_stake >= withdrawn_stake);
4128
4129            // `total_stake` must remains large than `total_shares`, otherwise, it means the reward are
4130            // lost during share-to-stake convertion.
4131            assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4132        }
4133    }
4134
4135    #[test]
4136    fn test_share_price_unlock() {
4137        let mut total_shares = 20 * AI3;
4138        let mut total_stake = 20 * AI3 + 12;
4139
4140        // Each item in this list represents a nominator unlock after the operator de-registered.
4141        //
4142        // The following is simulating how `do_unlock_nominator` work, `shares-to-stake` must return a
4143        // rouding down result, otherwise, `BalanceOverflow` error will happen on `current_total_stake`
4144        // during `do_unlock_nominator`.
4145        for to_unlock_share in [
4146            AI3 + 123,
4147            2 * AI3 - 456,
4148            3 * AI3 - 789,
4149            4 * AI3 - 123 + 456,
4150            7 * AI3 + 789 - 987654321,
4151            3 * AI3 + 987654321,
4152        ] {
4153            let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4154
4155            let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4156
4157            total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4158            total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4159        }
4160        assert_eq!(total_shares, 0);
4161    }
4162}