pallet_domains/
staking_epoch.rs

1//! Staking epoch transition for domain
2use crate::bundle_storage_fund::deposit_reserve_for_storage_fund;
3use crate::pallet::{
4    AccumulatedTreasuryFunds, Deposits, DomainStakingSummary, LastEpochStakingDistribution,
5    NominatorCount, OperatorIdOwner, Operators, PendingSlashes, PendingStakingOperationCount,
6    Withdrawals,
7};
8use crate::staking::{
9    DomainEpoch, Error as TransitionError, OperatorStatus, SharePrice, WithdrawalInShares,
10    do_cleanup_operator, do_convert_previous_epoch_deposits, do_convert_previous_epoch_withdrawal,
11};
12use crate::{
13    BalanceOf, Config, DepositOnHold, DomainChainRewards, ElectionVerificationParams, Event,
14    HoldIdentifier, OperatorEpochSharePrice, Pallet, bundle_storage_fund,
15};
16use frame_support::PalletError;
17use frame_support::traits::fungible::{Inspect, Mutate, MutateHold};
18use frame_support::traits::tokens::{
19    DepositConsequence, Fortitude, Precision, Provenance, Restriction,
20};
21use parity_scale_codec::{Decode, Encode};
22use scale_info::TypeInfo;
23use sp_core::Get;
24use sp_domains::{DomainId, EpochIndex, OperatorId, OperatorRewardSource};
25use sp_runtime::traits::{CheckedAdd, CheckedSub, One, Zero};
26use sp_runtime::{Perquintill, Saturating};
27use sp_std::collections::btree_map::BTreeMap;
28use sp_std::collections::btree_set::BTreeSet;
29
30#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
31pub enum Error {
32    FinalizeDomainEpochStaking(TransitionError),
33    OperatorRewardStaking(TransitionError),
34}
35
36pub(crate) struct EpochTransitionResult {
37    pub rewarded_operator_count: u32,
38    pub finalized_operator_count: u32,
39    pub completed_epoch_index: EpochIndex,
40}
41
42/// Finalizes the domain's current epoch and begins the next epoch.
43/// Returns true of the epoch indeed was finished and the number of operator processed.
44pub(crate) fn do_finalize_domain_current_epoch<T: Config>(
45    domain_id: DomainId,
46) -> Result<EpochTransitionResult, Error> {
47    // Reset pending staking operation count to 0
48    PendingStakingOperationCount::<T>::set(domain_id, 0);
49
50    // re stake operator's tax from the rewards
51    let rewarded_operator_count = operator_take_reward_tax_and_stake::<T>(domain_id)?;
52
53    // finalize any withdrawals and then deposits
54    let (completed_epoch_index, finalized_operator_count) =
55        do_finalize_domain_epoch_staking::<T>(domain_id)?;
56
57    Ok(EpochTransitionResult {
58        rewarded_operator_count,
59        finalized_operator_count,
60        completed_epoch_index,
61    })
62}
63
64/// Operator takes `NominationTax` of the current epoch rewards and stake them.
65pub(crate) fn operator_take_reward_tax_and_stake<T: Config>(
66    domain_id: DomainId,
67) -> Result<u32, Error> {
68    let mut rewarded_operator_count = 0;
69    DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_domain_stake_summary| {
70        let stake_summary = maybe_domain_stake_summary
71            .as_mut()
72            .ok_or(TransitionError::DomainNotInitialized)?;
73
74        let mut to_treasury = BalanceOf::<T>::zero();
75        let mut maybe_reward_per_operator = None;
76
77        let domain_rewards = DomainChainRewards::<T>::take(domain_id);
78        let active_operator_count = stake_summary.current_epoch_rewards.len() as u64;
79        match (active_operator_count > 0, !domain_rewards.is_zero()) {
80            // active operators exist and rewards are non-zero
81            (true, true) => {
82                let reward_per_operator = Perquintill::from_rational(1, active_operator_count).mul_floor(domain_rewards);
83                let total_allocated_rewards = reward_per_operator.saturating_mul(BalanceOf::<T>::from(active_operator_count));
84                maybe_reward_per_operator = Some(reward_per_operator);
85                to_treasury = domain_rewards.saturating_sub(total_allocated_rewards);
86            }
87
88            // no active operators but non-zero rewards
89            (false, true) => {
90                to_treasury = domain_rewards
91            }
92
93            // other cases are irrelevant here
94            _ => {}
95        }
96
97
98        while let Some((operator_id, mut reward)) = stake_summary.current_epoch_rewards.pop_first() {
99            reward = reward.saturating_add(maybe_reward_per_operator.unwrap_or_default());
100            Operators::<T>::try_mutate(operator_id, |maybe_operator| {
101                let operator = match maybe_operator.as_mut() {
102                    // It is possible that operator may have de registered and unlocked by the time they
103                    // got rewards, in this case, move the reward to the treasury
104                    None => {
105                        to_treasury += reward;
106                        return Ok(());
107                    }
108                    // Move the reward of slashed and pening slash operator to the treasury
109                    Some(operator) if matches!(*operator.status::<T>(operator_id), OperatorStatus::Slashed | OperatorStatus::PendingSlash) => {
110                        to_treasury += reward;
111                        return Ok(());
112                    }
113                    Some(operator) => operator,
114                };
115
116                if let Some(reward_per_operator) = maybe_reward_per_operator {
117                    Pallet::<T>::deposit_event(Event::OperatorRewarded {
118                        source: OperatorRewardSource::XDMProtocolFees,
119                        operator_id,
120                        reward: reward_per_operator,
121                    });
122                }
123
124                // calculate operator tax, mint the balance, and stake them
125                let operator_tax_amount = operator.nomination_tax.mul_floor(reward);
126                if !operator_tax_amount.is_zero() {
127                    let nominator_id = OperatorIdOwner::<T>::get(operator_id)
128                        .ok_or(TransitionError::MissingOperatorOwner)?;
129                    T::Currency::mint_into(&nominator_id, operator_tax_amount)
130                        .map_err(|_| TransitionError::MintBalance)?;
131
132                    // Reserve for the bundle storage fund
133                    let operator_tax_deposit =
134                        deposit_reserve_for_storage_fund::<T>(operator_id, &nominator_id, operator_tax_amount)
135                            .map_err(TransitionError::BundleStorageFund)?;
136
137                    crate::staking::hold_deposit::<T>(
138                        &nominator_id,
139                        operator_id,
140                        operator_tax_deposit.staking,
141                    )?;
142
143                    // increment total deposit for operator pool within this epoch
144                    operator.deposits_in_epoch = operator
145                        .deposits_in_epoch
146                        .checked_add(&operator_tax_deposit.staking)
147                        .ok_or(TransitionError::BalanceOverflow)?;
148
149                    // Increase total storage fee deposit as there is new deposit to the storage fund
150                    operator.total_storage_fee_deposit = operator
151                        .total_storage_fee_deposit
152                        .checked_add(&operator_tax_deposit.storage_fee_deposit)
153                        .ok_or(TransitionError::BalanceOverflow)?;
154
155                    let current_domain_epoch = (domain_id, stake_summary.current_epoch_index).into();
156                    crate::staking::do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
157                        operator_id,
158                        nominator_id,
159                        current_domain_epoch,
160                        operator_tax_deposit,
161                    )?;
162
163                    Pallet::<T>::deposit_event(Event::OperatorTaxCollected {
164                        operator_id,
165                        tax: operator_tax_amount,
166                    });
167                }
168
169                // Add the remaining rewards to the operator's `current_total_stake` which increases the
170                // share price of the staking pool and as a way to distribute the reward to the nominator
171                let rewards = reward
172                    .checked_sub(&operator_tax_amount)
173                    .ok_or(TransitionError::BalanceUnderflow)?;
174
175                operator.current_total_stake = operator
176                    .current_total_stake
177                    .checked_add(&rewards)
178                    .ok_or(TransitionError::BalanceOverflow)?;
179
180                rewarded_operator_count += 1;
181
182                Ok(())
183            })?;
184        }
185
186        mint_into_treasury::<T>(to_treasury)?;
187
188        Ok(())
189    })
190        .map_err(Error::OperatorRewardStaking)?;
191
192    Ok(rewarded_operator_count)
193}
194
195pub(crate) fn do_finalize_domain_epoch_staking<T: Config>(
196    domain_id: DomainId,
197) -> Result<(EpochIndex, u32), Error> {
198    let mut finalized_operator_count = 0;
199    DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_stake_summary| {
200        let stake_summary = maybe_stake_summary
201            .as_mut()
202            .ok_or(TransitionError::DomainNotInitialized)?;
203
204        let previous_epoch = stake_summary.current_epoch_index;
205        let next_epoch = previous_epoch
206            .checked_add(One::one())
207            .ok_or(TransitionError::EpochOverflow)?;
208
209        let mut total_domain_stake = BalanceOf::<T>::zero();
210        let mut current_operators = BTreeMap::new();
211        let mut next_operators = BTreeSet::new();
212        for next_operator_id in &stake_summary.next_operators {
213            // If an operator is pending to slash then similar to the slashed operator it should not be added
214            // into the `next_operators/current_operators` and we should not `do_finalize_operator_epoch_staking`
215            // for it.
216            if Pallet::<T>::is_operator_pending_to_slash(domain_id, *next_operator_id) {
217                continue;
218            }
219
220            let (operator_stake, stake_changed) = do_finalize_operator_epoch_staking::<T>(
221                domain_id,
222                *next_operator_id,
223                previous_epoch,
224            )?;
225
226            total_domain_stake = total_domain_stake
227                .checked_add(&operator_stake)
228                .ok_or(TransitionError::BalanceOverflow)?;
229            current_operators.insert(*next_operator_id, operator_stake);
230            next_operators.insert(*next_operator_id);
231
232            if stake_changed {
233                finalized_operator_count += 1;
234            }
235        }
236
237        let election_verification_params = ElectionVerificationParams {
238            operators: stake_summary.current_operators.clone(),
239            total_domain_stake: stake_summary.current_total_stake,
240        };
241
242        LastEpochStakingDistribution::<T>::insert(domain_id, election_verification_params);
243
244        let previous_epoch = stake_summary.current_epoch_index;
245        stake_summary.current_epoch_index = next_epoch;
246        stake_summary.current_total_stake = total_domain_stake;
247        stake_summary.current_operators = current_operators;
248        stake_summary.next_operators = next_operators;
249
250        Ok((previous_epoch, finalized_operator_count))
251    })
252    .map_err(Error::FinalizeDomainEpochStaking)
253}
254
255/// Finalize the epoch for the operator
256///
257/// Return the new total stake of the operator and a bool indicate if its total stake
258/// is changed due to deposit/withdraw/reward happened in the previous epoch
259pub(crate) fn do_finalize_operator_epoch_staking<T: Config>(
260    domain_id: DomainId,
261    operator_id: OperatorId,
262    previous_epoch: EpochIndex,
263) -> Result<(BalanceOf<T>, bool), TransitionError> {
264    let mut operator = match Operators::<T>::get(operator_id) {
265        Some(op) => op,
266        None => return Err(TransitionError::UnknownOperator),
267    };
268
269    if *operator.status::<T>(operator_id) != OperatorStatus::Registered {
270        return Err(TransitionError::OperatorNotRegistered);
271    }
272
273    // if there are no deposits, withdrawls, and epoch rewards for this operator
274    // then short-circuit and return early.
275    if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
276        return Ok((operator.current_total_stake, false));
277    }
278
279    let mut total_stake = operator.current_total_stake;
280    let mut total_shares = operator.current_total_shares;
281    let share_price = SharePrice::new::<T>(total_shares, total_stake);
282
283    // calculate and subtract total withdrew shares from previous epoch
284    if !operator.withdrawals_in_epoch.is_zero() {
285        let withdraw_stake = share_price.shares_to_stake::<T>(operator.withdrawals_in_epoch);
286        total_stake = total_stake
287            .checked_sub(&withdraw_stake)
288            .ok_or(TransitionError::BalanceUnderflow)?;
289        total_shares = total_shares
290            .checked_sub(&operator.withdrawals_in_epoch)
291            .ok_or(TransitionError::ShareUnderflow)?;
292
293        operator.withdrawals_in_epoch = Zero::zero();
294    };
295
296    // calculate and add total deposits from the previous epoch
297    if !operator.deposits_in_epoch.is_zero() {
298        let deposited_shares = share_price.stake_to_shares::<T>(operator.deposits_in_epoch);
299        total_stake = total_stake
300            .checked_add(&operator.deposits_in_epoch)
301            .ok_or(TransitionError::BalanceOverflow)?;
302        total_shares = total_shares
303            .checked_add(&deposited_shares)
304            .ok_or(TransitionError::ShareOverflow)?;
305
306        operator.deposits_in_epoch = Zero::zero();
307    };
308
309    // update operator pool epoch share price
310    // TODO: once we have reference counting, we do not need to
311    //  store this for every epoch for every operator but instead
312    //  store only those share prices of operators which has either a deposit or withdraw
313    OperatorEpochSharePrice::<T>::insert(
314        operator_id,
315        DomainEpoch::from((domain_id, previous_epoch)),
316        share_price,
317    );
318
319    // update operator state
320    operator.current_total_shares = total_shares;
321    operator.current_total_stake = total_stake;
322    Operators::<T>::set(operator_id, Some(operator));
323
324    Ok((total_stake, true))
325}
326
327pub(crate) fn mint_funds<T: Config>(
328    account_id: &T::AccountId,
329    amount_to_mint: BalanceOf<T>,
330) -> Result<(), TransitionError> {
331    if !amount_to_mint.is_zero() {
332        T::Currency::mint_into(account_id, amount_to_mint)
333            .map_err(|_| TransitionError::MintBalance)?;
334    }
335
336    Ok(())
337}
338
339pub(crate) fn mint_into_treasury<T: Config>(amount: BalanceOf<T>) -> Result<(), TransitionError> {
340    if amount.is_zero() {
341        return Ok(());
342    }
343
344    let total_funds = AccumulatedTreasuryFunds::<T>::get()
345        .checked_add(&amount)
346        .ok_or(TransitionError::BalanceOverflow)?;
347
348    match T::Currency::can_deposit(&T::TreasuryAccount::get(), total_funds, Provenance::Minted) {
349        // Deposit is possible, so we mint the funds into treasury.
350        DepositConsequence::Success => {
351            T::Currency::mint_into(&T::TreasuryAccount::get(), total_funds)
352                .map_err(|_| TransitionError::MintBalance)?;
353            AccumulatedTreasuryFunds::<T>::kill();
354        }
355        // Deposit cannot be done to treasury, so hold the funds until we can.
356        _ => AccumulatedTreasuryFunds::<T>::set(total_funds),
357    }
358    Ok(())
359}
360
361/// Slashes any pending slashed operators.
362/// At max slashes the `max_nominator_count` under given operator
363pub(crate) fn do_slash_operator<T: Config>(
364    domain_id: DomainId,
365    max_nominator_count: u32,
366) -> Result<u32, TransitionError> {
367    let mut slashed_nominator_count = 0u32;
368    let (operator_id, slashed_operators) = match PendingSlashes::<T>::get(domain_id) {
369        None => return Ok(0),
370        Some(mut slashed_operators) => match slashed_operators.pop_first() {
371            None => {
372                PendingSlashes::<T>::remove(domain_id);
373                return Ok(0);
374            }
375            Some(operator_id) => (operator_id, slashed_operators),
376        },
377    };
378
379    let current_domain_epoch_index = DomainStakingSummary::<T>::get(domain_id)
380        .ok_or(TransitionError::DomainNotInitialized)?
381        .current_epoch_index;
382
383    Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
384        // take the operator so this operator info is removed once we slash the operator.
385        let mut operator = maybe_operator
386            .take()
387            .ok_or(TransitionError::UnknownOperator)?;
388
389        let operator_owner =
390            OperatorIdOwner::<T>::get(operator_id).ok_or(TransitionError::UnknownOperator)?;
391
392        let staked_hold_id = T::HoldIdentifier::staking_staked();
393
394        let mut total_stake = operator.current_total_stake;
395        let mut total_shares = operator.current_total_shares;
396        let share_price = SharePrice::new::<T>(total_shares, total_stake);
397
398        let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
399
400        // transfer all the staked funds to the treasury account
401        // any gains will be minted to treasury account
402        for (nominator_id, mut deposit) in Deposits::<T>::drain_prefix(operator_id) {
403            let locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
404
405            // convert any previous epoch deposits
406            match do_convert_previous_epoch_deposits::<T>(
407                operator_id,
408                &mut deposit,
409                current_domain_epoch_index,
410            ) {
411                // Share price may be missing if there is deposit happen in the same epoch as slash
412                Ok(()) | Err(TransitionError::MissingOperatorEpochSharePrice) => {}
413                Err(err) => return Err(err),
414            }
415
416            // there maybe some withdrawals that are initiated in this epoch where operator was slash
417            // then collect and include them to find the final stake amount
418            let (
419                amount_ready_to_withdraw,
420                withdraw_storage_fee_on_hold,
421                shares_withdrew_in_current_epoch,
422            ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
423                .map(|mut withdrawal| {
424                    match do_convert_previous_epoch_withdrawal::<T>(
425                        operator_id,
426                        &mut withdrawal,
427                        current_domain_epoch_index,
428                    ) {
429                        // Share price may be missing if there is withdrawal happen in the same epoch as slash
430                        Ok(()) | Err(TransitionError::MissingOperatorEpochSharePrice) => {}
431                        Err(err) => return Err(err),
432                    }
433                    Ok((
434                        withdrawal.total_withdrawal_amount,
435                        withdrawal.total_storage_fee_withdrawal,
436                        withdrawal
437                            .withdrawal_in_shares
438                            .map(|WithdrawalInShares { shares, .. }| shares)
439                            .unwrap_or_default(),
440                    ))
441                })
442                .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
443
444            // include all the known shares and shares that were withdrawn in the current epoch
445            let nominator_shares = deposit
446                .known
447                .shares
448                .checked_add(&shares_withdrew_in_current_epoch)
449                .ok_or(TransitionError::ShareOverflow)?;
450
451            // current staked amount
452            let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
453
454            let pending_deposit = deposit
455                .pending
456                .map(|pending_deposit| pending_deposit.amount)
457                .unwrap_or_default();
458
459            // do not slash the deposit that is not staked yet
460            let amount_to_slash_in_holding = locked_amount
461                .checked_sub(&pending_deposit)
462                .ok_or(TransitionError::BalanceUnderflow)?;
463
464            T::Currency::transfer_on_hold(
465                &staked_hold_id,
466                &nominator_id,
467                &T::TreasuryAccount::get(),
468                amount_to_slash_in_holding,
469                Precision::Exact,
470                Restriction::Free,
471                Fortitude::Force,
472            )
473            .map_err(|_| TransitionError::RemoveLock)?;
474
475            // release rest of the deposited un staked amount back to nominator
476            T::Currency::release(
477                &staked_hold_id,
478                &nominator_id,
479                pending_deposit,
480                Precision::BestEffort,
481            )
482            .map_err(|_| TransitionError::RemoveLock)?;
483
484            // these are nominator rewards that will be minted to treasury
485            // include amount ready to be withdrawn to calculate the final reward
486            let nominator_reward = nominator_staked_amount
487                .checked_add(&amount_ready_to_withdraw)
488                .ok_or(TransitionError::BalanceOverflow)?
489                .checked_sub(&amount_to_slash_in_holding)
490                .ok_or(TransitionError::BalanceUnderflow)?;
491
492            mint_into_treasury::<T>(nominator_reward)?;
493
494            total_stake = total_stake.saturating_sub(nominator_staked_amount);
495            total_shares = total_shares.saturating_sub(nominator_shares);
496
497            // Transfer the deposited non-staked storage fee back to nominator
498            if let Some(pending_deposit) = deposit.pending {
499                let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
500                    operator_id,
501                    total_storage_fee_deposit,
502                );
503
504                bundle_storage_fund::withdraw_to::<T>(
505                    operator_id,
506                    &nominator_id,
507                    storage_fund_redeem_price.redeem(pending_deposit.storage_fee_deposit),
508                )
509                .map_err(TransitionError::BundleStorageFund)?;
510
511                total_storage_fee_deposit =
512                    total_storage_fee_deposit.saturating_sub(pending_deposit.storage_fee_deposit);
513            }
514
515            // Transfer all the storage fee on withdraw to the treasury
516            T::Currency::transfer_on_hold(
517                &T::HoldIdentifier::storage_fund_withdrawal(),
518                &nominator_id,
519                &T::TreasuryAccount::get(),
520                withdraw_storage_fee_on_hold,
521                Precision::Exact,
522                Restriction::Free,
523                Fortitude::Force,
524            )
525            .map_err(|_| TransitionError::RemoveLock)?;
526
527            // update nominator count.
528            let nominator_count = NominatorCount::<T>::get(operator_id);
529            if operator_owner != nominator_id && nominator_count > 0 {
530                NominatorCount::<T>::set(operator_id, nominator_count - 1);
531            }
532
533            slashed_nominator_count += 1;
534            if slashed_nominator_count >= max_nominator_count {
535                break;
536            }
537        }
538
539        let nominator_count = NominatorCount::<T>::get(operator_id);
540        let cleanup_operator =
541            nominator_count == 0 && !Deposits::<T>::contains_key(operator_id, operator_owner);
542
543        if cleanup_operator {
544            do_cleanup_operator::<T>(operator_id, total_stake)?;
545            if slashed_operators.is_empty() {
546                PendingSlashes::<T>::remove(domain_id);
547            } else {
548                PendingSlashes::<T>::set(domain_id, Some(slashed_operators));
549            }
550        } else {
551            // set update total shares, total stake and total storage fee deposit for operator
552            operator.current_total_shares = total_shares;
553            operator.current_total_stake = total_stake;
554            operator.total_storage_fee_deposit = total_storage_fee_deposit;
555            *maybe_operator = Some(operator);
556        }
557
558        Ok(slashed_nominator_count)
559    })
560}
561
562#[cfg(test)]
563mod tests {
564    use crate::bundle_storage_fund::STORAGE_FEE_RESERVE;
565    use crate::pallet::{
566        Deposits, DomainStakingSummary, HeadDomainNumber, LastEpochStakingDistribution,
567        NominatorCount, OperatorIdOwner, Operators, Withdrawals,
568    };
569    use crate::staking::tests::{Share, register_operator};
570    use crate::staking::{
571        Error as TransitionError, WithdrawStake, do_deregister_operator, do_nominate_operator,
572        do_reward_operators, do_unlock_nominator, do_withdraw_stake,
573    };
574    use crate::staking_epoch::{
575        do_finalize_domain_current_epoch, operator_take_reward_tax_and_stake,
576    };
577    use crate::tests::{Test, new_test_ext};
578    use crate::{BalanceOf, Config, HoldIdentifier, NominatorId};
579    #[cfg(not(feature = "std"))]
580    use alloc::vec;
581    use frame_support::traits::fungible::InspectHold;
582    use frame_support::{assert_err, assert_ok};
583    use sp_core::Pair;
584    use sp_domains::{DomainId, OperatorPair, OperatorRewardSource};
585    use sp_runtime::traits::Zero;
586    use sp_runtime::{PerThing, Percent};
587    use std::collections::BTreeMap;
588    use subspace_runtime_primitives::AI3;
589
590    type Balances = pallet_balances::Pallet<Test>;
591
592    fn unlock_nominator(
593        nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
594        pending_deposits: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
595        withdrawals: Vec<(NominatorId<Test>, Share)>,
596        expected_usable_balances: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
597        rewards: BalanceOf<Test>,
598    ) {
599        let domain_id = DomainId::new(0);
600        let operator_account = 1;
601        let pair = OperatorPair::from_seed(&[0; 32]);
602        let minimum_free_balance = 10 * AI3;
603        let mut nominators = BTreeMap::from_iter(
604            nominators
605                .into_iter()
606                .map(|(id, balance)| (id, (balance + minimum_free_balance, balance)))
607                .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
608        );
609
610        for pending_deposit in &pending_deposits {
611            let staked_deposit = nominators
612                .get(&pending_deposit.0)
613                .cloned()
614                .unwrap_or((minimum_free_balance, 0));
615            let total_balance = staked_deposit.0 + pending_deposit.1;
616            nominators.insert(pending_deposit.0, (total_balance, staked_deposit.1));
617        }
618
619        let mut ext = new_test_ext();
620        ext.execute_with(|| {
621            let (operator_free_balance, operator_stake) =
622                nominators.remove(&operator_account).unwrap();
623            let (operator_id, _) = register_operator(
624                domain_id,
625                operator_account,
626                operator_free_balance,
627                operator_stake,
628                10 * AI3,
629                pair.public(),
630                BTreeMap::from_iter(nominators.clone()),
631            );
632
633            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
634
635            // add pending deposits
636            for pending_deposit in &pending_deposits {
637                do_nominate_operator::<Test>(operator_id, pending_deposit.0, pending_deposit.1)
638                    .unwrap();
639            }
640
641            for (nominator_id, shares) in withdrawals {
642                do_withdraw_stake::<Test>(operator_id, nominator_id, WithdrawStake::Share(shares))
643                    .unwrap();
644            }
645
646            if !rewards.is_zero() {
647                do_reward_operators::<Test>(
648                    domain_id,
649                    OperatorRewardSource::Dummy,
650                    vec![operator_id].into_iter(),
651                    rewards,
652                )
653                .unwrap()
654            }
655
656            // de-register operator
657            let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
658            do_deregister_operator::<Test>(operator_account, operator_id).unwrap();
659
660            // After de-register both deposit and withdraw will be rejected
661            assert_err!(
662                do_nominate_operator::<Test>(operator_id, operator_account, AI3),
663                TransitionError::OperatorNotRegistered
664            );
665            assert_err!(
666                do_withdraw_stake::<Test>(
667                    operator_id,
668                    operator_account,
669                    WithdrawStake::Percent(Percent::from_percent(10))
670                ),
671                TransitionError::OperatorNotRegistered
672            );
673
674            // finalize and add to pending operator unlocks
675            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
676
677            // Update `HeadDomainNumber` to ensure unlock success
678            HeadDomainNumber::<Test>::set(
679                domain_id,
680                head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
681            );
682
683            for (nominator_id, _) in nominators {
684                assert_ok!(do_unlock_nominator::<Test>(operator_id, nominator_id));
685            }
686
687            assert_ok!(do_unlock_nominator::<Test>(operator_id, operator_account));
688
689            let hold_id = crate::tests::HoldIdentifierWrapper::staking_staked();
690            for (nominator_id, mut expected_usable_balance) in expected_usable_balances {
691                expected_usable_balance += minimum_free_balance;
692                assert_eq!(Deposits::<Test>::get(operator_id, nominator_id), None);
693                assert_eq!(Withdrawals::<Test>::get(operator_id, nominator_id), None);
694                assert_eq!(
695                    Balances::usable_balance(nominator_id),
696                    expected_usable_balance
697                );
698                assert_eq!(
699                    Balances::balance_on_hold(&hold_id, &nominator_id),
700                    Zero::zero()
701                );
702            }
703
704            assert_eq!(Operators::<Test>::get(operator_id), None);
705            assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
706            assert_eq!(NominatorCount::<Test>::get(operator_id), 0);
707        });
708    }
709
710    #[test]
711    fn unlock_operator_with_no_rewards() {
712        unlock_nominator(
713            vec![(1, 150 * AI3), (2, 50 * AI3), (3, 10 * AI3)],
714            vec![(2, 10 * AI3), (4, 10 * AI3)],
715            vec![(1, 20 * AI3), (2, 10 * AI3)],
716            vec![(1, 150 * AI3), (2, 60 * AI3), (3, 10 * AI3), (4, 10 * AI3)],
717            0,
718        );
719    }
720
721    #[test]
722    fn unlock_operator_with_rewards() {
723        unlock_nominator(
724            vec![(1, 150 * AI3), (2, 50 * AI3), (3, 10 * AI3)],
725            vec![(2, 10 * AI3), (4, 10 * AI3)],
726            vec![(1, 20 * AI3), (2, 10 * AI3)],
727            vec![
728                (1, 164285714327278911577),
729                (2, 64761904775759637192),
730                (3, 10952380955151927438),
731                (4, 10 * AI3),
732            ],
733            20 * AI3,
734        );
735    }
736
737    struct FinalizeDomainParams {
738        total_deposit: BalanceOf<Test>,
739        rewards: BalanceOf<Test>,
740        nominators: Vec<(NominatorId<Test>, <Test as Config>::Share)>,
741        deposits: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
742    }
743
744    fn finalize_domain_epoch(params: FinalizeDomainParams) {
745        let domain_id = DomainId::new(0);
746        let operator_account = 0;
747        let pair = OperatorPair::from_seed(&[0; 32]);
748        let FinalizeDomainParams {
749            total_deposit,
750            rewards,
751            nominators,
752            deposits,
753        } = params;
754
755        let minimum_free_balance = 10 * AI3;
756        let mut nominators = BTreeMap::from_iter(
757            nominators
758                .into_iter()
759                .map(|(id, balance)| (id, (balance + minimum_free_balance, balance)))
760                .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
761        );
762
763        for deposit in &deposits {
764            let values = nominators
765                .remove(&deposit.0)
766                .unwrap_or((minimum_free_balance, 0));
767            nominators.insert(deposit.0, (deposit.1 + values.0, values.1));
768        }
769
770        let mut ext = new_test_ext();
771        ext.execute_with(|| {
772            let (operator_free_balance, operator_stake) =
773                nominators.remove(&operator_account).unwrap();
774            let (operator_id, _) = register_operator(
775                domain_id,
776                operator_account,
777                operator_free_balance,
778                operator_stake,
779                10 * AI3,
780                pair.public(),
781                BTreeMap::from_iter(nominators),
782            );
783
784            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
785
786            let mut total_new_deposit = BalanceOf::<Test>::zero();
787            for deposit in &deposits {
788                do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
789                total_new_deposit += deposit.1;
790            }
791
792            if !rewards.is_zero() {
793                do_reward_operators::<Test>(
794                    domain_id,
795                    OperatorRewardSource::Dummy,
796                    vec![operator_id].into_iter(),
797                    rewards,
798                )
799                .unwrap();
800            }
801
802            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
803            for deposit in deposits {
804                Deposits::<Test>::contains_key(operator_id, deposit.0);
805            }
806
807            // should also store the previous epoch details in-block
808            let total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
809            let election_params = LastEpochStakingDistribution::<Test>::get(domain_id).unwrap();
810            assert_eq!(
811                election_params.operators,
812                BTreeMap::from_iter(vec![(operator_id, total_stake)])
813            );
814            assert_eq!(election_params.total_domain_stake, total_stake);
815
816            let total_updated_stake = total_deposit + total_new_deposit + rewards;
817            let operator = Operators::<Test>::get(operator_id).unwrap();
818            assert_eq!(
819                operator.current_total_stake + operator.total_storage_fee_deposit,
820                total_updated_stake
821            );
822
823            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
824            assert_eq!(
825                domain_stake_summary.current_total_stake,
826                total_updated_stake - operator.total_storage_fee_deposit
827            );
828            // epoch should be 3 since we did 3 epoch transitions
829            assert_eq!(domain_stake_summary.current_epoch_index, 3);
830        });
831    }
832
833    #[test]
834    fn finalize_domain_epoch_no_rewards() {
835        finalize_domain_epoch(FinalizeDomainParams {
836            total_deposit: 210 * AI3,
837            rewards: 0,
838            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
839            deposits: vec![(1, 50 * AI3), (3, 10 * AI3)],
840        })
841    }
842
843    #[test]
844    fn finalize_domain_epoch_with_rewards() {
845        finalize_domain_epoch(FinalizeDomainParams {
846            total_deposit: 210 * AI3,
847            rewards: 20 * AI3,
848            nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
849            deposits: vec![(1, 50 * AI3), (3, 10 * AI3)],
850        })
851    }
852
853    #[test]
854    fn operator_tax_and_staking() {
855        let domain_id = DomainId::new(0);
856        let operator_account = 1;
857        let pair = OperatorPair::from_seed(&[0; 32]);
858        let operator_rewards = 10 * AI3;
859        let mut nominators =
860            BTreeMap::from_iter(vec![(1, (110 * AI3, 100 * AI3)), (2, (60 * AI3, 50 * AI3))]);
861
862        let mut ext = new_test_ext();
863        ext.execute_with(|| {
864            let (operator_free_balance, operator_stake) =
865                nominators.remove(&operator_account).unwrap();
866            let (operator_id, _) = register_operator(
867                domain_id,
868                operator_account,
869                operator_free_balance,
870                operator_stake,
871                10 * AI3,
872                pair.public(),
873                BTreeMap::from_iter(nominators),
874            );
875
876            do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
877
878            // 10% tax
879            let nomination_tax = Percent::from_parts(10);
880            let mut operator = Operators::<Test>::get(operator_id).unwrap();
881            let pre_total_stake = operator.current_total_stake;
882            let pre_storage_fund_deposit = operator.total_storage_fee_deposit;
883            operator.nomination_tax = nomination_tax;
884            Operators::<Test>::insert(operator_id, operator);
885            let expected_operator_tax = nomination_tax.mul_ceil(operator_rewards);
886
887            do_reward_operators::<Test>(
888                domain_id,
889                OperatorRewardSource::Dummy,
890                vec![operator_id].into_iter(),
891                operator_rewards,
892            )
893            .unwrap();
894
895            operator_take_reward_tax_and_stake::<Test>(domain_id).unwrap();
896            let operator = Operators::<Test>::get(operator_id).unwrap();
897            let new_storage_fund_deposit =
898                operator.total_storage_fee_deposit - pre_storage_fund_deposit;
899            assert_eq!(
900                operator.current_total_stake - pre_total_stake,
901                (10 * AI3 - expected_operator_tax)
902            );
903
904            let staking_deposit = Deposits::<Test>::get(operator_id, operator_account)
905                .unwrap()
906                .pending
907                .unwrap()
908                .amount;
909            assert_eq!(
910                staking_deposit + new_storage_fund_deposit,
911                expected_operator_tax
912            );
913            assert_eq!(
914                staking_deposit,
915                STORAGE_FEE_RESERVE.left_from_one() * expected_operator_tax
916            );
917            assert_eq!(
918                new_storage_fund_deposit,
919                STORAGE_FEE_RESERVE * expected_operator_tax
920            );
921            let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
922            assert!(domain_stake_summary.current_epoch_rewards.is_empty())
923        });
924    }
925}