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