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 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
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 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 None => {
81 to_treasury += reward;
82 return Ok(())
83 }
84 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 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 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 operator.deposits_in_epoch = operator
113 .deposits_in_epoch
114 .checked_add(&operator_tax_deposit.staking)
115 .ok_or(TransitionError::BalanceOverflow)?;
116
117 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 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 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
223pub(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 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 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 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 OperatorEpochSharePrice::<T>::insert(
282 operator_id,
283 DomainEpoch::from((domain_id, previous_epoch)),
284 share_price,
285 );
286
287 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 DepositConsequence::Success => {
319 T::Currency::mint_into(&T::TreasuryAccount::get(), total_funds)
320 .map_err(|_| TransitionError::MintBalance)?;
321 AccumulatedTreasuryFunds::<T>::kill();
322 }
323 _ => AccumulatedTreasuryFunds::<T>::set(total_funds),
325 }
326 Ok(())
327}
328
329pub(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 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 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 match do_convert_previous_epoch_deposits::<T>(
375 operator_id,
376 &mut deposit,
377 current_domain_epoch_index,
378 ) {
379 Ok(()) | Err(TransitionError::MissingOperatorEpochSharePrice) => {}
381 Err(err) => return Err(err),
382 }
383
384 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 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 let nominator_shares = deposit
414 .known
415 .shares
416 .checked_add(&shares_withdrew_in_current_epoch)
417 .ok_or(TransitionError::ShareOverflow)?;
418
419 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 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 T::Currency::release(
445 &staked_hold_id,
446 &nominator_id,
447 pending_deposit,
448 Precision::BestEffort,
449 )
450 .map_err(|_| TransitionError::RemoveLock)?;
451
452 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 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 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 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 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 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 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
626 do_deregister_operator::<Test>(operator_account, operator_id).unwrap();
627
628 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 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
644
645 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 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 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 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}