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