1use 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
42pub(crate) fn do_finalize_domain_current_epoch<T: Config>(
45 domain_id: DomainId,
46) -> Result<EpochTransitionResult, Error> {
47 PendingStakingOperationCount::<T>::set(domain_id, 0);
49
50 let rewarded_operator_count = operator_take_reward_tax_and_stake::<T>(domain_id)?;
52
53 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
64pub(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 (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 (false, true) => {
90 to_treasury = domain_rewards
91 }
92
93 _ => {}
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 None => {
105 to_treasury += reward;
106 return Ok(());
107 }
108 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 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 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 operator.deposits_in_epoch = operator
145 .deposits_in_epoch
146 .checked_add(&operator_tax_deposit.staking)
147 .ok_or(TransitionError::BalanceOverflow)?;
148
149 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 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 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
255pub(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 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 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 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 OperatorEpochSharePrice::<T>::insert(
314 operator_id,
315 DomainEpoch::from((domain_id, previous_epoch)),
316 share_price,
317 );
318
319 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 DepositConsequence::Success => {
351 T::Currency::mint_into(&T::TreasuryAccount::get(), total_funds)
352 .map_err(|_| TransitionError::MintBalance)?;
353 AccumulatedTreasuryFunds::<T>::kill();
354 }
355 _ => AccumulatedTreasuryFunds::<T>::set(total_funds),
357 }
358 Ok(())
359}
360
361pub(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 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 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 match do_convert_previous_epoch_deposits::<T>(
407 operator_id,
408 &mut deposit,
409 current_domain_epoch_index,
410 ) {
411 Ok(()) | Err(TransitionError::MissingOperatorEpochSharePrice) => {}
413 Err(err) => return Err(err),
414 }
415
416 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 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 let nominator_shares = deposit
446 .known
447 .shares
448 .checked_add(&shares_withdrew_in_current_epoch)
449 .ok_or(TransitionError::ShareOverflow)?;
450
451 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 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 T::Currency::release(
477 &staked_hold_id,
478 &nominator_id,
479 pending_deposit,
480 Precision::BestEffort,
481 )
482 .map_err(|_| TransitionError::RemoveLock)?;
483
484 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 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 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 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 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 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 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
658 do_deregister_operator::<Test>(operator_account, operator_id).unwrap();
659
660 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 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
676
677 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 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 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 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}