1#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6use crate::block_tree::invalid_bundle_authors_for_receipt;
7use crate::bundle_storage_fund::{self, deposit_reserve_for_storage_fund};
8use crate::pallet::{
9 Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber, NextOperatorId,
10 OperatorIdOwner, Operators, PendingSlashes, PendingStakingOperationCount, Withdrawals,
11};
12use crate::staking_epoch::{mint_funds, mint_into_treasury};
13use crate::{
14 BalanceOf, Config, DepositOnHold, DeregisteredOperators, DomainBlockNumberFor,
15 DomainHashingFor, Event, ExecutionReceiptOf, HoldIdentifier, InvalidBundleAuthors, NominatorId,
16 OperatorEpochSharePrice, OperatorHighestSlot, Pallet, ReceiptHashFor, SlashedReason,
17};
18use frame_support::traits::fungible::{Inspect, MutateHold};
19use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
20use frame_support::{PalletError, StorageDoubleMap, ensure};
21use frame_system::pallet_prelude::BlockNumberFor;
22use parity_scale_codec::{Decode, Encode};
23use scale_info::TypeInfo;
24use sp_core::{Get, sr25519};
25use sp_domains::{DomainId, EpochIndex, OperatorId, OperatorPublicKey, OperatorRewardSource};
26use sp_runtime::traits::{CheckedAdd, CheckedSub, Zero};
27use sp_runtime::{PerThing, Percent, Perquintill, Saturating};
28use sp_std::collections::btree_map::BTreeMap;
29use sp_std::collections::btree_set::BTreeSet;
30use sp_std::collections::vec_deque::VecDeque;
31use sp_std::vec::IntoIter;
32
33#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
35pub(crate) struct Deposit<Share: Copy, Balance: Copy> {
36 pub(crate) known: KnownDeposit<Share, Balance>,
37 pub(crate) pending: Option<PendingDeposit<Balance>>,
38}
39
40#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
43pub struct SharePrice(pub Perquintill);
44
45impl SharePrice {
46 pub(crate) fn new<T: Config>(
49 total_shares: T::Share,
50 total_stake: BalanceOf<T>,
51 ) -> Result<Self, Error> {
52 if total_shares > total_stake.into() {
53 Err(Error::ShareOverflow)
55 } else if total_stake.is_zero() || total_shares.is_zero() {
56 Err(Error::ZeroSharePrice)
58 } else {
59 Ok(SharePrice(Perquintill::from_rational(
60 total_shares.into(),
61 total_stake,
62 )))
63 }
64 }
65
66 pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
69 self.0.mul_floor(stake).into()
70 }
71
72 pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
75 self.0
80 .plus_epsilon()
83 .saturating_reciprocal_mul_floor(shares.into())
84 }
85
86 pub(crate) fn one() -> Self {
88 Self(Perquintill::one())
89 }
90}
91
92#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
94pub struct DomainEpoch(DomainId, EpochIndex);
95
96impl DomainEpoch {
97 pub(crate) fn deconstruct(self) -> (DomainId, EpochIndex) {
98 (self.0, self.1)
99 }
100}
101
102impl From<(DomainId, EpochIndex)> for DomainEpoch {
103 fn from((domain_id, epoch_idx): (DomainId, EpochIndex)) -> Self {
104 Self(domain_id, epoch_idx)
105 }
106}
107
108pub struct NewDeposit<Balance> {
109 pub(crate) staking: Balance,
110 pub(crate) storage_fee_deposit: Balance,
111}
112
113#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq, Default)]
115pub(crate) struct KnownDeposit<Share: Copy, Balance: Copy> {
116 pub(crate) shares: Share,
117 pub(crate) storage_fee_deposit: Balance,
118}
119
120#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
122pub(crate) struct PendingDeposit<Balance: Copy> {
123 pub(crate) effective_domain_epoch: DomainEpoch,
124 pub(crate) amount: Balance,
125 pub(crate) storage_fee_deposit: Balance,
126}
127
128impl<Balance: Copy + CheckedAdd> PendingDeposit<Balance> {
129 fn total(&self) -> Result<Balance, Error> {
130 self.amount
131 .checked_add(&self.storage_fee_deposit)
132 .ok_or(Error::BalanceOverflow)
133 }
134}
135
136#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
138pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
139 pub(crate) total_withdrawal_amount: Balance,
142 pub(crate) total_storage_fee_withdrawal: Balance,
144 pub(crate) withdrawals: VecDeque<WithdrawalInBalance<DomainBlockNumber, Balance>>,
146 pub(crate) withdrawal_in_shares: Option<WithdrawalInShares<DomainBlockNumber, Share, Balance>>,
149}
150
151#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
152pub(crate) struct WithdrawalInBalance<DomainBlockNumber, Balance> {
153 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
154 pub(crate) amount_to_unlock: Balance,
155 pub(crate) storage_fee_refund: Balance,
156}
157
158#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
159pub(crate) struct WithdrawalInShares<DomainBlockNumber, Share, Balance> {
160 pub(crate) domain_epoch: DomainEpoch,
161 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
162 pub(crate) shares: Share,
163 pub(crate) storage_fee_refund: Balance,
164}
165
166#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
167pub struct OperatorDeregisteredInfo<DomainBlockNumber> {
168 pub domain_epoch: DomainEpoch,
169 pub unlock_at_confirmed_domain_block_number: DomainBlockNumber,
170}
171
172impl<DomainBlockNumber> From<(DomainId, EpochIndex, DomainBlockNumber)>
173 for OperatorDeregisteredInfo<DomainBlockNumber>
174{
175 fn from(value: (DomainId, EpochIndex, DomainBlockNumber)) -> Self {
176 OperatorDeregisteredInfo {
177 domain_epoch: (value.0, value.1).into(),
178 unlock_at_confirmed_domain_block_number: value.2,
179 }
180 }
181}
182
183#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
185pub enum OperatorStatus<DomainBlockNumber, ReceiptHash> {
186 #[codec(index = 0)]
187 Registered,
188 #[codec(index = 1)]
190 Deregistered(OperatorDeregisteredInfo<DomainBlockNumber>),
191 #[codec(index = 2)]
192 Slashed,
193 #[codec(index = 3)]
194 PendingSlash,
195 #[codec(index = 4)]
196 InvalidBundle(ReceiptHash),
197}
198
199#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
201pub struct Operator<Balance, Share, DomainBlockNumber, ReceiptHash> {
202 pub signing_key: OperatorPublicKey,
203 pub current_domain_id: DomainId,
204 pub next_domain_id: DomainId,
205 pub minimum_nominator_stake: Balance,
206 pub nomination_tax: Percent,
207 pub current_total_stake: Balance,
209 pub current_total_shares: Share,
211 partial_status: OperatorStatus<DomainBlockNumber, ReceiptHash>,
215 pub deposits_in_epoch: Balance,
217 pub withdrawals_in_epoch: Share,
219 pub total_storage_fee_deposit: Balance,
221}
222
223impl<Balance, Share, DomainBlockNumber, ReceiptHash>
224 Operator<Balance, Share, DomainBlockNumber, ReceiptHash>
225{
226 pub fn status<T: Config>(
227 &self,
228 operator_id: OperatorId,
229 ) -> &OperatorStatus<DomainBlockNumber, ReceiptHash> {
230 if matches!(self.partial_status, OperatorStatus::Slashed) {
231 &OperatorStatus::Slashed
232 } else if Pallet::<T>::is_operator_pending_to_slash(self.current_domain_id, operator_id) {
233 &OperatorStatus::PendingSlash
234 } else {
235 &self.partial_status
236 }
237 }
238
239 pub fn update_status(&mut self, new_status: OperatorStatus<DomainBlockNumber, ReceiptHash>) {
240 self.partial_status = new_status;
241 }
242}
243
244#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
245pub struct StakingSummary<OperatorId, Balance> {
246 pub current_epoch_index: EpochIndex,
248 pub current_total_stake: Balance,
250 pub current_operators: BTreeMap<OperatorId, Balance>,
252 pub next_operators: BTreeSet<OperatorId>,
254 pub current_epoch_rewards: BTreeMap<OperatorId, Balance>,
256}
257
258#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
259pub struct OperatorConfig<Balance> {
260 pub signing_key: OperatorPublicKey,
261 pub minimum_nominator_stake: Balance,
262 pub nomination_tax: Percent,
263}
264
265#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
266pub enum Error {
267 MaximumOperatorId,
268 DomainNotInitialized,
269 PendingOperatorSwitch,
270 InsufficientBalance,
271 InsufficientShares,
272 ZeroWithdraw,
273 BalanceFreeze,
274 MinimumOperatorStake,
275 UnknownOperator,
276 MinimumNominatorStake,
277 BalanceOverflow,
278 BalanceUnderflow,
279 NotOperatorOwner,
280 OperatorNotRegistered,
281 UnknownNominator,
282 MissingOperatorOwner,
283 MintBalance,
284 BlockNumberOverflow,
285 RemoveLock,
286 EpochOverflow,
287 ShareUnderflow,
288 ShareOverflow,
289 TooManyPendingStakingOperation,
290 OperatorNotAllowed,
291 InvalidOperatorSigningKey,
292 MissingOperatorEpochSharePrice,
293 MissingWithdrawal,
294 EpochNotComplete,
295 UnlockPeriodNotComplete,
296 OperatorNotDeregistered,
297 BundleStorageFund(bundle_storage_fund::Error),
298 UnconfirmedER,
299 TooManyWithdrawals,
300 ZeroDeposit,
301 ZeroSharePrice,
302}
303
304fn note_pending_staking_operation<T: Config>(domain_id: DomainId) -> Result<(), Error> {
307 let pending_op_count = PendingStakingOperationCount::<T>::get(domain_id);
308
309 ensure!(
310 pending_op_count < T::MaxPendingStakingOperation::get(),
311 Error::TooManyPendingStakingOperation
312 );
313
314 PendingStakingOperationCount::<T>::set(domain_id, pending_op_count.saturating_add(1));
315
316 Ok(())
317}
318
319pub fn do_register_operator<T: Config>(
320 operator_owner: T::AccountId,
321 domain_id: DomainId,
322 amount: BalanceOf<T>,
323 config: OperatorConfig<BalanceOf<T>>,
324) -> Result<(OperatorId, EpochIndex), Error> {
325 note_pending_staking_operation::<T>(domain_id)?;
326
327 DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_domain_stake_summary| {
328 ensure!(
329 config.signing_key != OperatorPublicKey::from(sr25519::Public::default()),
330 Error::InvalidOperatorSigningKey
331 );
332
333 ensure!(
334 config.minimum_nominator_stake >= T::MinNominatorStake::get(),
335 Error::MinimumNominatorStake
336 );
337
338 let domain_obj = DomainRegistry::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
339 ensure!(
340 domain_obj
341 .domain_config
342 .operator_allow_list
343 .is_operator_allowed(&operator_owner),
344 Error::OperatorNotAllowed
345 );
346
347 let operator_id = NextOperatorId::<T>::get();
348 let next_operator_id = operator_id.checked_add(1).ok_or(Error::MaximumOperatorId)?;
349 NextOperatorId::<T>::set(next_operator_id);
350
351 OperatorIdOwner::<T>::insert(operator_id, operator_owner.clone());
352
353 ensure!(
355 amount >= T::MinOperatorStake::get(),
356 Error::MinimumOperatorStake
357 );
358
359 let new_deposit =
360 deposit_reserve_for_storage_fund::<T>(operator_id, &operator_owner, amount)
361 .map_err(Error::BundleStorageFund)?;
362
363 hold_deposit::<T>(&operator_owner, operator_id, new_deposit.staking)?;
364
365 let domain_stake_summary = maybe_domain_stake_summary
366 .as_mut()
367 .ok_or(Error::DomainNotInitialized)?;
368
369 let OperatorConfig {
370 signing_key,
371 minimum_nominator_stake,
372 nomination_tax,
373 } = config;
374
375 let first_share_price = SharePrice::one();
384 let operator = Operator {
385 signing_key: signing_key.clone(),
386 current_domain_id: domain_id,
387 next_domain_id: domain_id,
388 minimum_nominator_stake,
389 nomination_tax,
390 current_total_stake: new_deposit.staking,
391 current_total_shares: first_share_price.stake_to_shares::<T>(new_deposit.staking),
392 partial_status: OperatorStatus::Registered,
393 deposits_in_epoch: Zero::zero(),
395 withdrawals_in_epoch: Zero::zero(),
396 total_storage_fee_deposit: new_deposit.storage_fee_deposit,
397 };
398 Operators::<T>::insert(operator_id, operator);
399 OperatorEpochSharePrice::<T>::insert(
400 operator_id,
401 DomainEpoch::from((domain_id, domain_stake_summary.current_epoch_index)),
402 first_share_price,
403 );
404
405 domain_stake_summary.next_operators.insert(operator_id);
407 let current_domain_epoch = (domain_id, domain_stake_summary.current_epoch_index).into();
409 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
410 operator_id,
411 operator_owner,
412 current_domain_epoch,
413 new_deposit,
414 None,
415 )?;
416
417 Ok((operator_id, domain_stake_summary.current_epoch_index))
418 })
419}
420
421pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T: Config>(
426 operator_id: OperatorId,
427 nominator_id: NominatorId<T>,
428 current_domain_epoch: DomainEpoch,
429 new_deposit: NewDeposit<BalanceOf<T>>,
430 required_minimum_nominator_stake: Option<BalanceOf<T>>,
431) -> Result<(), Error> {
432 Deposits::<T>::try_mutate(operator_id, nominator_id, |maybe_deposit| {
433 let mut deposit = maybe_deposit.take().unwrap_or_default();
434 do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, current_domain_epoch.1)?;
435
436 let pending_deposit = match deposit.pending {
438 None => PendingDeposit {
439 effective_domain_epoch: current_domain_epoch,
440 amount: new_deposit.staking,
441 storage_fee_deposit: new_deposit.storage_fee_deposit,
442 },
443 Some(pending_deposit) => PendingDeposit {
444 effective_domain_epoch: current_domain_epoch,
445 amount: pending_deposit
446 .amount
447 .checked_add(&new_deposit.staking)
448 .ok_or(Error::BalanceOverflow)?,
449 storage_fee_deposit: pending_deposit
450 .storage_fee_deposit
451 .checked_add(&new_deposit.storage_fee_deposit)
452 .ok_or(Error::BalanceOverflow)?,
453 },
454 };
455
456 if deposit.known.shares.is_zero()
457 && let Some(minimum_nominator_stake) = required_minimum_nominator_stake
458 {
459 ensure!(
460 pending_deposit.total()? >= minimum_nominator_stake,
461 Error::MinimumNominatorStake
462 );
463 }
464
465 deposit.pending = Some(pending_deposit);
466 *maybe_deposit = Some(deposit);
467 Ok(())
468 })
469}
470
471pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
472 operator_id: OperatorId,
473 deposit: &mut Deposit<T::Share, BalanceOf<T>>,
474 current_domain_epoch_index: EpochIndex,
475) -> Result<(), Error> {
476 let epoch_share_price = match deposit.pending {
478 None => return Ok(()),
479 Some(pending_deposit) => {
480 match OperatorEpochSharePrice::<T>::get(
481 operator_id,
482 pending_deposit.effective_domain_epoch,
483 ) {
484 Some(p) => p,
485 None => {
486 ensure!(
487 pending_deposit.effective_domain_epoch.1 >= current_domain_epoch_index,
488 Error::MissingOperatorEpochSharePrice
489 );
490 return Ok(());
491 }
492 }
493 }
494 };
495
496 if let Some(PendingDeposit {
497 amount,
498 storage_fee_deposit,
499 ..
500 }) = deposit.pending.take()
501 {
502 let new_shares = epoch_share_price.stake_to_shares::<T>(amount);
503 deposit.known.shares = deposit
504 .known
505 .shares
506 .checked_add(&new_shares)
507 .ok_or(Error::ShareOverflow)?;
508 deposit.known.storage_fee_deposit = deposit
509 .known
510 .storage_fee_deposit
511 .checked_add(&storage_fee_deposit)
512 .ok_or(Error::BalanceOverflow)?;
513 }
514
515 Ok(())
516}
517
518pub(crate) fn do_convert_previous_epoch_withdrawal<T: Config>(
524 operator_id: OperatorId,
525 withdrawal: &mut Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
526 current_domain_epoch_index: EpochIndex,
527) -> Result<(), Error> {
528 let epoch_share_price = match withdrawal.withdrawal_in_shares.as_ref() {
529 None => return Ok(()),
530 Some(withdraw) => {
531 if withdraw.domain_epoch.1 >= current_domain_epoch_index {
533 return Ok(());
534 }
535
536 match OperatorEpochSharePrice::<T>::get(operator_id, withdraw.domain_epoch) {
537 Some(p) => p,
538 None => return Err(Error::MissingOperatorEpochSharePrice),
539 }
540 }
541 };
542
543 if let Some(WithdrawalInShares {
544 unlock_at_confirmed_domain_block_number,
545 shares,
546 storage_fee_refund,
547 domain_epoch: _,
548 }) = withdrawal.withdrawal_in_shares.take()
549 {
550 let withdrawal_amount = epoch_share_price.shares_to_stake::<T>(shares);
551 withdrawal.total_withdrawal_amount = withdrawal
552 .total_withdrawal_amount
553 .checked_add(&withdrawal_amount)
554 .ok_or(Error::BalanceOverflow)?;
555
556 let withdraw_in_balance = WithdrawalInBalance {
557 unlock_at_confirmed_domain_block_number,
558 amount_to_unlock: withdrawal_amount,
559 storage_fee_refund,
560 };
561 withdrawal.withdrawals.push_back(withdraw_in_balance);
562 }
563
564 Ok(())
565}
566
567pub(crate) fn do_nominate_operator<T: Config>(
568 operator_id: OperatorId,
569 nominator_id: T::AccountId,
570 amount: BalanceOf<T>,
571) -> Result<(), Error> {
572 ensure!(!amount.is_zero(), Error::ZeroDeposit);
573
574 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
575 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
576
577 ensure!(
578 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
579 Error::OperatorNotRegistered
580 );
581
582 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
584 note_pending_staking_operation::<T>(operator.current_domain_id)?;
585 }
586
587 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
588 .ok_or(Error::DomainNotInitialized)?;
589
590 let new_deposit = deposit_reserve_for_storage_fund::<T>(operator_id, &nominator_id, amount)
592 .map_err(Error::BundleStorageFund)?;
593
594 hold_deposit::<T>(&nominator_id, operator_id, new_deposit.staking)?;
595 Pallet::<T>::deposit_event(Event::OperatorNominated {
596 operator_id,
597 nominator_id: nominator_id.clone(),
598 amount: new_deposit.staking,
599 });
600
601 operator.deposits_in_epoch = operator
603 .deposits_in_epoch
604 .checked_add(&new_deposit.staking)
605 .ok_or(Error::BalanceOverflow)?;
606
607 operator.total_storage_fee_deposit = operator
609 .total_storage_fee_deposit
610 .checked_add(&new_deposit.storage_fee_deposit)
611 .ok_or(Error::BalanceOverflow)?;
612
613 let current_domain_epoch = (
614 operator.current_domain_id,
615 domain_stake_summary.current_epoch_index,
616 )
617 .into();
618
619 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
620 operator_id,
621 nominator_id,
622 current_domain_epoch,
623 new_deposit,
624 Some(operator.minimum_nominator_stake),
625 )?;
626
627 Ok(())
628 })
629}
630
631pub(crate) fn hold_deposit<T: Config>(
632 who: &T::AccountId,
633 operator_id: OperatorId,
634 amount: BalanceOf<T>,
635) -> Result<(), Error> {
636 ensure!(
638 T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite) >= amount,
639 Error::InsufficientBalance
640 );
641
642 DepositOnHold::<T>::try_mutate((operator_id, who), |deposit_on_hold| {
643 *deposit_on_hold = deposit_on_hold
644 .checked_add(&amount)
645 .ok_or(Error::BalanceOverflow)?;
646 Ok(())
647 })?;
648
649 let pending_deposit_hold_id = T::HoldIdentifier::staking_staked();
650 T::Currency::hold(&pending_deposit_hold_id, who, amount).map_err(|_| Error::BalanceFreeze)?;
651
652 Ok(())
653}
654
655pub(crate) fn do_deregister_operator<T: Config>(
656 operator_owner: T::AccountId,
657 operator_id: OperatorId,
658) -> Result<(), Error> {
659 ensure!(
660 OperatorIdOwner::<T>::get(operator_id) == Some(operator_owner),
661 Error::NotOperatorOwner
662 );
663
664 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
665 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
666
667 ensure!(
668 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
669 Error::OperatorNotRegistered
670 );
671
672 DomainStakingSummary::<T>::try_mutate(
673 operator.current_domain_id,
674 |maybe_domain_stake_summary| {
675 let stake_summary = maybe_domain_stake_summary
676 .as_mut()
677 .ok_or(Error::DomainNotInitialized)?;
678
679 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
680 let unlock_operator_at_domain_block_number = head_domain_number
681 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
682 .ok_or(Error::BlockNumberOverflow)?;
683 let operator_deregister_info = (
684 operator.current_domain_id,
685 stake_summary.current_epoch_index,
686 unlock_operator_at_domain_block_number,
687 )
688 .into();
689
690 operator.update_status(OperatorStatus::Deregistered(operator_deregister_info));
691
692 stake_summary.next_operators.remove(&operator_id);
693
694 DeregisteredOperators::<T>::mutate(operator.current_domain_id, |operators| {
695 operators.insert(operator_id)
696 });
697 Ok(())
698 },
699 )
700 })
701}
702
703pub(crate) fn current_share_price<T: Config>(
706 operator_id: OperatorId,
707 operator: &Operator<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
708 domain_stake_summary: &StakingSummary<OperatorId, BalanceOf<T>>,
709) -> Result<SharePrice, Error> {
710 let total_stake = domain_stake_summary
712 .current_epoch_rewards
713 .get(&operator_id)
714 .and_then(|rewards| {
715 let operator_tax = operator.nomination_tax.mul_floor(*rewards);
716 operator
717 .current_total_stake
718 .checked_add(rewards)?
719 .checked_sub(&operator_tax)
721 })
722 .unwrap_or(operator.current_total_stake);
723
724 SharePrice::new::<T>(operator.current_total_shares, total_stake)
725}
726
727pub(crate) fn do_withdraw_stake<T: Config>(
736 operator_id: OperatorId,
737 nominator_id: NominatorId<T>,
738 to_withdraw: T::Share,
739) -> Result<(), Error> {
740 ensure!(!to_withdraw.is_zero(), Error::ZeroWithdraw);
743
744 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
745 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
746 ensure!(
747 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
748 Error::OperatorNotRegistered
749 );
750
751 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
753 note_pending_staking_operation::<T>(operator.current_domain_id)?;
754 }
755
756 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
758 .ok_or(Error::DomainNotInitialized)?;
759 let domain_current_epoch = (
760 operator.current_domain_id,
761 domain_stake_summary.current_epoch_index,
762 )
763 .into();
764
765 let known_shares =
766 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
767 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
768 do_convert_previous_epoch_deposits::<T>(
769 operator_id,
770 deposit,
771 domain_stake_summary.current_epoch_index,
772 )?;
773 Ok(deposit.known.shares)
774 })?;
775
776 Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
777 if let Some(withdrawal) = maybe_withdrawal {
778 do_convert_previous_epoch_withdrawal::<T>(
779 operator_id,
780 withdrawal,
781 domain_stake_summary.current_epoch_index,
782 )?;
783 if withdrawal.withdrawals.len() as u32 >= T::WithdrawalLimit::get() {
784 return Err(Error::TooManyWithdrawals);
785 }
786 }
787 Ok(())
788 })?;
789
790 let operator_owner =
791 OperatorIdOwner::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
792
793 let is_operator_owner = operator_owner == nominator_id;
794
795 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
796 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
797
798 let (remaining_shares, shares_withdrew) = {
799 let remaining_shares = known_shares
800 .checked_sub(&to_withdraw)
801 .ok_or(Error::InsufficientShares)?;
802
803 if remaining_shares.is_zero() {
805 if is_operator_owner {
806 return Err(Error::MinimumOperatorStake);
807 }
808
809 (remaining_shares, to_withdraw)
810 } else {
811 let share_price =
812 current_share_price::<T>(operator_id, operator, &domain_stake_summary)?;
813
814 let remaining_storage_fee =
815 Perquintill::from_rational(remaining_shares.into(), known_shares.into())
816 .mul_floor(deposit.known.storage_fee_deposit);
817
818 let remaining_stake = share_price
819 .shares_to_stake::<T>(remaining_shares)
820 .checked_add(&remaining_storage_fee)
821 .ok_or(Error::BalanceOverflow)?;
822
823 if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
826 return Err(Error::MinimumOperatorStake);
827 }
828
829 if !is_operator_owner && remaining_stake.lt(&operator.minimum_nominator_stake) {
831 (T::Share::zero(), known_shares)
832 } else {
833 (remaining_shares, to_withdraw)
834 }
835 }
836 };
837
838 let storage_fee_to_withdraw =
841 Perquintill::from_rational(shares_withdrew.into(), known_shares.into())
842 .mul_floor(deposit.known.storage_fee_deposit);
843
844 let withdraw_storage_fee = {
845 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
846 operator_id,
847 operator.total_storage_fee_deposit,
848 );
849 bundle_storage_fund::withdraw_and_hold::<T>(
850 operator_id,
851 &nominator_id,
852 storage_fund_redeem_price.redeem(storage_fee_to_withdraw),
853 )
854 .map_err(Error::BundleStorageFund)?
855 };
856
857 deposit.known.storage_fee_deposit = deposit
858 .known
859 .storage_fee_deposit
860 .checked_sub(&storage_fee_to_withdraw)
861 .ok_or(Error::BalanceOverflow)?;
862
863 operator.total_storage_fee_deposit = operator
864 .total_storage_fee_deposit
865 .checked_sub(&storage_fee_to_withdraw)
866 .ok_or(Error::BalanceOverflow)?;
867
868 operator.withdrawals_in_epoch = operator
870 .withdrawals_in_epoch
871 .checked_add(&shares_withdrew)
872 .ok_or(Error::ShareOverflow)?;
873
874 deposit.known.shares = remaining_shares;
875 if remaining_shares.is_zero()
876 && let Some(pending_deposit) = deposit.pending
877 {
878 ensure!(
881 pending_deposit.total()? >= operator.minimum_nominator_stake,
882 Error::MinimumNominatorStake
883 );
884 }
885
886 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
887 let unlock_at_confirmed_domain_block_number = head_domain_number
888 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
889 .ok_or(Error::BlockNumberOverflow)?;
890
891 Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
892 let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
893 let new_withdrawal_in_shares = match withdrawal.withdrawal_in_shares.take() {
896 Some(WithdrawalInShares {
897 shares,
898 storage_fee_refund,
899 ..
900 }) => WithdrawalInShares {
901 domain_epoch: domain_current_epoch,
902 shares: shares
903 .checked_add(&shares_withdrew)
904 .ok_or(Error::ShareOverflow)?,
905 unlock_at_confirmed_domain_block_number,
906 storage_fee_refund: storage_fee_refund
907 .checked_add(&withdraw_storage_fee)
908 .ok_or(Error::BalanceOverflow)?,
909 },
910 None => WithdrawalInShares {
911 domain_epoch: domain_current_epoch,
912 unlock_at_confirmed_domain_block_number,
913 shares: shares_withdrew,
914 storage_fee_refund: withdraw_storage_fee,
915 },
916 };
917 withdrawal.withdrawal_in_shares = Some(new_withdrawal_in_shares);
918 withdrawal.total_storage_fee_withdrawal = withdrawal
919 .total_storage_fee_withdrawal
920 .checked_add(&withdraw_storage_fee)
921 .ok_or(Error::BalanceOverflow)?;
922
923 *maybe_withdrawal = Some(withdrawal);
924 Ok(())
925 })
926 })
927 })
928}
929
930pub(crate) fn do_unlock_funds<T: Config>(
934 operator_id: OperatorId,
935 nominator_id: NominatorId<T>,
936) -> Result<u32, Error> {
937 let operator = Operators::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
938 ensure!(
939 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
940 Error::OperatorNotRegistered
941 );
942
943 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
944 .ok_or(Error::DomainNotInitialized)?
945 .current_epoch_index;
946
947 Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
948 let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
949 do_convert_previous_epoch_withdrawal::<T>(
950 operator_id,
951 withdrawal,
952 current_domain_epoch_index,
953 )?;
954
955 ensure!(!withdrawal.withdrawals.is_empty(), Error::MissingWithdrawal);
956
957 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
958
959 let mut withdrawal_count = 0;
960 let mut total_unlocked_amount = BalanceOf::<T>::zero();
961 let mut total_storage_fee_refund = BalanceOf::<T>::zero();
962 loop {
963 if withdrawal
964 .withdrawals
965 .front()
966 .map(|w| w.unlock_at_confirmed_domain_block_number > head_domain_number)
967 .unwrap_or(true)
968 {
969 break;
970 }
971
972 let WithdrawalInBalance {
973 amount_to_unlock,
974 storage_fee_refund,
975 ..
976 } = withdrawal
977 .withdrawals
978 .pop_front()
979 .expect("Must not empty as checked above; qed");
980
981 total_unlocked_amount = total_unlocked_amount
982 .checked_add(&amount_to_unlock)
983 .ok_or(Error::BalanceOverflow)?;
984
985 total_storage_fee_refund = total_storage_fee_refund
986 .checked_add(&storage_fee_refund)
987 .ok_or(Error::BalanceOverflow)?;
988
989 withdrawal_count += 1;
990 }
991
992 ensure!(
995 !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
996 Error::UnlockPeriodNotComplete
997 );
998
999 withdrawal.total_withdrawal_amount = withdrawal
1001 .total_withdrawal_amount
1002 .checked_sub(&total_unlocked_amount)
1003 .ok_or(Error::BalanceUnderflow)?;
1004
1005 withdrawal.total_storage_fee_withdrawal = withdrawal
1006 .total_storage_fee_withdrawal
1007 .checked_sub(&total_storage_fee_refund)
1008 .ok_or(Error::BalanceUnderflow)?;
1009
1010 let (amount_to_mint, amount_to_release) = DepositOnHold::<T>::try_mutate(
1013 (operator_id, nominator_id.clone()),
1014 |deposit_on_hold| {
1015 let amount_to_release = total_unlocked_amount.min(*deposit_on_hold);
1016 let amount_to_mint = total_unlocked_amount.saturating_sub(*deposit_on_hold);
1017
1018 *deposit_on_hold = deposit_on_hold.saturating_sub(amount_to_release);
1019
1020 Ok((amount_to_mint, amount_to_release))
1021 },
1022 )?;
1023
1024 if !amount_to_mint.is_zero() {
1026 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1027 }
1028 if !amount_to_release.is_zero() {
1030 let staked_hold_id = T::HoldIdentifier::staking_staked();
1031 T::Currency::release(
1032 &staked_hold_id,
1033 &nominator_id,
1034 amount_to_release,
1035 Precision::Exact,
1036 )
1037 .map_err(|_| Error::RemoveLock)?;
1038 }
1039
1040 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1041 operator_id,
1042 nominator_id: nominator_id.clone(),
1043 unlocked_amount: total_unlocked_amount,
1044 });
1045
1046 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1048 T::Currency::release(
1049 &storage_fund_hold_id,
1050 &nominator_id,
1051 total_storage_fee_refund,
1052 Precision::Exact,
1053 )
1054 .map_err(|_| Error::RemoveLock)?;
1055
1056 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1057 operator_id,
1058 nominator_id: nominator_id.clone(),
1059 storage_fee: total_storage_fee_refund,
1060 });
1061
1062 if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1064 *maybe_withdrawal = None;
1065 Deposits::<T>::mutate_exists(operator_id, nominator_id.clone(), |maybe_deposit| {
1067 if let Some(deposit) = maybe_deposit
1068 && deposit.known.shares.is_zero()
1069 && deposit.pending.is_none()
1070 {
1071 *maybe_deposit = None;
1072
1073 DepositOnHold::<T>::mutate_exists(
1074 (operator_id, nominator_id),
1075 |maybe_deposit_on_hold| {
1076 if let Some(deposit_on_hold) = maybe_deposit_on_hold
1077 && deposit_on_hold.is_zero()
1078 {
1079 *maybe_deposit_on_hold = None
1080 }
1081 },
1082 );
1083 }
1084 });
1085 }
1086
1087 Ok(withdrawal_count)
1088 })
1089}
1090
1091pub(crate) fn do_unlock_nominator<T: Config>(
1093 operator_id: OperatorId,
1094 nominator_id: NominatorId<T>,
1095) -> Result<(), Error> {
1096 Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
1097 let mut operator = maybe_operator.take().ok_or(Error::UnknownOperator)?;
1099 let OperatorDeregisteredInfo {
1100 domain_epoch,
1101 unlock_at_confirmed_domain_block_number,
1102 } = match operator.status::<T>(operator_id) {
1103 OperatorStatus::Deregistered(operator_deregistered_info) => operator_deregistered_info,
1104 _ => return Err(Error::OperatorNotDeregistered),
1105 };
1106
1107 let (domain_id, _) = domain_epoch.deconstruct();
1108 let head_domain_number = HeadDomainNumber::<T>::get(domain_id);
1109 ensure!(
1110 *unlock_at_confirmed_domain_block_number <= head_domain_number,
1111 Error::UnlockPeriodNotComplete
1112 );
1113
1114 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1115 .ok_or(Error::DomainNotInitialized)?
1116 .current_epoch_index;
1117
1118 let mut total_shares = operator.current_total_shares;
1119 let mut total_stake = operator.current_total_stake;
1120 let share_price = SharePrice::new::<T>(total_shares, total_stake)?;
1121
1122 let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
1123 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1124 operator_id,
1125 total_storage_fee_deposit,
1126 );
1127 let mut deposit = Deposits::<T>::take(operator_id, nominator_id.clone())
1128 .ok_or(Error::UnknownNominator)?;
1129
1130 do_convert_previous_epoch_deposits::<T>(
1137 operator_id,
1138 &mut deposit,
1139 current_domain_epoch_index,
1140 )?;
1141
1142 let (
1145 amount_ready_to_withdraw,
1146 total_storage_fee_withdrawal,
1147 shares_withdrew_in_current_epoch,
1148 ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
1149 .map(|mut withdrawal| {
1150 do_convert_previous_epoch_withdrawal::<T>(
1155 operator_id,
1156 &mut withdrawal,
1157 current_domain_epoch_index,
1158 )?;
1159 Ok((
1160 withdrawal.total_withdrawal_amount,
1161 withdrawal.total_storage_fee_withdrawal,
1162 withdrawal
1163 .withdrawal_in_shares
1164 .map(|WithdrawalInShares { shares, .. }| shares)
1165 .unwrap_or_default(),
1166 ))
1167 })
1168 .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
1169
1170 let nominator_shares = deposit
1172 .known
1173 .shares
1174 .checked_add(&shares_withdrew_in_current_epoch)
1175 .ok_or(Error::ShareOverflow)?;
1176 total_shares = total_shares
1177 .checked_sub(&nominator_shares)
1178 .ok_or(Error::ShareOverflow)?;
1179
1180 let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
1182 total_stake = total_stake
1183 .checked_sub(&nominator_staked_amount)
1184 .ok_or(Error::BalanceOverflow)?;
1185
1186 let amount_deposited_in_epoch = deposit
1188 .pending
1189 .map(|pending_deposit| pending_deposit.amount)
1190 .unwrap_or_default();
1191
1192 let total_amount_to_unlock = nominator_staked_amount
1193 .checked_add(&amount_ready_to_withdraw)
1194 .and_then(|amount| amount.checked_add(&amount_deposited_in_epoch))
1195 .ok_or(Error::BalanceOverflow)?;
1196
1197 let current_locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
1199 if let Some(amount_to_mint) = total_amount_to_unlock.checked_sub(¤t_locked_amount) {
1200 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1201 }
1202 if !current_locked_amount.is_zero() {
1203 let staked_hold_id = T::HoldIdentifier::staking_staked();
1204 T::Currency::release(
1205 &staked_hold_id,
1206 &nominator_id,
1207 current_locked_amount,
1208 Precision::Exact,
1209 )
1210 .map_err(|_| Error::RemoveLock)?;
1211 }
1212
1213 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1214 operator_id,
1215 nominator_id: nominator_id.clone(),
1216 unlocked_amount: total_amount_to_unlock,
1217 });
1218
1219 let nominator_total_storage_fee_deposit = deposit
1221 .pending
1222 .map(|pending_deposit| pending_deposit.storage_fee_deposit)
1223 .unwrap_or(Zero::zero())
1224 .checked_add(&deposit.known.storage_fee_deposit)
1225 .ok_or(Error::BalanceOverflow)?;
1226
1227 bundle_storage_fund::withdraw_to::<T>(
1228 operator_id,
1229 &nominator_id,
1230 storage_fund_redeem_price.redeem(nominator_total_storage_fee_deposit),
1231 )
1232 .map_err(Error::BundleStorageFund)?;
1233
1234 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1236 T::Currency::release(
1237 &storage_fund_hold_id,
1238 &nominator_id,
1239 total_storage_fee_withdrawal,
1240 Precision::Exact,
1241 )
1242 .map_err(|_| Error::RemoveLock)?;
1243
1244 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1245 operator_id,
1246 nominator_id: nominator_id.clone(),
1247 storage_fee: total_storage_fee_withdrawal,
1248 });
1249
1250 total_storage_fee_deposit =
1252 total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1253
1254 let cleanup_operator = !Deposits::<T>::contains_prefix(operator_id)
1257 && !Withdrawals::<T>::contains_prefix(operator_id);
1258
1259 if cleanup_operator {
1260 do_cleanup_operator::<T>(operator_id, total_stake)?
1261 } else {
1262 operator.current_total_shares = total_shares;
1264 operator.current_total_stake = total_stake;
1265 operator.total_storage_fee_deposit = total_storage_fee_deposit;
1266
1267 *maybe_operator = Some(operator);
1268 }
1269
1270 Ok(())
1271 })
1272}
1273
1274pub(crate) fn do_cleanup_operator<T: Config>(
1276 operator_id: OperatorId,
1277 total_stake: BalanceOf<T>,
1278) -> Result<(), Error> {
1279 bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1281 .map_err(Error::BundleStorageFund)?;
1282
1283 mint_into_treasury::<T>(total_stake)?;
1285
1286 OperatorIdOwner::<T>::remove(operator_id);
1288
1289 OperatorHighestSlot::<T>::remove(operator_id);
1291
1292 let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1294
1295 Ok(())
1296}
1297
1298pub(crate) fn do_reward_operators<T: Config>(
1300 domain_id: DomainId,
1301 source: OperatorRewardSource<BlockNumberFor<T>>,
1302 operators: IntoIter<OperatorId>,
1303 rewards: BalanceOf<T>,
1304) -> Result<(), Error> {
1305 if rewards.is_zero() {
1306 return Ok(());
1307 }
1308 DomainStakingSummary::<T>::mutate(domain_id, |maybe_stake_summary| {
1309 let stake_summary = maybe_stake_summary
1310 .as_mut()
1311 .ok_or(Error::DomainNotInitialized)?;
1312
1313 let total_count = operators.len() as u64;
1314 let operator_weights = operators.into_iter().fold(
1316 BTreeMap::<OperatorId, u64>::new(),
1317 |mut acc, operator_id| {
1318 acc.entry(operator_id)
1319 .and_modify(|weight| *weight += 1)
1320 .or_insert(1);
1321 acc
1322 },
1323 );
1324
1325 let mut allocated_rewards = BalanceOf::<T>::zero();
1326 for (operator_id, weight) in operator_weights {
1327 let operator_reward = {
1328 let distribution = Perquintill::from_rational(weight, total_count);
1329 distribution.mul_floor(rewards)
1330 };
1331
1332 stake_summary
1333 .current_epoch_rewards
1334 .entry(operator_id)
1335 .and_modify(|rewards| *rewards = rewards.saturating_add(operator_reward))
1336 .or_insert(operator_reward);
1337
1338 Pallet::<T>::deposit_event(Event::OperatorRewarded {
1339 source: source.clone(),
1340 operator_id,
1341 reward: operator_reward,
1342 });
1343
1344 allocated_rewards = allocated_rewards
1345 .checked_add(&operator_reward)
1346 .ok_or(Error::BalanceOverflow)?;
1347 }
1348
1349 mint_into_treasury::<T>(
1351 rewards
1352 .checked_sub(&allocated_rewards)
1353 .ok_or(Error::BalanceUnderflow)?,
1354 )
1355 })
1356}
1357
1358pub(crate) fn do_mark_operators_as_slashed<T: Config>(
1361 operator_ids: impl AsRef<[OperatorId]>,
1362 slash_reason: SlashedReason<DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
1363) -> Result<(), Error> {
1364 for operator_id in operator_ids.as_ref() {
1365 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1366 let operator = match maybe_operator.as_mut() {
1367 None => return Ok(()),
1371 Some(operator) => operator,
1372 };
1373 let mut pending_slashes =
1374 PendingSlashes::<T>::get(operator.current_domain_id).unwrap_or_default();
1375
1376 if pending_slashes.contains(operator_id) {
1377 return Ok(());
1378 }
1379
1380 DomainStakingSummary::<T>::try_mutate(
1381 operator.current_domain_id,
1382 |maybe_domain_stake_summary| {
1383 let stake_summary = maybe_domain_stake_summary
1384 .as_mut()
1385 .ok_or(Error::DomainNotInitialized)?;
1386
1387 operator.update_status(OperatorStatus::Slashed);
1389
1390 if stake_summary
1393 .current_operators
1394 .remove(operator_id)
1395 .is_some()
1396 {
1397 stake_summary.current_total_stake = stake_summary
1398 .current_total_stake
1399 .checked_sub(&operator.current_total_stake)
1400 .ok_or(Error::BalanceUnderflow)?;
1401 }
1402 stake_summary.next_operators.remove(operator_id);
1403 pending_slashes.insert(*operator_id);
1404 PendingSlashes::<T>::insert(operator.current_domain_id, pending_slashes);
1405 Pallet::<T>::deposit_event(Event::OperatorSlashed {
1406 operator_id: *operator_id,
1407 reason: slash_reason.clone(),
1408 });
1409 Ok(())
1410 },
1411 )
1412 })?
1413 }
1414
1415 Ok(())
1416}
1417
1418pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1420 domain_id: DomainId,
1421 er: &ExecutionReceiptOf<T>,
1422) -> Result<(), Error> {
1423 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1424 let er_hash = er.hash::<DomainHashingFor<T>>();
1425 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1426 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1427 let mut stake_summary =
1428 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1429
1430 for operator_id in invalid_bundle_authors {
1431 if pending_slashes.contains(&operator_id) {
1432 continue;
1433 }
1434
1435 mark_invalid_bundle_author::<T>(
1436 operator_id,
1437 er_hash,
1438 &mut stake_summary,
1439 &mut invalid_bundle_authors_in_epoch,
1440 )?;
1441 }
1442
1443 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1444 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1445 Ok(())
1446}
1447
1448pub(crate) fn mark_invalid_bundle_author<T: Config>(
1449 operator_id: OperatorId,
1450 er_hash: ReceiptHashFor<T>,
1451 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1452 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1453) -> Result<(), Error> {
1454 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1455 let operator = match maybe_operator.as_mut() {
1456 None => return Ok(()),
1460 Some(operator) => operator,
1461 };
1462
1463 if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1466 return Ok(());
1467 }
1468
1469 operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1471 invalid_bundle_authors.insert(operator_id);
1472 if stake_summary
1473 .current_operators
1474 .remove(&operator_id)
1475 .is_some()
1476 {
1477 stake_summary.current_total_stake = stake_summary
1478 .current_total_stake
1479 .checked_sub(&operator.current_total_stake)
1480 .ok_or(Error::BalanceUnderflow)?;
1481 }
1482 stake_summary.next_operators.remove(&operator_id);
1483 Ok(())
1484 })
1485}
1486
1487pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1491 domain_id: DomainId,
1492 er: &ExecutionReceiptOf<T>,
1493) -> Result<(), Error> {
1494 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1495 let er_hash = er.hash::<DomainHashingFor<T>>();
1496 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1497 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1498 let mut stake_summary =
1499 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1500
1501 for operator_id in invalid_bundle_authors {
1502 if pending_slashes.contains(&operator_id)
1503 || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1504 {
1505 continue;
1506 }
1507
1508 unmark_invalid_bundle_author::<T>(
1509 operator_id,
1510 er_hash,
1511 &mut stake_summary,
1512 &mut invalid_bundle_authors_in_epoch,
1513 )?;
1514 }
1515
1516 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1517 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1518 Ok(())
1519}
1520
1521fn unmark_invalid_bundle_author<T: Config>(
1522 operator_id: OperatorId,
1523 er_hash: ReceiptHashFor<T>,
1524 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1525 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1526) -> Result<(), Error> {
1527 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1528 let operator = match maybe_operator.as_mut() {
1529 None => return Ok(()),
1533 Some(operator) => operator,
1534 };
1535
1536 if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1538 return Ok(());
1539 }
1540
1541 operator.update_status(OperatorStatus::Registered);
1543 invalid_bundle_authors.remove(&operator_id);
1544 stake_summary.next_operators.insert(operator_id);
1545 Ok(())
1546 })
1547}
1548
1549#[cfg(test)]
1550pub(crate) mod tests {
1551 use crate::domain_registry::{DomainConfig, DomainObject};
1552 use crate::pallet::{
1553 Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1554 NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1555 };
1556 use crate::staking::{
1557 DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorStatus, SharePrice,
1558 StakingSummary, do_convert_previous_epoch_withdrawal, do_mark_operators_as_slashed,
1559 do_nominate_operator, do_reward_operators, do_unlock_funds, do_withdraw_stake,
1560 };
1561 use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1562 use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext};
1563 use crate::{
1564 BalanceOf, Error, MAX_NOMINATORS_TO_SLASH, NominatorId, OperatorEpochSharePrice,
1565 SlashedReason, bundle_storage_fund,
1566 };
1567 use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
1568 use frame_support::traits::Currency;
1569 use frame_support::traits::fungible::Mutate;
1570 use frame_support::weights::Weight;
1571 use frame_support::{assert_err, assert_ok};
1572 use prop_test::prelude::*;
1573 use prop_test::proptest::test_runner::TestCaseResult;
1574 use sp_core::{Pair, sr25519};
1575 use sp_domains::{
1576 DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1577 OperatorRewardSource,
1578 };
1579 use sp_runtime::traits::Zero;
1580 use sp_runtime::{PerThing, Percent, Perquintill};
1581 use std::collections::{BTreeMap, BTreeSet};
1582 use std::ops::RangeInclusive;
1583 use std::vec;
1584 use subspace_runtime_primitives::AI3;
1585
1586 type Balances = pallet_balances::Pallet<Test>;
1587 type Domains = crate::Pallet<Test>;
1588
1589 const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1590
1591 #[allow(clippy::too_many_arguments)]
1592 pub(crate) fn register_operator(
1593 domain_id: DomainId,
1594 operator_account: <Test as frame_system::Config>::AccountId,
1595 operator_free_balance: BalanceOf<Test>,
1596 operator_stake: BalanceOf<Test>,
1597 minimum_nominator_stake: BalanceOf<Test>,
1598 signing_key: OperatorPublicKey,
1599 nomination_tax: Percent,
1600 mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1601 ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1602 nominators.insert(operator_account, (operator_free_balance, operator_stake));
1603 for nominator in &nominators {
1604 Balances::set_balance(nominator.0, nominator.1.0);
1605 assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1606 }
1607 nominators.remove(&operator_account);
1608
1609 if !DomainRegistry::<Test>::contains_key(domain_id) {
1610 let domain_config = DomainConfig {
1611 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1612 runtime_id: 0,
1613 max_bundle_size: u32::MAX,
1614 max_bundle_weight: Weight::MAX,
1615 bundle_slot_probability: (0, 0),
1616 operator_allow_list: OperatorAllowList::Anyone,
1617 initial_balances: Default::default(),
1618 };
1619
1620 let domain_obj = DomainObject {
1621 owner_account_id: 0,
1622 created_at: 0,
1623 genesis_receipt_hash: Default::default(),
1624 domain_config,
1625 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1626 domain_instantiation_deposit: Default::default(),
1627 };
1628
1629 DomainRegistry::<Test>::insert(domain_id, domain_obj);
1630 }
1631
1632 if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1633 DomainStakingSummary::<Test>::insert(
1634 domain_id,
1635 StakingSummary {
1636 current_epoch_index: 0,
1637 current_total_stake: 0,
1638 current_operators: BTreeMap::new(),
1639 next_operators: BTreeSet::new(),
1640 current_epoch_rewards: BTreeMap::new(),
1641 },
1642 );
1643 }
1644
1645 let operator_config = OperatorConfig {
1646 signing_key,
1647 minimum_nominator_stake,
1648 nomination_tax,
1649 };
1650
1651 let res = Domains::register_operator(
1652 RuntimeOrigin::signed(operator_account),
1653 domain_id,
1654 operator_stake,
1655 operator_config.clone(),
1656 );
1657 assert_ok!(res);
1658
1659 let operator_id = NextOperatorId::<Test>::get() - 1;
1660 for nominator in nominators {
1661 if nominator.1.1.is_zero() {
1662 continue;
1663 }
1664
1665 let res = Domains::nominate_operator(
1666 RuntimeOrigin::signed(nominator.0),
1667 operator_id,
1668 nominator.1.1,
1669 );
1670 assert_ok!(res);
1671 assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1672 }
1673
1674 (operator_id, operator_config)
1675 }
1676
1677 #[test]
1678 fn test_register_operator_invalid_signing_key() {
1679 let domain_id = DomainId::new(0);
1680 let operator_account = 1;
1681
1682 let mut ext = new_test_ext();
1683 ext.execute_with(|| {
1684 let operator_config = OperatorConfig {
1685 signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1686 minimum_nominator_stake: Default::default(),
1687 nomination_tax: Default::default(),
1688 };
1689
1690 let res = Domains::register_operator(
1691 RuntimeOrigin::signed(operator_account),
1692 domain_id,
1693 Default::default(),
1694 operator_config,
1695 );
1696 assert_err!(
1697 res,
1698 Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1699 );
1700 });
1701 }
1702
1703 #[test]
1704 fn test_register_operator_minimum_nominator_stake() {
1705 let domain_id = DomainId::new(0);
1706 let operator_account = 1;
1707 let pair = OperatorPair::from_seed(&[0; 32]);
1708
1709 let mut ext = new_test_ext();
1710 ext.execute_with(|| {
1711 let operator_config = OperatorConfig {
1712 signing_key: pair.public(),
1713 minimum_nominator_stake: Default::default(),
1714 nomination_tax: Default::default(),
1715 };
1716
1717 let res = Domains::register_operator(
1718 RuntimeOrigin::signed(operator_account),
1719 domain_id,
1720 Default::default(),
1721 operator_config,
1722 );
1723 assert_err!(
1724 res,
1725 Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1726 );
1727 });
1728 }
1729
1730 #[test]
1731 fn test_register_operator() {
1732 let domain_id = DomainId::new(0);
1733 let operator_account = 1;
1734 let operator_free_balance = 2500 * AI3;
1735 let operator_total_stake = 1000 * AI3;
1736 let operator_stake = 800 * AI3;
1737 let operator_storage_fee_deposit = 200 * AI3;
1738 let pair = OperatorPair::from_seed(&[0; 32]);
1739
1740 let mut ext = new_test_ext();
1741 ext.execute_with(|| {
1742 let (operator_id, mut operator_config) = register_operator(
1743 domain_id,
1744 operator_account,
1745 operator_free_balance,
1746 operator_total_stake,
1747 AI3,
1748 pair.public(),
1749 Default::default(),
1750 BTreeMap::new(),
1751 );
1752
1753 assert_eq!(NextOperatorId::<Test>::get(), 1);
1754 assert_eq!(
1756 OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1757 operator_account
1758 );
1759 assert_eq!(
1760 Operators::<Test>::get(operator_id).unwrap(),
1761 Operator {
1762 signing_key: pair.public(),
1763 current_domain_id: domain_id,
1764 next_domain_id: domain_id,
1765 minimum_nominator_stake: AI3,
1766 nomination_tax: Default::default(),
1767 current_total_stake: operator_stake,
1768 current_total_shares: operator_stake,
1769 partial_status: OperatorStatus::Registered,
1770 deposits_in_epoch: 0,
1771 withdrawals_in_epoch: 0,
1772 total_storage_fee_deposit: operator_storage_fee_deposit,
1773 }
1774 );
1775
1776 let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1777 assert!(stake_summary.next_operators.contains(&operator_id));
1778 assert_eq!(stake_summary.current_total_stake, operator_stake);
1779
1780 assert_eq!(
1781 Balances::usable_balance(operator_account),
1782 operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1783 );
1784
1785 let res = Domains::register_operator(
1787 RuntimeOrigin::signed(operator_account),
1788 domain_id,
1789 operator_stake,
1790 operator_config.clone(),
1791 );
1792 assert_ok!(res);
1793
1794 let new_pair = OperatorPair::from_seed(&[1; 32]);
1796 operator_config.signing_key = new_pair.public();
1797 let res = Domains::register_operator(
1798 RuntimeOrigin::signed(operator_account),
1799 domain_id,
1800 operator_stake,
1801 operator_config,
1802 );
1803 assert_err!(
1804 res,
1805 Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1806 );
1807 });
1808 }
1809
1810 #[test]
1811 fn nominate_operator() {
1812 let domain_id = DomainId::new(0);
1813 let operator_account = 1;
1814 let operator_free_balance = 1500 * AI3;
1815 let operator_total_stake = 1000 * AI3;
1816 let operator_stake = 800 * AI3;
1817 let operator_storage_fee_deposit = 200 * AI3;
1818 let pair = OperatorPair::from_seed(&[0; 32]);
1819
1820 let nominator_account = 2;
1821 let nominator_free_balance = 150 * AI3;
1822 let nominator_total_stake = 100 * AI3;
1823 let nominator_stake = 80 * AI3;
1824 let nominator_storage_fee_deposit = 20 * AI3;
1825
1826 let mut ext = new_test_ext();
1827 ext.execute_with(|| {
1828 let (operator_id, _) = register_operator(
1829 domain_id,
1830 operator_account,
1831 operator_free_balance,
1832 operator_total_stake,
1833 10 * AI3,
1834 pair.public(),
1835 Default::default(),
1836 BTreeMap::from_iter(vec![(
1837 nominator_account,
1838 (nominator_free_balance, nominator_total_stake),
1839 )]),
1840 );
1841
1842 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1843 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
1844
1845 let operator = Operators::<Test>::get(operator_id).unwrap();
1846 assert_eq!(operator.current_total_stake, operator_stake);
1847 assert_eq!(operator.current_total_shares, operator_stake);
1848 assert_eq!(
1849 operator.total_storage_fee_deposit,
1850 operator_storage_fee_deposit + nominator_storage_fee_deposit
1851 );
1852 assert_eq!(operator.deposits_in_epoch, nominator_stake);
1853
1854 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1855 .unwrap()
1856 .pending
1857 .unwrap();
1858 assert_eq!(pending_deposit.amount, nominator_stake);
1859 assert_eq!(
1860 pending_deposit.storage_fee_deposit,
1861 nominator_storage_fee_deposit
1862 );
1863 assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
1864
1865 assert_eq!(
1866 Balances::usable_balance(nominator_account),
1867 nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
1868 );
1869
1870 let additional_nomination_total_stake = 40 * AI3;
1872 let additional_nomination_stake = 32 * AI3;
1873 let additional_nomination_storage_fee_deposit = 8 * AI3;
1874 let res = Domains::nominate_operator(
1875 RuntimeOrigin::signed(nominator_account),
1876 operator_id,
1877 additional_nomination_total_stake,
1878 );
1879 assert_ok!(res);
1880 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1881 .unwrap()
1882 .pending
1883 .unwrap();
1884 assert_eq!(
1885 pending_deposit.amount,
1886 nominator_stake + additional_nomination_stake
1887 );
1888 assert_eq!(
1889 pending_deposit.storage_fee_deposit,
1890 nominator_storage_fee_deposit + additional_nomination_storage_fee_deposit
1891 );
1892
1893 let operator = Operators::<Test>::get(operator_id).unwrap();
1894 assert_eq!(operator.current_total_stake, operator_stake);
1895 assert_eq!(
1896 operator.deposits_in_epoch,
1897 nominator_stake + additional_nomination_stake
1898 );
1899 assert_eq!(
1900 operator.total_storage_fee_deposit,
1901 operator_storage_fee_deposit
1902 + nominator_storage_fee_deposit
1903 + additional_nomination_storage_fee_deposit
1904 );
1905
1906 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
1908
1909 let operator = Operators::<Test>::get(operator_id).unwrap();
1910 assert_eq!(
1911 operator.current_total_stake,
1912 operator_stake + nominator_stake + additional_nomination_stake
1913 );
1914
1915 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1916 assert_eq!(
1917 domain_staking_summary.current_total_stake,
1918 operator_stake + nominator_stake + additional_nomination_stake
1919 );
1920 });
1921 }
1922
1923 #[test]
1924 fn operator_deregistration() {
1925 let domain_id = DomainId::new(0);
1926 let operator_account = 1;
1927 let operator_stake = 200 * AI3;
1928 let operator_free_balance = 250 * AI3;
1929 let pair = OperatorPair::from_seed(&[0; 32]);
1930 let mut ext = new_test_ext();
1931 ext.execute_with(|| {
1932 let (operator_id, _) = register_operator(
1933 domain_id,
1934 operator_account,
1935 operator_free_balance,
1936 operator_stake,
1937 AI3,
1938 pair.public(),
1939 Default::default(),
1940 BTreeMap::new(),
1941 );
1942
1943 let res =
1944 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
1945 assert_ok!(res);
1946
1947 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1948 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
1949
1950 let operator = Operators::<Test>::get(operator_id).unwrap();
1951 assert_eq!(
1952 *operator.status::<Test>(operator_id),
1953 OperatorStatus::Deregistered(
1954 (
1955 domain_id,
1956 domain_stake_summary.current_epoch_index,
1957 5
1959 )
1960 .into()
1961 )
1962 );
1963
1964 let new_domain_id = DomainId::new(1);
1966 let domain_config = DomainConfig {
1967 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1968 runtime_id: 0,
1969 max_bundle_size: u32::MAX,
1970 max_bundle_weight: Weight::MAX,
1971 bundle_slot_probability: (0, 0),
1972 operator_allow_list: OperatorAllowList::Anyone,
1973 initial_balances: Default::default(),
1974 };
1975
1976 let domain_obj = DomainObject {
1977 owner_account_id: 0,
1978 created_at: 0,
1979 genesis_receipt_hash: Default::default(),
1980 domain_config,
1981 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1982 domain_instantiation_deposit: Default::default(),
1983 };
1984
1985 DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
1986 DomainStakingSummary::<Test>::insert(
1987 new_domain_id,
1988 StakingSummary {
1989 current_epoch_index: 0,
1990 current_total_stake: 0,
1991 current_operators: BTreeMap::new(),
1992 next_operators: BTreeSet::new(),
1993 current_epoch_rewards: BTreeMap::new(),
1994 },
1995 );
1996
1997 let nominator_account = 100;
1999 let nominator_stake = 100 * AI3;
2000 let res = Domains::nominate_operator(
2001 RuntimeOrigin::signed(nominator_account),
2002 operator_id,
2003 nominator_stake,
2004 );
2005 assert_err!(
2006 res,
2007 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2008 );
2009 });
2010 }
2011
2012 type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2013
2014 type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2018
2019 type StorageFundChange = (bool, u32);
2021
2022 pub(crate) type Share = <Test as Config>::Share;
2023
2024 struct WithdrawParams {
2025 minimum_nominator_stake: BalanceOf<Test>,
2027 nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2030 operator_reward: BalanceOf<Test>,
2032 nominator_id: NominatorId<Test>,
2034 withdraws: WithdrawWithResult,
2036 maybe_deposit: Option<BalanceOf<Test>>,
2038 expected_withdraw: ExpectedWithdrawAmount,
2041 expected_nominator_count_reduced_by: u32,
2043 storage_fund_change: StorageFundChange,
2045 }
2046
2047 fn withdraw_stake(params: WithdrawParams) {
2049 withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2050 }
2051
2052 fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2054 withdraw_stake_inner(params, true)
2055 }
2056
2057 fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2059 let WithdrawParams {
2060 minimum_nominator_stake,
2061 nominators,
2062 operator_reward,
2063 nominator_id,
2064 withdraws,
2065 maybe_deposit,
2066 expected_withdraw,
2067 expected_nominator_count_reduced_by,
2068 storage_fund_change,
2069 } = params;
2070 let domain_id = DomainId::new(0);
2071 let operator_account = 0;
2072 let pair = OperatorPair::from_seed(&[0; 32]);
2073 let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2074 + operator_reward
2075 + maybe_deposit.unwrap_or(0);
2076
2077 let mut nominators = BTreeMap::from_iter(
2078 nominators
2079 .into_iter()
2080 .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2081 .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2082 );
2083
2084 let mut ext = new_test_ext();
2085 ext.execute_with(|| {
2086 let (operator_free_balance, operator_stake) =
2087 nominators.remove(&operator_account).unwrap();
2088 let (operator_id, _) = register_operator(
2089 domain_id,
2090 operator_account,
2091 operator_free_balance,
2092 operator_stake,
2093 minimum_nominator_stake,
2094 pair.public(),
2095 Default::default(),
2096 nominators,
2097 );
2098
2099 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2100
2101 if !operator_reward.is_zero() {
2102 do_reward_operators::<Test>(
2103 domain_id,
2104 OperatorRewardSource::Dummy,
2105 vec![operator_id].into_iter(),
2106 operator_reward,
2107 )
2108 .unwrap();
2109 }
2110
2111 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2112
2113 if let Some(deposit_amount) = maybe_deposit {
2114 Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2115 let res = Domains::nominate_operator(
2116 RuntimeOrigin::signed(nominator_id),
2117 operator_id,
2118 deposit_amount,
2119 );
2120 assert_ok!(res);
2121 }
2122
2123 let operator = Operators::<Test>::get(operator_id).unwrap();
2124 let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2125 if is_storage_fund_increased {
2126 bundle_storage_fund::refund_storage_fee::<Test>(
2127 storage_fund_change_amount as u128 * AI3,
2128 BTreeMap::from_iter([(operator_id, 1)]),
2129 )
2130 .unwrap();
2131 assert_eq!(
2132 operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2133 bundle_storage_fund::total_balance::<Test>(operator_id)
2134 );
2135 total_balance += storage_fund_change_amount as u128 * AI3;
2136 } else {
2137 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2138 operator_id,
2139 storage_fund_change_amount,
2140 )
2141 .unwrap();
2142 assert_eq!(
2143 operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2144 bundle_storage_fund::total_balance::<Test>(operator_id)
2145 );
2146 total_balance -= storage_fund_change_amount as u128 * AI3;
2147 }
2148
2149 for (withdraw, expected_result) in withdraws {
2150 let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2151 let res = Domains::withdraw_stake(
2152 RuntimeOrigin::signed(nominator_id),
2153 operator_id,
2154 withdraw_share_amount,
2155 );
2156 if is_proptest {
2157 prop_assert_eq!(
2158 res,
2159 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2160 "unexpected withdraw_stake result",
2161 );
2162 } else {
2163 assert_eq!(
2164 res,
2165 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2166 "unexpected withdraw_stake result",
2167 );
2168 }
2169 }
2170
2171 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2172
2173 if let Some((withdraw, include_ed)) = expected_withdraw {
2174 let previous_usable_balance = Balances::usable_balance(nominator_id);
2175
2176 HeadDomainNumber::<Test>::set(
2178 domain_id,
2179 head_domain_number
2180 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2181 );
2182 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2183
2184 let expected_balance = if include_ed {
2185 total_balance += crate::tests::ExistentialDeposit::get();
2186 previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get()
2187 } else {
2188 previous_usable_balance + withdraw
2189 };
2190
2191 if is_proptest {
2192 prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2193 } else {
2194 assert_eq!(
2195 Balances::usable_balance(nominator_id),
2196 expected_balance,
2197 "usable balance is not equal to expected balance:\n{} !=\n{}",
2198 Balances::usable_balance(nominator_id),
2199 expected_balance,
2200 );
2201 }
2202
2203 assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2205 }
2206
2207 if expected_nominator_count_reduced_by > 0 {
2210 if is_proptest {
2211 prop_assert!(
2212 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2213 "deposit exists for nominator: {nominator_id}",
2214 );
2215 prop_assert!(
2216 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2217 "deposit on hold exists for nominator: {nominator_id}",
2218 );
2219 } else {
2220 assert!(
2221 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2222 "deposit exists for nominator: {nominator_id}",
2223 );
2224 assert!(
2225 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2226 "deposit on hold exists for nominator: {nominator_id}",
2227 );
2228 }
2229 }
2230
2231 let operator = Operators::<Test>::get(operator_id).unwrap();
2233 if is_proptest {
2234 prop_assert_approx!(
2235 Balances::usable_balance(nominator_id)
2236 + operator.current_total_stake
2237 + bundle_storage_fund::total_balance::<Test>(operator_id),
2238 total_balance,
2239 "\n{} + {} + {} =",
2240 Balances::usable_balance(nominator_id),
2241 operator.current_total_stake,
2242 bundle_storage_fund::total_balance::<Test>(operator_id),
2243 );
2244 } else {
2245 assert_eq!(
2246 Balances::usable_balance(nominator_id)
2247 + operator.current_total_stake
2248 + bundle_storage_fund::total_balance::<Test>(operator_id),
2249 total_balance,
2250 "initial total balance is not equal to final total balance:\n\
2251 {} + {} + {} =\n{} !=\n{}",
2252 Balances::usable_balance(nominator_id),
2253 operator.current_total_stake,
2254 bundle_storage_fund::total_balance::<Test>(operator_id),
2255 Balances::usable_balance(nominator_id)
2256 + operator.current_total_stake
2257 + bundle_storage_fund::total_balance::<Test>(operator_id),
2258 total_balance,
2259 );
2260 }
2261
2262 Ok(())
2263 })
2264 }
2265
2266 macro_rules! prop_assert_approx {
2272 ($actual:expr, $expected:expr $(,)?) => {
2273 prop_assert_approx!($actual, $expected, "");
2274 };
2275
2276 ($actual:expr, $expected:expr, $fmt:expr) => {
2277 prop_assert_approx!($actual, $expected, $fmt,);
2278 };
2279
2280 ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2281 let actual = $actual;
2282 let expected = $expected;
2283 let extra = format!($fmt, $($args)*);
2284 prop_test::proptest::prop_assert!(
2285 actual <= expected,
2286 "extra minting: actual amount is greater than expected amount:{}{}\
2287 \n{} >\
2288 \n{}",
2289 if extra.is_empty() { "" } else { "\n" },
2290 extra,
2291 actual,
2292 expected,
2293 );
2294 let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2295 .mul_floor(expected)
2296 .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2297 prop_test::proptest::prop_assert!(
2298 actual >= expected_rounded_down,
2299 "excess rounding losses: actual amount is less than expected amount \
2300 rounded down:{}{}\
2301 \n{} <\
2302 \n{} (from\
2303 \n{})",
2304 if extra.is_empty() { "" } else { "\n" },
2305 extra,
2306 actual,
2307 expected_rounded_down,
2308 expected,
2309 );
2310 }};
2311 }
2312
2313 pub(crate) use prop_assert_approx;
2315
2316 pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2320 Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2321
2322 pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2325
2326 pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2334
2335 pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2338
2339 pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2341 <Test as Config>::MinNominatorStake::get() + 1;
2342
2343 pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2345 MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2346
2347 pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2349 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2350
2351 pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2354 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2355
2356 pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2358
2359 pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2361 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2362
2363 #[test]
2369 fn prop_withdraw_excess_operator_stake() {
2370 prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2371 let mut excess_stake =
2372 operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2373
2374 excess_stake = Perquintill::from_parts(1)
2376 .left_from_one()
2377 .mul_ceil(excess_stake);
2378 excess_stake -= 1;
2379
2380 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2381
2382 let expected_withdraw = (excess_stake, false);
2383
2384 withdraw_stake_prop(WithdrawParams {
2385 minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2386 nominators: vec![(0, operator_stake)],
2387 operator_reward: 0,
2388 nominator_id: 0,
2389 withdraws: vec![(excess_stake, Ok(()))],
2390 maybe_deposit: None,
2391 expected_withdraw: Some(expected_withdraw),
2392 expected_nominator_count_reduced_by: 0,
2393 storage_fund_change: (true, 0),
2394 })
2395 });
2396 }
2397
2398 #[test]
2401 fn prop_withdraw_excess_operator_stake_with_nominator() {
2402 prop_test!(
2403 &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2404 |(operator_stake, nominator_stake)| {
2405 prop_assert!(
2407 [operator_stake, nominator_stake]
2408 .into_iter()
2409 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2410 .is_some()
2411 );
2412
2413 let expected_withdraw = (nominator_stake, true);
2414
2415 let excess_stake = expected_withdraw
2416 .0
2417 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2418
2419 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2420
2421 withdraw_stake_prop(WithdrawParams {
2422 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2423 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2424 operator_reward: 0,
2425 nominator_id: 1,
2426 withdraws: vec![(excess_stake, Ok(()))],
2427 maybe_deposit: None,
2428 expected_withdraw: Some(expected_withdraw),
2429 expected_nominator_count_reduced_by: 1,
2430 storage_fund_change: (true, 0),
2431 })
2432 }
2433 );
2434 }
2435
2436 #[test]
2439 fn prop_withdraw_excess_operator_stake_with_deposit() {
2440 prop_test!(
2441 &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2442 |(mut operator_stake, maybe_deposit)| {
2443 operator_stake = operator_stake.saturating_add(maybe_deposit);
2444
2445 prop_assert!(
2446 operator_stake.saturating_sub(maybe_deposit)
2447 >= <Test as Config>::MinOperatorStake::get(),
2448 "would cause MinimumOperatorStake error"
2449 );
2450
2451 prop_assert!(
2453 [operator_stake, maybe_deposit]
2454 .into_iter()
2455 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2456 .is_some()
2457 );
2458
2459 let expected_withdraw = (
2460 STORAGE_FEE_RESERVE
2462 .left_from_one()
2463 .mul_ceil(operator_stake)
2464 .saturating_sub(maybe_deposit),
2465 true,
2466 );
2467
2468 let excess_stake = expected_withdraw
2469 .0
2470 .saturating_sub(<Test as Config>::MinOperatorStake::get());
2471
2472 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2473
2474 let maybe_deposit = if maybe_deposit == 0 {
2476 None
2477 } else {
2478 Some(maybe_deposit)
2479 };
2480
2481 withdraw_stake_prop(WithdrawParams {
2482 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2483 nominators: vec![(0, operator_stake)],
2484 operator_reward: 0,
2485 nominator_id: 0,
2486 withdraws: vec![(excess_stake, Ok(()))],
2487 maybe_deposit,
2488 expected_withdraw: Some(expected_withdraw),
2489 expected_nominator_count_reduced_by: 0,
2490 storage_fund_change: (true, 0),
2491 })
2492 }
2493 );
2494 }
2495
2496 #[test]
2500 fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
2501 prop_test!(
2502 &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2503 |(mut nominator_stake, maybe_deposit)| {
2504 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2505
2506 prop_assert!(
2507 nominator_stake.saturating_sub(maybe_deposit)
2508 >= <Test as Config>::MinNominatorStake::get(),
2509 "would cause MinimumNominatorStake error"
2510 );
2511
2512 let operator_stake = <Test as Config>::MinOperatorStake::get();
2514
2515 prop_assert!(
2517 [operator_stake, nominator_stake, maybe_deposit]
2518 .into_iter()
2519 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2520 .is_some()
2521 );
2522
2523 let expected_withdraw = (
2524 STORAGE_FEE_RESERVE
2525 .left_from_one()
2526 .mul_ceil(nominator_stake)
2527 .saturating_sub(maybe_deposit),
2528 true,
2529 );
2530
2531 let excess_stake = expected_withdraw
2532 .0
2533 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2534
2535 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2536
2537 let maybe_deposit = if maybe_deposit == 0 {
2539 None
2540 } else {
2541 Some(maybe_deposit)
2542 };
2543
2544 withdraw_stake_prop(WithdrawParams {
2545 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2546 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2547 operator_reward: 0,
2548 nominator_id: 1,
2549 withdraws: vec![(excess_stake, Ok(()))],
2550 maybe_deposit,
2551 expected_withdraw: Some(expected_withdraw),
2552 expected_nominator_count_reduced_by: 0,
2553 storage_fund_change: (true, 0),
2554 })
2555 }
2556 );
2557 }
2558
2559 #[test]
2562 fn prop_withdraw_excess_nominator_stake_with_deposit() {
2563 prop_test!(
2564 &(
2565 PROP_OPERATOR_STAKE_RANGE,
2566 PROP_NOMINATOR_STAKE_RANGE,
2567 PROP_DEPOSIT_RANGE,
2568 ),
2569 |(operator_stake, mut nominator_stake, maybe_deposit)| {
2570 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2571
2572 prop_assert!(
2573 nominator_stake.saturating_sub(maybe_deposit)
2574 >= <Test as Config>::MinNominatorStake::get(),
2575 "would cause MinimumNominatorStake error"
2576 );
2577
2578 prop_assert!(
2580 [operator_stake, nominator_stake, maybe_deposit]
2581 .into_iter()
2582 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2583 .is_some()
2584 );
2585
2586 let expected_withdraw = (
2588 STORAGE_FEE_RESERVE
2589 .left_from_one()
2590 .mul_ceil(nominator_stake)
2591 .saturating_sub(maybe_deposit),
2592 true,
2593 );
2594
2595 let excess_stake = expected_withdraw
2596 .0
2597 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2598
2599 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2600
2601 let maybe_deposit = if maybe_deposit == 0 {
2603 None
2604 } else {
2605 Some(maybe_deposit)
2606 };
2607
2608 withdraw_stake_prop(WithdrawParams {
2609 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2610 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2611 operator_reward: 0,
2612 nominator_id: 1,
2613 withdraws: vec![(excess_stake, Ok(()))],
2614 maybe_deposit,
2615 expected_withdraw: Some(expected_withdraw),
2616 expected_nominator_count_reduced_by: 0,
2617 storage_fund_change: (true, 0),
2618 })
2619 }
2620 );
2621 }
2622
2623 #[test]
2626 fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
2627 prop_test!(
2628 &(
2629 PROP_OPERATOR_STAKE_RANGE,
2630 PROP_NOMINATOR_STAKE_RANGE,
2631 PROP_REWARD_RANGE,
2632 ),
2633 |(operator_stake, nominator_stake, operator_reward)| {
2634 prop_assert!(
2636 [operator_stake, nominator_stake, operator_reward]
2637 .into_iter()
2638 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2639 .is_some()
2640 );
2641
2642 let total_stake = operator_stake.saturating_add(nominator_stake);
2644 let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
2645 .mul_floor(operator_reward);
2646
2647 let excess_stake =
2650 operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
2651
2652 let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
2653
2654 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2655
2656 withdraw_stake_prop(WithdrawParams {
2657 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2658 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2659 operator_reward,
2660 nominator_id: 0,
2661 withdraws: vec![(excess_stake, Ok(()))],
2662 maybe_deposit: None,
2663 expected_withdraw: Some(expected_withdraw),
2664 expected_nominator_count_reduced_by: 0,
2665 storage_fund_change: (true, 0),
2666 })
2667 }
2668 );
2669 }
2670
2671 #[test]
2674 fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
2675 prop_test!(
2676 &(
2677 PROP_OPERATOR_STAKE_RANGE,
2678 PROP_DEPOSIT_RANGE,
2679 PROP_REWARD_RANGE,
2680 ),
2681 |(mut operator_stake, maybe_deposit, operator_reward)| {
2682 operator_stake = operator_stake.saturating_add(maybe_deposit);
2683
2684 prop_assert!(
2685 operator_stake.saturating_sub(maybe_deposit)
2686 >= <Test as Config>::MinOperatorStake::get(),
2687 "would cause MinimumOperatorStake error"
2688 );
2689
2690 prop_assert!(
2692 [operator_stake, maybe_deposit, operator_reward]
2693 .into_iter()
2694 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2695 .is_some()
2696 );
2697
2698 let reserve = 2 * <Test as Config>::MinOperatorStake::get();
2701 let excess_stake = operator_stake.saturating_sub(reserve);
2702
2703 let expected_withdraw = (
2705 operator_stake
2706 .saturating_add(operator_reward)
2707 .saturating_sub(reserve),
2708 true,
2709 );
2710
2711 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2712
2713 let maybe_deposit = if maybe_deposit == 0 {
2715 None
2716 } else {
2717 Some(maybe_deposit)
2718 };
2719
2720 withdraw_stake_prop(WithdrawParams {
2721 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2722 nominators: vec![(0, operator_stake)],
2723 operator_reward,
2724 nominator_id: 0,
2725 withdraws: vec![(excess_stake, Ok(()))],
2726 maybe_deposit,
2727 expected_withdraw: Some(expected_withdraw),
2728 expected_nominator_count_reduced_by: 0,
2729 storage_fund_change: (true, 0),
2730 })
2731 }
2732 );
2733 }
2734
2735 #[test]
2736 fn withdraw_stake_operator_all() {
2737 withdraw_stake(WithdrawParams {
2738 minimum_nominator_stake: 10 * AI3,
2739 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2740 operator_reward: 20 * AI3,
2741 nominator_id: 0,
2742 withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
2743 maybe_deposit: None,
2744 expected_withdraw: None,
2745 expected_nominator_count_reduced_by: 0,
2746 storage_fund_change: (true, 0),
2747 })
2748 }
2749
2750 #[test]
2751 fn withdraw_stake_operator_below_minimum() {
2752 withdraw_stake(WithdrawParams {
2753 minimum_nominator_stake: 10 * AI3,
2754 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2755 operator_reward: 20 * AI3,
2756 nominator_id: 0,
2757 withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
2758 maybe_deposit: None,
2759 expected_withdraw: None,
2760 expected_nominator_count_reduced_by: 0,
2761 storage_fund_change: (true, 0),
2762 })
2763 }
2764
2765 #[test]
2766 fn withdraw_stake_operator_below_minimum_no_rewards() {
2767 withdraw_stake(WithdrawParams {
2768 minimum_nominator_stake: 10 * AI3,
2769 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2770 operator_reward: Zero::zero(),
2771 nominator_id: 0,
2772 withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
2773 maybe_deposit: None,
2774 expected_withdraw: None,
2775 expected_nominator_count_reduced_by: 0,
2776 storage_fund_change: (true, 0),
2777 })
2778 }
2779
2780 #[test]
2781 fn withdraw_stake_operator_above_minimum() {
2782 withdraw_stake(WithdrawParams {
2783 minimum_nominator_stake: 10 * AI3,
2784 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2785 operator_reward: 20 * AI3,
2786 nominator_id: 0,
2787 withdraws: vec![(58 * AI3, Ok(()))],
2788 maybe_deposit: None,
2791 expected_withdraw: Some((63523809523809523770, false)),
2792 expected_nominator_count_reduced_by: 0,
2793 storage_fund_change: (true, 0),
2794 })
2795 }
2796
2797 #[test]
2798 fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
2799 withdraw_stake(WithdrawParams {
2800 minimum_nominator_stake: 10 * AI3,
2801 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2802 operator_reward: 20 * AI3,
2803 nominator_id: 0,
2804 withdraws: vec![
2805 (58 * AI3, Ok(())),
2806 (5 * AI3, Err(StakingError::MinimumOperatorStake)),
2807 ],
2808 maybe_deposit: None,
2809 expected_withdraw: Some((63523809523809523770, false)),
2810 expected_nominator_count_reduced_by: 0,
2811 storage_fund_change: (true, 0),
2812 })
2813 }
2814
2815 #[test]
2816 fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
2817 withdraw_stake(WithdrawParams {
2818 minimum_nominator_stake: 10 * AI3,
2819 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2820 operator_reward: 20 * AI3,
2821 nominator_id: 0,
2822 withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
2823 maybe_deposit: None,
2824 expected_withdraw: Some((63523809523809523769, false)),
2825 expected_nominator_count_reduced_by: 0,
2826 storage_fund_change: (true, 0),
2827 })
2828 }
2829
2830 #[test]
2831 fn withdraw_stake_operator_above_minimum_no_rewards() {
2832 withdraw_stake(WithdrawParams {
2833 minimum_nominator_stake: 10 * AI3,
2834 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2835 operator_reward: Zero::zero(),
2836 nominator_id: 0,
2837 withdraws: vec![(49 * AI3, Ok(()))],
2838 maybe_deposit: None,
2839 expected_withdraw: Some((48999999999999999980, false)),
2840 expected_nominator_count_reduced_by: 0,
2841 storage_fund_change: (true, 0),
2842 })
2843 }
2844
2845 #[test]
2846 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
2847 withdraw_stake(WithdrawParams {
2848 minimum_nominator_stake: 10 * AI3,
2849 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2850 operator_reward: Zero::zero(),
2851 nominator_id: 0,
2852 withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
2853 maybe_deposit: None,
2854 expected_withdraw: Some((48999999999999999981, false)),
2855 expected_nominator_count_reduced_by: 0,
2856 storage_fund_change: (true, 0),
2857 })
2858 }
2859
2860 #[test]
2861 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
2862 withdraw_stake(WithdrawParams {
2863 minimum_nominator_stake: 10 * AI3,
2864 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2865 operator_reward: Zero::zero(),
2866 nominator_id: 0,
2867 withdraws: vec![
2868 (29 * AI3, Ok(())),
2869 (20 * AI3, Ok(())),
2870 (20 * AI3, Err(StakingError::MinimumOperatorStake)),
2871 ],
2872 maybe_deposit: None,
2873 expected_withdraw: Some((48999999999999999981, false)),
2874 expected_nominator_count_reduced_by: 0,
2875 storage_fund_change: (true, 0),
2876 })
2877 }
2878
2879 #[test]
2880 fn withdraw_stake_nominator_below_minimum_with_rewards() {
2881 withdraw_stake(WithdrawParams {
2882 minimum_nominator_stake: 10 * AI3,
2883 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2884 operator_reward: 20 * AI3,
2885 nominator_id: 1,
2886 withdraws: vec![(45 * AI3, Ok(()))],
2887 maybe_deposit: None,
2891 expected_withdraw: Some((54761904761904761888, true)),
2892 expected_nominator_count_reduced_by: 1,
2893 storage_fund_change: (true, 0),
2894 })
2895 }
2896
2897 #[test]
2898 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
2899 withdraw_stake(WithdrawParams {
2900 minimum_nominator_stake: 10 * AI3,
2901 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2902 operator_reward: 20 * AI3,
2903 nominator_id: 1,
2904 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2905 maybe_deposit: None,
2909 expected_withdraw: Some((54761904761904761888, true)),
2910 expected_nominator_count_reduced_by: 1,
2911 storage_fund_change: (true, 0),
2912 })
2913 }
2914
2915 #[test]
2916 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
2917 withdraw_stake(WithdrawParams {
2918 minimum_nominator_stake: 10 * AI3,
2919 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2920 operator_reward: 20 * AI3,
2921 nominator_id: 1,
2922 withdraws: vec![
2923 (25 * AI3, Ok(())),
2924 (20 * AI3, Ok(())),
2925 (20 * AI3, Err(StakingError::InsufficientShares)),
2926 ],
2927 maybe_deposit: None,
2931 expected_withdraw: Some((54761904761904761888, true)),
2932 expected_nominator_count_reduced_by: 1,
2933 storage_fund_change: (true, 0),
2934 })
2935 }
2936
2937 #[test]
2938 fn withdraw_stake_nominator_below_minimum_no_reward() {
2939 withdraw_stake(WithdrawParams {
2940 minimum_nominator_stake: 10 * AI3,
2941 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2942 operator_reward: Zero::zero(),
2943 nominator_id: 1,
2944 withdraws: vec![(45 * AI3, Ok(()))],
2945 maybe_deposit: None,
2946 expected_withdraw: Some((50 * AI3, true)),
2947 expected_nominator_count_reduced_by: 1,
2948 storage_fund_change: (true, 0),
2949 })
2950 }
2951
2952 #[test]
2953 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
2954 withdraw_stake(WithdrawParams {
2955 minimum_nominator_stake: 10 * AI3,
2956 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2957 operator_reward: Zero::zero(),
2958 nominator_id: 1,
2959 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2960 maybe_deposit: None,
2961 expected_withdraw: Some((50 * AI3, true)),
2962 expected_nominator_count_reduced_by: 1,
2963 storage_fund_change: (true, 0),
2964 })
2965 }
2966
2967 #[test]
2968 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
2969 withdraw_stake(WithdrawParams {
2970 minimum_nominator_stake: 10 * AI3,
2971 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2972 operator_reward: Zero::zero(),
2973 nominator_id: 1,
2974 withdraws: vec![
2975 (25 * AI3, Ok(())),
2976 (20 * AI3, Ok(())),
2977 (20 * AI3, Err(StakingError::InsufficientShares)),
2978 ],
2979 maybe_deposit: None,
2980 expected_withdraw: Some((50 * AI3, true)),
2981 expected_nominator_count_reduced_by: 1,
2982 storage_fund_change: (true, 0),
2983 })
2984 }
2985
2986 #[test]
2987 fn withdraw_stake_nominator_above_minimum() {
2988 withdraw_stake(WithdrawParams {
2989 minimum_nominator_stake: 10 * AI3,
2990 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2991 operator_reward: 20 * AI3,
2992 nominator_id: 1,
2993 withdraws: vec![(40 * AI3, Ok(()))],
2994 maybe_deposit: None,
2995 expected_withdraw: Some((43809523809523809511, false)),
2996 expected_nominator_count_reduced_by: 0,
2997 storage_fund_change: (true, 0),
2998 })
2999 }
3000
3001 #[test]
3002 fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
3003 withdraw_stake(WithdrawParams {
3004 minimum_nominator_stake: 10 * AI3,
3005 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3006 operator_reward: 20 * AI3,
3007 nominator_id: 1,
3008 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3009 maybe_deposit: None,
3010 expected_withdraw: Some((43809523809523809510, false)),
3011 expected_nominator_count_reduced_by: 0,
3012 storage_fund_change: (true, 0),
3013 })
3014 }
3015
3016 #[test]
3017 fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3018 withdraw_stake(WithdrawParams {
3019 minimum_nominator_stake: 10 * AI3,
3020 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3021 operator_reward: 20 * AI3,
3022 nominator_id: 1,
3023 withdraws: vec![
3024 (35 * AI3, Ok(())),
3025 (5 * AI3, Ok(())),
3026 (15 * AI3, Err(StakingError::InsufficientShares)),
3027 ],
3028 maybe_deposit: None,
3029 expected_withdraw: Some((43809523809523809510, false)),
3030 expected_nominator_count_reduced_by: 0,
3031 storage_fund_change: (true, 0),
3032 })
3033 }
3034
3035 #[test]
3036 fn withdraw_stake_nominator_above_minimum_no_rewards() {
3037 withdraw_stake(WithdrawParams {
3038 minimum_nominator_stake: 10 * AI3,
3039 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3040 operator_reward: Zero::zero(),
3041 nominator_id: 1,
3042 withdraws: vec![(39 * AI3, Ok(()))],
3043 maybe_deposit: None,
3044 expected_withdraw: Some((39 * AI3, false)),
3045 expected_nominator_count_reduced_by: 0,
3046 storage_fund_change: (true, 0),
3047 })
3048 }
3049
3050 #[test]
3051 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3052 withdraw_stake(WithdrawParams {
3053 minimum_nominator_stake: 10 * AI3,
3054 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3055 operator_reward: Zero::zero(),
3056 nominator_id: 1,
3057 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3058 maybe_deposit: None,
3059 expected_withdraw: Some((39999999899999999998, false)),
3060 expected_nominator_count_reduced_by: 0,
3061 storage_fund_change: (true, 0),
3062 })
3063 }
3064
3065 #[test]
3066 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3067 withdraw_stake(WithdrawParams {
3068 minimum_nominator_stake: 10 * AI3,
3069 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3070 operator_reward: Zero::zero(),
3071 nominator_id: 1,
3072 withdraws: vec![
3073 (35 * AI3, Ok(())),
3074 (5 * AI3 - 100000000000, Ok(())),
3075 (15 * AI3, Err(StakingError::InsufficientShares)),
3076 ],
3077 maybe_deposit: None,
3078 expected_withdraw: Some((39999999899999999998, false)),
3079 expected_nominator_count_reduced_by: 0,
3080 storage_fund_change: (true, 0),
3081 })
3082 }
3083
3084 #[test]
3085 fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3086 withdraw_stake(WithdrawParams {
3087 minimum_nominator_stake: 10 * AI3,
3088 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3089 operator_reward: Zero::zero(),
3090 nominator_id: 1,
3091 withdraws: vec![
3092 (35 * AI3, Ok(())),
3093 (5 * AI3 - 100000000000, Ok(())),
3094 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3095 ],
3096 maybe_deposit: Some(2 * AI3),
3097 expected_withdraw: Some((39999999899999999998, false)),
3098 expected_nominator_count_reduced_by: 0,
3099 storage_fund_change: (true, 0),
3100 })
3101 }
3102
3103 #[test]
3104 fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3105 withdraw_stake(WithdrawParams {
3106 minimum_nominator_stake: 10 * AI3,
3107 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3108 operator_reward: 20 * AI3,
3109 nominator_id: 1,
3110 withdraws: vec![
3111 (35 * AI3, Ok(())),
3112 (5 * AI3, Ok(())),
3113 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3114 ],
3115 maybe_deposit: Some(2 * AI3),
3119 expected_withdraw: Some((43809523809523809510, false)),
3120 expected_nominator_count_reduced_by: 0,
3121 storage_fund_change: (true, 0),
3122 })
3123 }
3124
3125 #[test]
3126 fn withdraw_stake_nominator_zero_amount() {
3127 withdraw_stake(WithdrawParams {
3128 minimum_nominator_stake: 10 * AI3,
3129 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3130 operator_reward: Zero::zero(),
3131 nominator_id: 1,
3132 withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3133 maybe_deposit: None,
3134 expected_withdraw: None,
3135 expected_nominator_count_reduced_by: 0,
3136 storage_fund_change: (true, 0),
3137 })
3138 }
3139
3140 #[test]
3141 fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3142 withdraw_stake(WithdrawParams {
3143 minimum_nominator_stake: 10 * AI3,
3144 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3145 operator_reward: Zero::zero(),
3146 nominator_id: 1,
3147 withdraws: vec![(50 * AI3, Ok(()))],
3148 maybe_deposit: None,
3149 storage_fund_change: (true, 21),
3152 expected_withdraw: Some((54999999999999999985, true)),
3153 expected_nominator_count_reduced_by: 1,
3154 })
3155 }
3156
3157 #[test]
3158 fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3159 withdraw_stake(WithdrawParams {
3160 minimum_nominator_stake: 10 * AI3,
3161 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3162 operator_reward: Zero::zero(),
3163 nominator_id: 1,
3164 withdraws: vec![(50 * AI3, Ok(()))],
3165 maybe_deposit: None,
3166 storage_fund_change: (false, 21),
3169 expected_withdraw: Some((44999999999999999995, true)),
3170 expected_nominator_count_reduced_by: 1,
3171 })
3172 }
3173
3174 #[test]
3175 fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3176 withdraw_stake(WithdrawParams {
3177 minimum_nominator_stake: 10 * AI3,
3178 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3179 operator_reward: Zero::zero(),
3180 nominator_id: 1,
3181 withdraws: vec![(50 * AI3, Ok(()))],
3182 maybe_deposit: None,
3183 storage_fund_change: (false, 42),
3186 expected_withdraw: Some((40 * AI3, true)),
3187 expected_nominator_count_reduced_by: 1,
3188 })
3189 }
3190
3191 #[test]
3192 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3193 withdraw_stake(WithdrawParams {
3194 minimum_nominator_stake: 10 * AI3,
3195 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3196 operator_reward: Zero::zero(),
3197 nominator_id: 1,
3198 withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3199 maybe_deposit: None,
3200 storage_fund_change: (true, 21),
3204 expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3205 expected_nominator_count_reduced_by: 0,
3206 })
3207 }
3208
3209 #[test]
3210 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3211 withdraw_stake(WithdrawParams {
3212 minimum_nominator_stake: 10 * AI3,
3213 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3214 operator_reward: Zero::zero(),
3215 nominator_id: 1,
3216 withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3217 maybe_deposit: None,
3218 storage_fund_change: (false, 21),
3222 expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3223 expected_nominator_count_reduced_by: 0,
3224 })
3225 }
3226
3227 #[test]
3228 fn unlock_multiple_withdrawals() {
3229 let domain_id = DomainId::new(0);
3230 let operator_account = 1;
3231 let operator_free_balance = 250 * AI3;
3232 let operator_stake = 200 * AI3;
3233 let pair = OperatorPair::from_seed(&[0; 32]);
3234 let nominator_account = 2;
3235 let nominator_free_balance = 150 * AI3;
3236 let nominator_stake = 100 * AI3;
3237
3238 let nominators = vec![
3239 (operator_account, (operator_free_balance, operator_stake)),
3240 (nominator_account, (nominator_free_balance, nominator_stake)),
3241 ];
3242
3243 let total_deposit = 300 * AI3;
3244 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3245 let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3246
3247 let mut ext = new_test_ext();
3248 ext.execute_with(|| {
3249 let (operator_id, _) = register_operator(
3250 domain_id,
3251 operator_account,
3252 operator_free_balance,
3253 operator_stake,
3254 10 * AI3,
3255 pair.public(),
3256 Default::default(),
3257 BTreeMap::from_iter(nominators),
3258 );
3259
3260 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3261 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3262 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3263
3264 let operator = Operators::<Test>::get(operator_id).unwrap();
3265 assert_eq!(operator.current_total_stake, init_total_stake);
3266 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3267 assert_eq!(
3268 operator.total_storage_fee_deposit,
3269 bundle_storage_fund::total_balance::<Test>(operator_id)
3270 );
3271
3272 let shares_per_withdraw = init_total_stake / 100;
3274 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3275
3276 for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3278 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3279 .unwrap();
3280 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3281 }
3282 HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3284
3285 for _ in 0..5 {
3288 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3289 .unwrap();
3290 }
3291 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3292
3293 assert_err!(
3295 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3296 StakingError::TooManyWithdrawals
3297 );
3298 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3299 Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3300 let withdrawal = maybe_withdrawal.as_mut().unwrap();
3301 do_convert_previous_epoch_withdrawal::<Test>(
3302 operator_id,
3303 withdrawal,
3304 domain_stake_summary.current_epoch_index,
3305 )
3306 .unwrap();
3307 assert_eq!(
3308 withdrawal.withdrawals.len() as u32,
3309 <Test as crate::Config>::WithdrawalLimit::get()
3310 );
3311 Ok::<(), StakingError>(())
3312 })
3313 .unwrap();
3314
3315 HeadDomainNumber::<Test>::set(
3317 domain_id,
3318 head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3319 );
3320 let total_balance = Balances::usable_balance(nominator_account);
3321 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3322 assert_eq!(
3323 Balances::usable_balance(nominator_account) + 74, total_balance
3325 + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3326 / 100
3327 );
3328 let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3329 assert_eq!(withdrawal.withdrawals.len(), 1);
3330
3331 HeadDomainNumber::<Test>::set(
3333 domain_id,
3334 head_domain_number
3335 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3336 + 1,
3337 );
3338 let total_balance = Balances::usable_balance(nominator_account);
3339 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3340 assert_eq!(
3341 Balances::usable_balance(nominator_account) - 2, total_balance + 5 * total_deposit / 100
3343 );
3344 assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3345 });
3346 }
3347
3348 #[test]
3349 fn slash_operator() {
3350 let domain_id = DomainId::new(0);
3351 let operator_account = 1;
3352 let operator_free_balance = 250 * AI3;
3353 let operator_stake = 200 * AI3;
3354 let operator_extra_deposit = 40 * AI3;
3355 let pair = OperatorPair::from_seed(&[0; 32]);
3356 let nominator_account = 2;
3357 let nominator_free_balance = 150 * AI3;
3358 let nominator_stake = 100 * AI3;
3359 let nominator_extra_deposit = 40 * AI3;
3360
3361 let nominators = vec![
3362 (operator_account, (operator_free_balance, operator_stake)),
3363 (nominator_account, (nominator_free_balance, nominator_stake)),
3364 ];
3365
3366 let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3367
3368 let deposits = vec![
3369 (operator_account, operator_extra_deposit),
3370 (nominator_account, nominator_extra_deposit),
3371 ];
3372
3373 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3374 let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3375
3376 let mut ext = new_test_ext();
3377 ext.execute_with(|| {
3378 let (operator_id, _) = register_operator(
3379 domain_id,
3380 operator_account,
3381 operator_free_balance,
3382 operator_stake,
3383 10 * AI3,
3384 pair.public(),
3385 Default::default(),
3386 BTreeMap::from_iter(nominators),
3387 );
3388
3389 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3390 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3391 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3392
3393 let operator = Operators::<Test>::get(operator_id).unwrap();
3394 assert_eq!(operator.current_total_stake, init_total_stake);
3395 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3396 assert_eq!(
3397 operator.total_storage_fee_deposit,
3398 bundle_storage_fund::total_balance::<Test>(operator_id)
3399 );
3400
3401 for unlock in &unlocking {
3402 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3403 }
3404
3405 do_reward_operators::<Test>(
3406 domain_id,
3407 OperatorRewardSource::Dummy,
3408 vec![operator_id].into_iter(),
3409 20 * AI3,
3410 )
3411 .unwrap();
3412 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3413
3414 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3416 for id in [operator_account, nominator_account] {
3417 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3418 do_convert_previous_epoch_withdrawal::<Test>(
3419 operator_id,
3420 maybe_withdrawal.as_mut().unwrap(),
3421 domain_stake_summary.current_epoch_index,
3422 )
3423 })
3424 .unwrap();
3425 }
3426
3427 let operator = Operators::<Test>::get(operator_id).unwrap();
3430 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3431 let operator_withdrawal =
3432 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3433 let nominator_withdrawal =
3434 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3435
3436 let total_deposit =
3437 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3438 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3439 + nominator_withdrawal.total_withdrawal_amount;
3440 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3441 .storage_fee_refund
3442 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3443 assert_eq!(293333333333333333336, total_deposit,);
3444 assert_eq!(21666666666666666664, total_stake_withdrawal);
3445 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3446 assert_eq!(
3447 320 * AI3,
3448 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3449 );
3450 assert_eq!(
3451 operator.total_storage_fee_deposit,
3452 bundle_storage_fund::total_balance::<Test>(operator_id)
3453 );
3454
3455 for deposit in deposits {
3456 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3457 }
3458
3459 do_mark_operators_as_slashed::<Test>(
3460 vec![operator_id],
3461 SlashedReason::InvalidBundle(1),
3462 )
3463 .unwrap();
3464
3465 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3466 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3467
3468 let operator = Operators::<Test>::get(operator_id).unwrap();
3469 assert_eq!(
3470 *operator.status::<Test>(operator_id),
3471 OperatorStatus::Slashed
3472 );
3473
3474 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3475 assert!(pending_slashes.contains(&operator_id));
3476
3477 assert_eq!(
3478 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3479 0
3480 );
3481
3482 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3483 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3484 assert_eq!(Operators::<Test>::get(operator_id), None);
3485 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3486
3487 assert_eq!(
3488 Balances::total_balance(&operator_account),
3489 operator_free_balance - operator_stake
3490 );
3491 assert_eq!(
3492 Balances::total_balance(&nominator_account),
3493 nominator_free_balance - nominator_stake
3494 );
3495
3496 assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3);
3497 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3498 });
3499 }
3500
3501 #[test]
3502 fn slash_operator_with_more_than_max_nominators_to_slash() {
3503 let domain_id = DomainId::new(0);
3504 let operator_account = 1;
3505 let operator_free_balance = 250 * AI3;
3506 let operator_stake = 200 * AI3;
3507 let operator_extra_deposit = 40 * AI3;
3508 let operator_extra_withdraw = 5 * AI3;
3509 let pair = OperatorPair::from_seed(&[0; 32]);
3510
3511 let nominator_accounts: Vec<crate::tests::AccountId> = (2..22).collect();
3512 let nominator_free_balance = 150 * AI3;
3513 let nominator_stake = 100 * AI3;
3514 let nominator_extra_deposit = 40 * AI3;
3515 let nominator_extra_withdraw = 5 * AI3;
3516
3517 let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
3518 for nominator_account in nominator_accounts.clone() {
3519 nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
3520 }
3521
3522 let last_nominator_account = nominator_accounts.last().cloned().unwrap();
3523 let unlocking = vec![
3524 (operator_account, 10 * AI3),
3525 (last_nominator_account, 10 * AI3),
3526 ];
3527
3528 let deposits = vec![
3529 (operator_account, operator_extra_deposit),
3530 (last_nominator_account, nominator_extra_deposit),
3531 ];
3532 let withdrawals = vec![
3533 (operator_account, operator_extra_withdraw),
3534 (last_nominator_account, nominator_extra_withdraw),
3535 ];
3536
3537 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
3538 * (200 + (100 * nominator_accounts.len() as u128))
3539 * AI3;
3540 let init_total_storage_fund =
3541 STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
3542
3543 let mut ext = new_test_ext();
3544 ext.execute_with(|| {
3545 let (operator_id, _) = register_operator(
3546 domain_id,
3547 operator_account,
3548 operator_free_balance,
3549 operator_stake,
3550 10 * AI3,
3551 pair.public(),
3552 Default::default(),
3553 BTreeMap::from_iter(nominators),
3554 );
3555
3556 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3557 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3558 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3559
3560 let operator = Operators::<Test>::get(operator_id).unwrap();
3561 assert_eq!(operator.current_total_stake, init_total_stake);
3562 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3563 assert_eq!(
3564 operator.total_storage_fee_deposit,
3565 bundle_storage_fund::total_balance::<Test>(operator_id)
3566 );
3567
3568 for unlock in &unlocking {
3569 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3570 }
3571
3572 do_reward_operators::<Test>(
3573 domain_id,
3574 OperatorRewardSource::Dummy,
3575 vec![operator_id].into_iter(),
3576 20 * AI3,
3577 )
3578 .unwrap();
3579 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3580
3581 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3583 for id in [operator_account, last_nominator_account] {
3584 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3585 do_convert_previous_epoch_withdrawal::<Test>(
3586 operator_id,
3587 maybe_withdrawal.as_mut().unwrap(),
3588 domain_stake_summary.current_epoch_index,
3589 )
3590 })
3591 .unwrap();
3592 }
3593
3594 let operator = Operators::<Test>::get(operator_id).unwrap();
3597 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3598 let operator_withdrawal =
3599 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3600 let nominator_withdrawal =
3601 Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
3602
3603 let total_deposit =
3604 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3605 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3606 + nominator_withdrawal.total_withdrawal_amount;
3607 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3608 .storage_fee_refund
3609 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3610 assert_eq!(2194772727272727272734, total_deposit,);
3611 assert_eq!(20227272727272727266, total_stake_withdrawal);
3612 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3613 assert_eq!(
3614 2220 * AI3,
3615 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3616 );
3617
3618 assert_eq!(
3619 operator.total_storage_fee_deposit,
3620 bundle_storage_fund::total_balance::<Test>(operator_id)
3621 );
3622
3623 for deposit in deposits {
3624 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3625 }
3626 for withdrawal in withdrawals {
3627 do_withdraw_stake::<Test>(
3628 operator_id,
3629 withdrawal.0,
3630 withdrawal.1,
3633 )
3634 .unwrap();
3635 }
3636
3637 do_mark_operators_as_slashed::<Test>(
3638 vec![operator_id],
3639 SlashedReason::InvalidBundle(1),
3640 )
3641 .unwrap();
3642
3643 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3644
3645 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3646 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3647
3648 let operator = Operators::<Test>::get(operator_id).unwrap();
3649 assert_eq!(
3650 *operator.status::<Test>(operator_id),
3651 OperatorStatus::Slashed
3652 );
3653
3654 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3655 assert!(pending_slashes.contains(&operator_id));
3656
3657 assert_eq!(
3658 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3659 0
3660 );
3661
3662 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3665 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3666 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3667
3668 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3669 assert_eq!(Operators::<Test>::get(operator_id), None);
3670 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3671
3672 assert_eq!(
3673 Balances::total_balance(&operator_account),
3674 operator_free_balance - operator_stake
3675 );
3676 for nominator_account in nominator_accounts {
3677 assert_eq!(
3678 Balances::total_balance(&nominator_account),
3679 nominator_free_balance - nominator_stake
3680 );
3681 }
3682
3683 assert_eq!(
3684 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3685 2220 * AI3
3686 );
3687 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3688 });
3689 }
3690
3691 #[test]
3692 fn slash_operators() {
3693 let domain_id = DomainId::new(0);
3694 let operator_free_balance = 250 * AI3;
3695 let operator_stake = 200 * AI3;
3696
3697 let operator_account_1 = 1;
3698 let operator_account_2 = 2;
3699 let operator_account_3 = 3;
3700
3701 let pair_1 = OperatorPair::from_seed(&[0; 32]);
3702 let pair_2 = OperatorPair::from_seed(&[1; 32]);
3703 let pair_3 = OperatorPair::from_seed(&[2; 32]);
3704
3705 let mut ext = new_test_ext();
3706 ext.execute_with(|| {
3707 let (operator_id_1, _) = register_operator(
3708 domain_id,
3709 operator_account_1,
3710 operator_free_balance,
3711 operator_stake,
3712 10 * AI3,
3713 pair_1.public(),
3714 Default::default(),
3715 Default::default(),
3716 );
3717
3718 let (operator_id_2, _) = register_operator(
3719 domain_id,
3720 operator_account_2,
3721 operator_free_balance,
3722 operator_stake,
3723 10 * AI3,
3724 pair_2.public(),
3725 Default::default(),
3726 Default::default(),
3727 );
3728
3729 let (operator_id_3, _) = register_operator(
3730 domain_id,
3731 operator_account_3,
3732 operator_free_balance,
3733 operator_stake,
3734 10 * AI3,
3735 pair_3.public(),
3736 Default::default(),
3737 Default::default(),
3738 );
3739
3740 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3741 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3742 assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
3743 assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
3744 assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
3745 assert_eq!(
3746 domain_stake_summary.current_total_stake,
3747 STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
3748 );
3749 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3750 let operator = Operators::<Test>::get(operator_id).unwrap();
3751 assert_eq!(
3752 operator.total_storage_fee_deposit,
3753 STORAGE_FEE_RESERVE * operator_stake
3754 );
3755 assert_eq!(
3756 operator.total_storage_fee_deposit,
3757 bundle_storage_fund::total_balance::<Test>(operator_id)
3758 );
3759 }
3760
3761 do_mark_operators_as_slashed::<Test>(
3762 vec![operator_id_1],
3763 SlashedReason::InvalidBundle(1),
3764 )
3765 .unwrap();
3766 do_mark_operators_as_slashed::<Test>(
3767 vec![operator_id_2],
3768 SlashedReason::InvalidBundle(2),
3769 )
3770 .unwrap();
3771 do_mark_operators_as_slashed::<Test>(
3772 vec![operator_id_3],
3773 SlashedReason::InvalidBundle(3),
3774 )
3775 .unwrap();
3776
3777 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3778 assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
3779 assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
3780 assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
3781
3782 let operator = Operators::<Test>::get(operator_id_1).unwrap();
3783 assert_eq!(
3784 *operator.status::<Test>(operator_id_1),
3785 OperatorStatus::Slashed
3786 );
3787
3788 let operator = Operators::<Test>::get(operator_id_2).unwrap();
3789 assert_eq!(
3790 *operator.status::<Test>(operator_id_2),
3791 OperatorStatus::Slashed
3792 );
3793
3794 let operator = Operators::<Test>::get(operator_id_3).unwrap();
3795 assert_eq!(
3796 *operator.status::<Test>(operator_id_3),
3797 OperatorStatus::Slashed
3798 );
3799
3800 assert_eq!(
3801 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3802 0
3803 );
3804
3805 let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
3806 slashed_operators.into_iter().for_each(|_| {
3807 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3808 });
3809
3810 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3811 assert_eq!(Operators::<Test>::get(operator_id_1), None);
3812 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
3813 assert_eq!(Operators::<Test>::get(operator_id_2), None);
3814 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
3815 assert_eq!(Operators::<Test>::get(operator_id_3), None);
3816 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
3817
3818 assert_eq!(
3819 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3820 600 * AI3
3821 );
3822 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3823 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3824 }
3825 });
3826 }
3827
3828 #[test]
3829 fn bundle_storage_fund_charged_and_refund_storage_fee() {
3830 let domain_id = DomainId::new(0);
3831 let operator_account = 1;
3832 let operator_free_balance = 150 * AI3;
3833 let operator_total_stake = 100 * AI3;
3834 let operator_stake = 80 * AI3;
3835 let operator_storage_fee_deposit = 20 * AI3;
3836 let pair = OperatorPair::from_seed(&[0; 32]);
3837 let nominator_account = 2;
3838
3839 let mut ext = new_test_ext();
3840 ext.execute_with(|| {
3841 let (operator_id, _) = register_operator(
3842 domain_id,
3843 operator_account,
3844 operator_free_balance,
3845 operator_total_stake,
3846 AI3,
3847 pair.public(),
3848 Default::default(),
3849 BTreeMap::default(),
3850 );
3851
3852 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3853 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
3854
3855 let operator = Operators::<Test>::get(operator_id).unwrap();
3856 assert_eq!(operator.current_total_stake, operator_stake);
3857 assert_eq!(operator.current_total_shares, operator_stake);
3858 assert_eq!(
3859 operator.total_storage_fee_deposit,
3860 operator_storage_fee_deposit
3861 );
3862
3863 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
3865 operator_id,
3866 (operator_storage_fee_deposit / AI3) as u32,
3868 )
3869 .unwrap();
3870 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3871 assert_err!(
3872 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
3873 bundle_storage_fund::Error::BundleStorageFeePayment
3874 );
3875
3876 do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
3878 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3879
3880 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3881 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3882
3883 Balances::set_balance(&nominator_account, 100 * AI3);
3885 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
3886 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3887
3888 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3889 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3890
3891 bundle_storage_fund::refund_storage_fee::<Test>(
3893 10 * AI3,
3894 BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
3895 )
3896 .unwrap();
3897 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3898
3899 assert_eq!(
3901 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3902 9 * AI3
3903 );
3904
3905 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3906 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3907 });
3908 }
3909
3910 #[test]
3911 fn zero_amount_deposit_and_withdraw() {
3912 let domain_id = DomainId::new(0);
3913 let operator_account = 1;
3914 let operator_free_balance = 250 * AI3;
3915 let operator_stake = 200 * AI3;
3916 let pair = OperatorPair::from_seed(&[0; 32]);
3917 let nominator_account = 2;
3918 let nominator_free_balance = 150 * AI3;
3919 let nominator_stake = 100 * AI3;
3920
3921 let nominators = vec![
3922 (operator_account, (operator_free_balance, operator_stake)),
3923 (nominator_account, (nominator_free_balance, nominator_stake)),
3924 ];
3925
3926 let total_deposit = 300 * AI3;
3927 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3928
3929 let mut ext = new_test_ext();
3930 ext.execute_with(|| {
3931 let (operator_id, _) = register_operator(
3932 domain_id,
3933 operator_account,
3934 operator_free_balance,
3935 operator_stake,
3936 10 * AI3,
3937 pair.public(),
3938 Default::default(),
3939 BTreeMap::from_iter(nominators),
3940 );
3941
3942 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3943 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3944 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3945
3946 assert_err!(
3948 do_nominate_operator::<Test>(operator_id, nominator_account, 0),
3949 StakingError::ZeroDeposit
3950 );
3951
3952 assert_err!(
3954 do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
3955 StakingError::ZeroWithdraw
3956 );
3957
3958 do_withdraw_stake::<Test>(
3960 operator_id,
3961 nominator_account,
3962 STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
3964 )
3965 .unwrap();
3966 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3967 });
3968 }
3969
3970 #[test]
3971 fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
3972 let domain_id = DomainId::new(0);
3973 let operator_account = 1;
3974 let operator_free_balance = 250 * AI3;
3975 let operator_stake = 200 * AI3;
3976 let pair = OperatorPair::from_seed(&[0; 32]);
3977 let nominator_account = 2;
3978 let nominator_free_balance = 150 * AI3;
3979 let nominator_stake = 100 * AI3;
3980
3981 let nominators = vec![
3982 (operator_account, (operator_free_balance, operator_stake)),
3983 (nominator_account, (nominator_free_balance, nominator_stake)),
3984 ];
3985
3986 let total_deposit = 300 * AI3;
3987 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3988
3989 let mut ext = new_test_ext();
3990 ext.execute_with(|| {
3991 let (operator_id, _) = register_operator(
3992 domain_id,
3993 operator_account,
3994 operator_free_balance,
3995 operator_stake,
3996 10 * AI3,
3997 pair.public(),
3998 Default::default(),
3999 BTreeMap::from_iter(nominators),
4000 );
4001
4002 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4003 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4004 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4005
4006 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4007 do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4009
4010 let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4012 OperatorEpochSharePrice::<Test>::remove(
4014 operator_id,
4015 DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4016 );
4017
4018 assert_err!(
4020 do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4021 StakingError::MissingOperatorEpochSharePrice
4022 );
4023 assert_err!(
4024 do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4025 StakingError::MissingOperatorEpochSharePrice
4026 );
4027 });
4028 }
4029
4030 #[test]
4031 fn test_share_price_deposit() {
4032 let total_shares = 45 * AI3;
4033 let total_stake = 45 * AI3 + 37;
4034 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4035
4036 let to_deposit_stakes = [
4038 5,
4039 7,
4040 9,
4041 11,
4042 17,
4043 23,
4044 934,
4045 24931,
4046 349083467,
4047 2 * AI3 + 32,
4048 52 * AI3 - 4729034,
4049 2732 * AI3 - 1720,
4050 1117 * AI3 + 1839832,
4051 31232 * AI3 - 987654321,
4052 ];
4053
4054 let mut deposited_share = 0;
4055 let mut deposited_stake = 0;
4056 for to_deposit_stake in to_deposit_stakes {
4057 let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4058
4059 deposited_stake += to_deposit_stake;
4061 deposited_share += to_deposit_share;
4064
4065 let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4070
4071 assert!(total_deposited_share >= deposited_share);
4076
4077 assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4080 }
4081 }
4082
4083 #[test]
4084 fn test_share_price_withdraw() {
4085 let total_shares = 123 * AI3;
4086 let total_stake = 123 * AI3 + 13;
4087 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4088
4089 let to_withdraw_shares = [
4091 1,
4092 3,
4093 7,
4094 13,
4095 17,
4096 123,
4097 43553,
4098 546393039,
4099 15 * AI3 + 1342,
4100 2 * AI3 - 423,
4101 31 * AI3 - 1321,
4102 42 * AI3 + 4564234,
4103 7 * AI3 - 987654321,
4104 3 * AI3 + 987654321123879,
4105 ];
4106
4107 let mut withdrawn_share = 0;
4108 let mut withdrawn_stake = 0;
4109 for to_withdraw_share in to_withdraw_shares {
4110 let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4111
4112 withdrawn_share += to_withdraw_share;
4114 withdrawn_stake += to_withdraw_stake;
4117
4118 let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4123
4124 assert!(total_withdrawn_stake >= withdrawn_stake);
4128
4129 assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4132 }
4133 }
4134
4135 #[test]
4136 fn test_share_price_unlock() {
4137 let mut total_shares = 20 * AI3;
4138 let mut total_stake = 20 * AI3 + 12;
4139
4140 for to_unlock_share in [
4146 AI3 + 123,
4147 2 * AI3 - 456,
4148 3 * AI3 - 789,
4149 4 * AI3 - 123 + 456,
4150 7 * AI3 + 789 - 987654321,
4151 3 * AI3 + 987654321,
4152 ] {
4153 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4154
4155 let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4156
4157 total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4158 total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4159 }
4160 assert_eq!(total_shares, 0);
4161 }
4162}