pallet_domains/
staking.rs

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