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, DomainBlockNumberFor, DomainHashingFor, Event,
15 ExecutionReceiptOf, HoldIdentifier, InvalidBundleAuthors, NominatorId, OperatorEpochSharePrice,
16 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 Ok(())
694 },
695 )
696 })
697}
698
699pub(crate) fn current_share_price<T: Config>(
702 operator_id: OperatorId,
703 operator: &Operator<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
704 domain_stake_summary: &StakingSummary<OperatorId, BalanceOf<T>>,
705) -> Result<SharePrice, Error> {
706 let total_stake = domain_stake_summary
708 .current_epoch_rewards
709 .get(&operator_id)
710 .and_then(|rewards| {
711 let operator_tax = operator.nomination_tax.mul_floor(*rewards);
712 operator
713 .current_total_stake
714 .checked_add(rewards)?
715 .checked_sub(&operator_tax)
717 })
718 .unwrap_or(operator.current_total_stake);
719
720 SharePrice::new::<T>(operator.current_total_shares, total_stake)
721}
722
723pub(crate) fn do_withdraw_stake<T: Config>(
732 operator_id: OperatorId,
733 nominator_id: NominatorId<T>,
734 to_withdraw: T::Share,
735) -> Result<(), Error> {
736 ensure!(!to_withdraw.is_zero(), Error::ZeroWithdraw);
739
740 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
741 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
742 ensure!(
743 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
744 Error::OperatorNotRegistered
745 );
746
747 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
749 note_pending_staking_operation::<T>(operator.current_domain_id)?;
750 }
751
752 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
754 .ok_or(Error::DomainNotInitialized)?;
755 let domain_current_epoch = (
756 operator.current_domain_id,
757 domain_stake_summary.current_epoch_index,
758 )
759 .into();
760
761 let known_shares =
762 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
763 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
764 do_convert_previous_epoch_deposits::<T>(
765 operator_id,
766 deposit,
767 domain_stake_summary.current_epoch_index,
768 )?;
769 Ok(deposit.known.shares)
770 })?;
771
772 Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
773 if let Some(withdrawal) = maybe_withdrawal {
774 do_convert_previous_epoch_withdrawal::<T>(
775 operator_id,
776 withdrawal,
777 domain_stake_summary.current_epoch_index,
778 )?;
779 if withdrawal.withdrawals.len() as u32 >= T::WithdrawalLimit::get() {
780 return Err(Error::TooManyWithdrawals);
781 }
782 }
783 Ok(())
784 })?;
785
786 let operator_owner =
787 OperatorIdOwner::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
788
789 let is_operator_owner = operator_owner == nominator_id;
790
791 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
792 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
793
794 let (remaining_shares, shares_withdrew) = {
795 let remaining_shares = known_shares
796 .checked_sub(&to_withdraw)
797 .ok_or(Error::InsufficientShares)?;
798
799 if remaining_shares.is_zero() {
801 if is_operator_owner {
802 return Err(Error::MinimumOperatorStake);
803 }
804
805 (remaining_shares, to_withdraw)
806 } else {
807 let share_price =
808 current_share_price::<T>(operator_id, operator, &domain_stake_summary)?;
809
810 let remaining_storage_fee =
811 Perquintill::from_rational(remaining_shares.into(), known_shares.into())
812 .mul_floor(deposit.known.storage_fee_deposit);
813
814 let remaining_stake = share_price
815 .shares_to_stake::<T>(remaining_shares)
816 .checked_add(&remaining_storage_fee)
817 .ok_or(Error::BalanceOverflow)?;
818
819 if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
822 return Err(Error::MinimumOperatorStake);
823 }
824
825 if !is_operator_owner && remaining_stake.lt(&operator.minimum_nominator_stake) {
827 (T::Share::zero(), known_shares)
828 } else {
829 (remaining_shares, to_withdraw)
830 }
831 }
832 };
833
834 let storage_fee_to_withdraw =
837 Perquintill::from_rational(shares_withdrew.into(), known_shares.into())
838 .mul_floor(deposit.known.storage_fee_deposit);
839
840 let withdraw_storage_fee = {
841 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
842 operator_id,
843 operator.total_storage_fee_deposit,
844 );
845 bundle_storage_fund::withdraw_and_hold::<T>(
846 operator_id,
847 &nominator_id,
848 storage_fund_redeem_price.redeem(storage_fee_to_withdraw),
849 )
850 .map_err(Error::BundleStorageFund)?
851 };
852
853 deposit.known.storage_fee_deposit = deposit
854 .known
855 .storage_fee_deposit
856 .checked_sub(&storage_fee_to_withdraw)
857 .ok_or(Error::BalanceOverflow)?;
858
859 operator.total_storage_fee_deposit = operator
860 .total_storage_fee_deposit
861 .checked_sub(&storage_fee_to_withdraw)
862 .ok_or(Error::BalanceOverflow)?;
863
864 operator.withdrawals_in_epoch = operator
866 .withdrawals_in_epoch
867 .checked_add(&shares_withdrew)
868 .ok_or(Error::ShareOverflow)?;
869
870 deposit.known.shares = remaining_shares;
871 if remaining_shares.is_zero()
872 && let Some(pending_deposit) = deposit.pending
873 {
874 ensure!(
877 pending_deposit.total()? >= operator.minimum_nominator_stake,
878 Error::MinimumNominatorStake
879 );
880 }
881
882 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
883 let unlock_at_confirmed_domain_block_number = head_domain_number
884 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
885 .ok_or(Error::BlockNumberOverflow)?;
886
887 Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
888 let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
889 let new_withdrawal_in_shares = match withdrawal.withdrawal_in_shares.take() {
892 Some(WithdrawalInShares {
893 shares,
894 storage_fee_refund,
895 ..
896 }) => WithdrawalInShares {
897 domain_epoch: domain_current_epoch,
898 shares: shares
899 .checked_add(&shares_withdrew)
900 .ok_or(Error::ShareOverflow)?,
901 unlock_at_confirmed_domain_block_number,
902 storage_fee_refund: storage_fee_refund
903 .checked_add(&withdraw_storage_fee)
904 .ok_or(Error::BalanceOverflow)?,
905 },
906 None => WithdrawalInShares {
907 domain_epoch: domain_current_epoch,
908 unlock_at_confirmed_domain_block_number,
909 shares: shares_withdrew,
910 storage_fee_refund: withdraw_storage_fee,
911 },
912 };
913 withdrawal.withdrawal_in_shares = Some(new_withdrawal_in_shares);
914 withdrawal.total_storage_fee_withdrawal = withdrawal
915 .total_storage_fee_withdrawal
916 .checked_add(&withdraw_storage_fee)
917 .ok_or(Error::BalanceOverflow)?;
918
919 *maybe_withdrawal = Some(withdrawal);
920 Ok(())
921 })
922 })
923 })
924}
925
926pub(crate) fn do_unlock_funds<T: Config>(
930 operator_id: OperatorId,
931 nominator_id: NominatorId<T>,
932) -> Result<u32, Error> {
933 let operator = Operators::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
934 ensure!(
935 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
936 Error::OperatorNotRegistered
937 );
938
939 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
940 .ok_or(Error::DomainNotInitialized)?
941 .current_epoch_index;
942
943 Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
944 let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
945 do_convert_previous_epoch_withdrawal::<T>(
946 operator_id,
947 withdrawal,
948 current_domain_epoch_index,
949 )?;
950
951 ensure!(!withdrawal.withdrawals.is_empty(), Error::MissingWithdrawal);
952
953 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
954
955 let mut withdrawal_count = 0;
956 let mut total_unlocked_amount = BalanceOf::<T>::zero();
957 let mut total_storage_fee_refund = BalanceOf::<T>::zero();
958 loop {
959 if withdrawal
960 .withdrawals
961 .front()
962 .map(|w| w.unlock_at_confirmed_domain_block_number > head_domain_number)
963 .unwrap_or(true)
964 {
965 break;
966 }
967
968 let WithdrawalInBalance {
969 amount_to_unlock,
970 storage_fee_refund,
971 ..
972 } = withdrawal
973 .withdrawals
974 .pop_front()
975 .expect("Must not empty as checked above; qed");
976
977 total_unlocked_amount = total_unlocked_amount
978 .checked_add(&amount_to_unlock)
979 .ok_or(Error::BalanceOverflow)?;
980
981 total_storage_fee_refund = total_storage_fee_refund
982 .checked_add(&storage_fee_refund)
983 .ok_or(Error::BalanceOverflow)?;
984
985 withdrawal_count += 1;
986 }
987
988 ensure!(
991 !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
992 Error::UnlockPeriodNotComplete
993 );
994
995 withdrawal.total_withdrawal_amount = withdrawal
997 .total_withdrawal_amount
998 .checked_sub(&total_unlocked_amount)
999 .ok_or(Error::BalanceUnderflow)?;
1000
1001 withdrawal.total_storage_fee_withdrawal = withdrawal
1002 .total_storage_fee_withdrawal
1003 .checked_sub(&total_storage_fee_refund)
1004 .ok_or(Error::BalanceUnderflow)?;
1005
1006 let (amount_to_mint, amount_to_release) = DepositOnHold::<T>::try_mutate(
1009 (operator_id, nominator_id.clone()),
1010 |deposit_on_hold| {
1011 let amount_to_release = total_unlocked_amount.min(*deposit_on_hold);
1012 let amount_to_mint = total_unlocked_amount.saturating_sub(*deposit_on_hold);
1013
1014 *deposit_on_hold = deposit_on_hold.saturating_sub(amount_to_release);
1015
1016 Ok((amount_to_mint, amount_to_release))
1017 },
1018 )?;
1019
1020 if !amount_to_mint.is_zero() {
1022 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1023 }
1024 if !amount_to_release.is_zero() {
1026 let staked_hold_id = T::HoldIdentifier::staking_staked();
1027 T::Currency::release(
1028 &staked_hold_id,
1029 &nominator_id,
1030 amount_to_release,
1031 Precision::Exact,
1032 )
1033 .map_err(|_| Error::RemoveLock)?;
1034 }
1035
1036 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1037 operator_id,
1038 nominator_id: nominator_id.clone(),
1039 unlocked_amount: total_unlocked_amount,
1040 });
1041
1042 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1044 T::Currency::release(
1045 &storage_fund_hold_id,
1046 &nominator_id,
1047 total_storage_fee_refund,
1048 Precision::Exact,
1049 )
1050 .map_err(|_| Error::RemoveLock)?;
1051
1052 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1053 operator_id,
1054 nominator_id: nominator_id.clone(),
1055 storage_fee: total_storage_fee_refund,
1056 });
1057
1058 if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1060 *maybe_withdrawal = None;
1061 Deposits::<T>::mutate_exists(operator_id, nominator_id.clone(), |maybe_deposit| {
1063 if let Some(deposit) = maybe_deposit
1064 && deposit.known.shares.is_zero()
1065 && deposit.pending.is_none()
1066 {
1067 *maybe_deposit = None;
1068
1069 DepositOnHold::<T>::mutate_exists(
1070 (operator_id, nominator_id),
1071 |maybe_deposit_on_hold| {
1072 if let Some(deposit_on_hold) = maybe_deposit_on_hold
1073 && deposit_on_hold.is_zero()
1074 {
1075 *maybe_deposit_on_hold = None
1076 }
1077 },
1078 );
1079 }
1080 });
1081 }
1082
1083 Ok(withdrawal_count)
1084 })
1085}
1086
1087pub(crate) fn do_unlock_nominator<T: Config>(
1089 operator_id: OperatorId,
1090 nominator_id: NominatorId<T>,
1091) -> Result<(), Error> {
1092 Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
1093 let mut operator = maybe_operator.take().ok_or(Error::UnknownOperator)?;
1095 let OperatorDeregisteredInfo {
1096 domain_epoch,
1097 unlock_at_confirmed_domain_block_number,
1098 } = match operator.status::<T>(operator_id) {
1099 OperatorStatus::Deregistered(operator_deregistered_info) => operator_deregistered_info,
1100 _ => return Err(Error::OperatorNotDeregistered),
1101 };
1102
1103 let (domain_id, _) = domain_epoch.deconstruct();
1104 let head_domain_number = HeadDomainNumber::<T>::get(domain_id);
1105 ensure!(
1106 *unlock_at_confirmed_domain_block_number <= head_domain_number,
1107 Error::UnlockPeriodNotComplete
1108 );
1109
1110 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1111 .ok_or(Error::DomainNotInitialized)?
1112 .current_epoch_index;
1113
1114 let mut total_shares = operator.current_total_shares;
1115 let mut total_stake = operator.current_total_stake;
1116 let share_price = SharePrice::new::<T>(total_shares, total_stake)?;
1117
1118 let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
1119 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1120 operator_id,
1121 total_storage_fee_deposit,
1122 );
1123 let mut deposit = Deposits::<T>::take(operator_id, nominator_id.clone())
1124 .ok_or(Error::UnknownNominator)?;
1125
1126 match do_convert_previous_epoch_deposits::<T>(
1128 operator_id,
1129 &mut deposit,
1130 current_domain_epoch_index,
1131 ) {
1132 Ok(()) | Err(Error::MissingOperatorEpochSharePrice) => {}
1134 Err(err) => return Err(err),
1135 }
1136
1137 let (
1142 amount_ready_to_withdraw,
1143 total_storage_fee_withdrawal,
1144 shares_withdrew_in_current_epoch,
1145 ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
1146 .map(|mut withdrawal| {
1147 match do_convert_previous_epoch_withdrawal::<T>(
1148 operator_id,
1149 &mut withdrawal,
1150 current_domain_epoch_index,
1151 ) {
1152 Ok(()) | Err(Error::MissingOperatorEpochSharePrice) => {}
1154 Err(err) => return Err(err),
1155 }
1156 Ok((
1157 withdrawal.total_withdrawal_amount,
1158 withdrawal.total_storage_fee_withdrawal,
1159 withdrawal
1160 .withdrawal_in_shares
1161 .map(|WithdrawalInShares { shares, .. }| shares)
1162 .unwrap_or_default(),
1163 ))
1164 })
1165 .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
1166
1167 let nominator_shares = deposit
1169 .known
1170 .shares
1171 .checked_add(&shares_withdrew_in_current_epoch)
1172 .ok_or(Error::ShareOverflow)?;
1173 total_shares = total_shares
1174 .checked_sub(&nominator_shares)
1175 .ok_or(Error::ShareOverflow)?;
1176
1177 let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
1179 total_stake = total_stake
1180 .checked_sub(&nominator_staked_amount)
1181 .ok_or(Error::BalanceOverflow)?;
1182
1183 let amount_deposited_in_epoch = deposit
1185 .pending
1186 .map(|pending_deposit| pending_deposit.amount)
1187 .unwrap_or_default();
1188
1189 let total_amount_to_unlock = nominator_staked_amount
1190 .checked_add(&amount_ready_to_withdraw)
1191 .and_then(|amount| amount.checked_add(&amount_deposited_in_epoch))
1192 .ok_or(Error::BalanceOverflow)?;
1193
1194 let current_locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
1196 if let Some(amount_to_mint) = total_amount_to_unlock.checked_sub(¤t_locked_amount) {
1197 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1198 }
1199 if !current_locked_amount.is_zero() {
1200 let staked_hold_id = T::HoldIdentifier::staking_staked();
1201 T::Currency::release(
1202 &staked_hold_id,
1203 &nominator_id,
1204 current_locked_amount,
1205 Precision::Exact,
1206 )
1207 .map_err(|_| Error::RemoveLock)?;
1208 }
1209
1210 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1211 operator_id,
1212 nominator_id: nominator_id.clone(),
1213 unlocked_amount: total_amount_to_unlock,
1214 });
1215
1216 let nominator_total_storage_fee_deposit = deposit
1218 .pending
1219 .map(|pending_deposit| pending_deposit.storage_fee_deposit)
1220 .unwrap_or(Zero::zero())
1221 .checked_add(&deposit.known.storage_fee_deposit)
1222 .ok_or(Error::BalanceOverflow)?;
1223
1224 bundle_storage_fund::withdraw_to::<T>(
1225 operator_id,
1226 &nominator_id,
1227 storage_fund_redeem_price.redeem(nominator_total_storage_fee_deposit),
1228 )
1229 .map_err(Error::BundleStorageFund)?;
1230
1231 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1233 T::Currency::release(
1234 &storage_fund_hold_id,
1235 &nominator_id,
1236 total_storage_fee_withdrawal,
1237 Precision::Exact,
1238 )
1239 .map_err(|_| Error::RemoveLock)?;
1240
1241 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1242 operator_id,
1243 nominator_id: nominator_id.clone(),
1244 storage_fee: total_storage_fee_withdrawal,
1245 });
1246
1247 total_storage_fee_deposit =
1249 total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1250
1251 let cleanup_operator = !Deposits::<T>::contains_prefix(operator_id)
1254 && !Withdrawals::<T>::contains_prefix(operator_id);
1255
1256 if cleanup_operator {
1257 do_cleanup_operator::<T>(operator_id, total_stake)?
1258 } else {
1259 operator.current_total_shares = total_shares;
1261 operator.current_total_stake = total_stake;
1262 operator.total_storage_fee_deposit = total_storage_fee_deposit;
1263
1264 *maybe_operator = Some(operator);
1265 }
1266
1267 Ok(())
1268 })
1269}
1270
1271pub(crate) fn do_cleanup_operator<T: Config>(
1273 operator_id: OperatorId,
1274 total_stake: BalanceOf<T>,
1275) -> Result<(), Error> {
1276 bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1278 .map_err(Error::BundleStorageFund)?;
1279
1280 mint_into_treasury::<T>(total_stake)?;
1282
1283 OperatorIdOwner::<T>::remove(operator_id);
1285
1286 OperatorHighestSlot::<T>::remove(operator_id);
1288
1289 let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1291
1292 Ok(())
1293}
1294
1295pub(crate) fn do_reward_operators<T: Config>(
1297 domain_id: DomainId,
1298 source: OperatorRewardSource<BlockNumberFor<T>>,
1299 operators: IntoIter<OperatorId>,
1300 rewards: BalanceOf<T>,
1301) -> Result<(), Error> {
1302 if rewards.is_zero() {
1303 return Ok(());
1304 }
1305 DomainStakingSummary::<T>::mutate(domain_id, |maybe_stake_summary| {
1306 let stake_summary = maybe_stake_summary
1307 .as_mut()
1308 .ok_or(Error::DomainNotInitialized)?;
1309
1310 let total_count = operators.len() as u64;
1311 let operator_weights = operators.into_iter().fold(
1313 BTreeMap::<OperatorId, u64>::new(),
1314 |mut acc, operator_id| {
1315 acc.entry(operator_id)
1316 .and_modify(|weight| *weight += 1)
1317 .or_insert(1);
1318 acc
1319 },
1320 );
1321
1322 let mut allocated_rewards = BalanceOf::<T>::zero();
1323 for (operator_id, weight) in operator_weights {
1324 let operator_reward = {
1325 let distribution = Perquintill::from_rational(weight, total_count);
1326 distribution.mul_floor(rewards)
1327 };
1328
1329 stake_summary
1330 .current_epoch_rewards
1331 .entry(operator_id)
1332 .and_modify(|rewards| *rewards = rewards.saturating_add(operator_reward))
1333 .or_insert(operator_reward);
1334
1335 Pallet::<T>::deposit_event(Event::OperatorRewarded {
1336 source: source.clone(),
1337 operator_id,
1338 reward: operator_reward,
1339 });
1340
1341 allocated_rewards = allocated_rewards
1342 .checked_add(&operator_reward)
1343 .ok_or(Error::BalanceOverflow)?;
1344 }
1345
1346 mint_into_treasury::<T>(
1348 rewards
1349 .checked_sub(&allocated_rewards)
1350 .ok_or(Error::BalanceUnderflow)?,
1351 )
1352 })
1353}
1354
1355pub(crate) fn do_mark_operators_as_slashed<T: Config>(
1358 operator_ids: impl AsRef<[OperatorId]>,
1359 slash_reason: SlashedReason<DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
1360) -> Result<(), Error> {
1361 for operator_id in operator_ids.as_ref() {
1362 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1363 let operator = match maybe_operator.as_mut() {
1364 None => return Ok(()),
1368 Some(operator) => operator,
1369 };
1370 let mut pending_slashes =
1371 PendingSlashes::<T>::get(operator.current_domain_id).unwrap_or_default();
1372
1373 if pending_slashes.contains(operator_id) {
1374 return Ok(());
1375 }
1376
1377 DomainStakingSummary::<T>::try_mutate(
1378 operator.current_domain_id,
1379 |maybe_domain_stake_summary| {
1380 let stake_summary = maybe_domain_stake_summary
1381 .as_mut()
1382 .ok_or(Error::DomainNotInitialized)?;
1383
1384 operator.update_status(OperatorStatus::Slashed);
1386
1387 if stake_summary
1390 .current_operators
1391 .remove(operator_id)
1392 .is_some()
1393 {
1394 stake_summary.current_total_stake = stake_summary
1395 .current_total_stake
1396 .checked_sub(&operator.current_total_stake)
1397 .ok_or(Error::BalanceUnderflow)?;
1398 }
1399 stake_summary.next_operators.remove(operator_id);
1400 pending_slashes.insert(*operator_id);
1401 PendingSlashes::<T>::insert(operator.current_domain_id, pending_slashes);
1402 Pallet::<T>::deposit_event(Event::OperatorSlashed {
1403 operator_id: *operator_id,
1404 reason: slash_reason.clone(),
1405 });
1406 Ok(())
1407 },
1408 )
1409 })?
1410 }
1411
1412 Ok(())
1413}
1414
1415pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1417 domain_id: DomainId,
1418 er: &ExecutionReceiptOf<T>,
1419) -> Result<(), Error> {
1420 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1421 let er_hash = er.hash::<DomainHashingFor<T>>();
1422 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1423 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1424 let mut stake_summary =
1425 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1426
1427 for operator_id in invalid_bundle_authors {
1428 if pending_slashes.contains(&operator_id) {
1429 continue;
1430 }
1431
1432 mark_invalid_bundle_author::<T>(
1433 operator_id,
1434 er_hash,
1435 &mut stake_summary,
1436 &mut invalid_bundle_authors_in_epoch,
1437 )?;
1438 }
1439
1440 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1441 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1442 Ok(())
1443}
1444
1445fn mark_invalid_bundle_author<T: Config>(
1446 operator_id: OperatorId,
1447 er_hash: ReceiptHashFor<T>,
1448 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1449 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1450) -> Result<(), Error> {
1451 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1452 let operator = match maybe_operator.as_mut() {
1453 None => return Ok(()),
1457 Some(operator) => operator,
1458 };
1459
1460 if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1463 return Ok(());
1464 }
1465
1466 operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1468 invalid_bundle_authors.insert(operator_id);
1469 if stake_summary
1470 .current_operators
1471 .remove(&operator_id)
1472 .is_some()
1473 {
1474 stake_summary.current_total_stake = stake_summary
1475 .current_total_stake
1476 .checked_sub(&operator.current_total_stake)
1477 .ok_or(Error::BalanceUnderflow)?;
1478 }
1479 stake_summary.next_operators.remove(&operator_id);
1480 Ok(())
1481 })
1482}
1483
1484pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1488 domain_id: DomainId,
1489 er: &ExecutionReceiptOf<T>,
1490) -> Result<(), Error> {
1491 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1492 let er_hash = er.hash::<DomainHashingFor<T>>();
1493 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1494 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1495 let mut stake_summary =
1496 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1497
1498 for operator_id in invalid_bundle_authors {
1499 if pending_slashes.contains(&operator_id)
1500 || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1501 {
1502 continue;
1503 }
1504
1505 unmark_invalid_bundle_author::<T>(
1506 operator_id,
1507 er_hash,
1508 &mut stake_summary,
1509 &mut invalid_bundle_authors_in_epoch,
1510 )?;
1511 }
1512
1513 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1514 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1515 Ok(())
1516}
1517
1518fn unmark_invalid_bundle_author<T: Config>(
1519 operator_id: OperatorId,
1520 er_hash: ReceiptHashFor<T>,
1521 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1522 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1523) -> Result<(), Error> {
1524 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1525 let operator = match maybe_operator.as_mut() {
1526 None => return Ok(()),
1530 Some(operator) => operator,
1531 };
1532
1533 if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1535 return Ok(());
1536 }
1537
1538 operator.update_status(OperatorStatus::Registered);
1540 invalid_bundle_authors.remove(&operator_id);
1541 stake_summary.next_operators.insert(operator_id);
1542 Ok(())
1543 })
1544}
1545
1546#[cfg(test)]
1547pub(crate) mod tests {
1548 use crate::domain_registry::{DomainConfig, DomainObject};
1549 use crate::pallet::{
1550 Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1551 NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1552 };
1553 use crate::staking::{
1554 DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorStatus, SharePrice,
1555 StakingSummary, do_convert_previous_epoch_withdrawal, do_mark_operators_as_slashed,
1556 do_nominate_operator, do_reward_operators, do_unlock_funds, do_withdraw_stake,
1557 };
1558 use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1559 use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext};
1560 use crate::{
1561 BalanceOf, Error, MAX_NOMINATORS_TO_SLASH, NominatorId, OperatorEpochSharePrice,
1562 SlashedReason, bundle_storage_fund,
1563 };
1564 use frame_support::traits::Currency;
1565 use frame_support::traits::fungible::Mutate;
1566 use frame_support::weights::Weight;
1567 use frame_support::{assert_err, assert_ok};
1568 use prop_test::prelude::*;
1569 use prop_test::proptest::test_runner::TestCaseResult;
1570 use sp_core::{Pair, sr25519};
1571 use sp_domains::{
1572 DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1573 OperatorRewardSource,
1574 };
1575 use sp_runtime::traits::Zero;
1576 use sp_runtime::{PerThing, Perquintill};
1577 use std::collections::{BTreeMap, BTreeSet};
1578 use std::ops::RangeInclusive;
1579 use std::vec;
1580 use subspace_runtime_primitives::AI3;
1581
1582 type Balances = pallet_balances::Pallet<Test>;
1583 type Domains = crate::Pallet<Test>;
1584
1585 const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1586
1587 #[allow(clippy::too_many_arguments)]
1588 pub(crate) fn register_operator(
1589 domain_id: DomainId,
1590 operator_account: <Test as frame_system::Config>::AccountId,
1591 operator_free_balance: BalanceOf<Test>,
1592 operator_stake: BalanceOf<Test>,
1593 minimum_nominator_stake: BalanceOf<Test>,
1594 signing_key: OperatorPublicKey,
1595 mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1596 ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1597 nominators.insert(operator_account, (operator_free_balance, operator_stake));
1598 for nominator in &nominators {
1599 Balances::set_balance(nominator.0, nominator.1.0);
1600 assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1601 }
1602 nominators.remove(&operator_account);
1603
1604 if !DomainRegistry::<Test>::contains_key(domain_id) {
1605 let domain_config = DomainConfig {
1606 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1607 runtime_id: 0,
1608 max_bundle_size: u32::MAX,
1609 max_bundle_weight: Weight::MAX,
1610 bundle_slot_probability: (0, 0),
1611 operator_allow_list: OperatorAllowList::Anyone,
1612 initial_balances: Default::default(),
1613 };
1614
1615 let domain_obj = DomainObject {
1616 owner_account_id: 0,
1617 created_at: 0,
1618 genesis_receipt_hash: Default::default(),
1619 domain_config,
1620 domain_runtime_info: Default::default(),
1621 domain_instantiation_deposit: Default::default(),
1622 };
1623
1624 DomainRegistry::<Test>::insert(domain_id, domain_obj);
1625 }
1626
1627 if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1628 DomainStakingSummary::<Test>::insert(
1629 domain_id,
1630 StakingSummary {
1631 current_epoch_index: 0,
1632 current_total_stake: 0,
1633 current_operators: BTreeMap::new(),
1634 next_operators: BTreeSet::new(),
1635 current_epoch_rewards: BTreeMap::new(),
1636 },
1637 );
1638 }
1639
1640 let operator_config = OperatorConfig {
1641 signing_key,
1642 minimum_nominator_stake,
1643 nomination_tax: Default::default(),
1644 };
1645
1646 let res = Domains::register_operator(
1647 RuntimeOrigin::signed(operator_account),
1648 domain_id,
1649 operator_stake,
1650 operator_config.clone(),
1651 );
1652 assert_ok!(res);
1653
1654 let operator_id = NextOperatorId::<Test>::get() - 1;
1655 for nominator in nominators {
1656 if nominator.1.1.is_zero() {
1657 continue;
1658 }
1659
1660 let res = Domains::nominate_operator(
1661 RuntimeOrigin::signed(nominator.0),
1662 operator_id,
1663 nominator.1.1,
1664 );
1665 assert_ok!(res);
1666 assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1667 }
1668
1669 (operator_id, operator_config)
1670 }
1671
1672 #[test]
1673 fn test_register_operator_invalid_signing_key() {
1674 let domain_id = DomainId::new(0);
1675 let operator_account = 1;
1676
1677 let mut ext = new_test_ext();
1678 ext.execute_with(|| {
1679 let operator_config = OperatorConfig {
1680 signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1681 minimum_nominator_stake: Default::default(),
1682 nomination_tax: Default::default(),
1683 };
1684
1685 let res = Domains::register_operator(
1686 RuntimeOrigin::signed(operator_account),
1687 domain_id,
1688 Default::default(),
1689 operator_config,
1690 );
1691 assert_err!(
1692 res,
1693 Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1694 );
1695 });
1696 }
1697
1698 #[test]
1699 fn test_register_operator_minimum_nominator_stake() {
1700 let domain_id = DomainId::new(0);
1701 let operator_account = 1;
1702 let pair = OperatorPair::from_seed(&[0; 32]);
1703
1704 let mut ext = new_test_ext();
1705 ext.execute_with(|| {
1706 let operator_config = OperatorConfig {
1707 signing_key: pair.public(),
1708 minimum_nominator_stake: Default::default(),
1709 nomination_tax: Default::default(),
1710 };
1711
1712 let res = Domains::register_operator(
1713 RuntimeOrigin::signed(operator_account),
1714 domain_id,
1715 Default::default(),
1716 operator_config,
1717 );
1718 assert_err!(
1719 res,
1720 Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1721 );
1722 });
1723 }
1724
1725 #[test]
1726 fn test_register_operator() {
1727 let domain_id = DomainId::new(0);
1728 let operator_account = 1;
1729 let operator_free_balance = 2500 * AI3;
1730 let operator_total_stake = 1000 * AI3;
1731 let operator_stake = 800 * AI3;
1732 let operator_storage_fee_deposit = 200 * AI3;
1733 let pair = OperatorPair::from_seed(&[0; 32]);
1734
1735 let mut ext = new_test_ext();
1736 ext.execute_with(|| {
1737 let (operator_id, mut operator_config) = register_operator(
1738 domain_id,
1739 operator_account,
1740 operator_free_balance,
1741 operator_total_stake,
1742 AI3,
1743 pair.public(),
1744 BTreeMap::new(),
1745 );
1746
1747 assert_eq!(NextOperatorId::<Test>::get(), 1);
1748 assert_eq!(
1750 OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1751 operator_account
1752 );
1753 assert_eq!(
1754 Operators::<Test>::get(operator_id).unwrap(),
1755 Operator {
1756 signing_key: pair.public(),
1757 current_domain_id: domain_id,
1758 next_domain_id: domain_id,
1759 minimum_nominator_stake: AI3,
1760 nomination_tax: Default::default(),
1761 current_total_stake: operator_stake,
1762 current_total_shares: operator_stake,
1763 partial_status: OperatorStatus::Registered,
1764 deposits_in_epoch: 0,
1765 withdrawals_in_epoch: 0,
1766 total_storage_fee_deposit: operator_storage_fee_deposit,
1767 }
1768 );
1769
1770 let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1771 assert!(stake_summary.next_operators.contains(&operator_id));
1772 assert_eq!(stake_summary.current_total_stake, operator_stake);
1773
1774 assert_eq!(
1775 Balances::usable_balance(operator_account),
1776 operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1777 );
1778
1779 let res = Domains::register_operator(
1781 RuntimeOrigin::signed(operator_account),
1782 domain_id,
1783 operator_stake,
1784 operator_config.clone(),
1785 );
1786 assert_ok!(res);
1787
1788 let new_pair = OperatorPair::from_seed(&[1; 32]);
1790 operator_config.signing_key = new_pair.public();
1791 let res = Domains::register_operator(
1792 RuntimeOrigin::signed(operator_account),
1793 domain_id,
1794 operator_stake,
1795 operator_config,
1796 );
1797 assert_err!(
1798 res,
1799 Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1800 );
1801 });
1802 }
1803
1804 #[test]
1805 fn nominate_operator() {
1806 let domain_id = DomainId::new(0);
1807 let operator_account = 1;
1808 let operator_free_balance = 1500 * AI3;
1809 let operator_total_stake = 1000 * AI3;
1810 let operator_stake = 800 * AI3;
1811 let operator_storage_fee_deposit = 200 * AI3;
1812 let pair = OperatorPair::from_seed(&[0; 32]);
1813
1814 let nominator_account = 2;
1815 let nominator_free_balance = 150 * AI3;
1816 let nominator_total_stake = 100 * AI3;
1817 let nominator_stake = 80 * AI3;
1818 let nominator_storage_fee_deposit = 20 * AI3;
1819
1820 let mut ext = new_test_ext();
1821 ext.execute_with(|| {
1822 let (operator_id, _) = register_operator(
1823 domain_id,
1824 operator_account,
1825 operator_free_balance,
1826 operator_total_stake,
1827 10 * AI3,
1828 pair.public(),
1829 BTreeMap::from_iter(vec![(
1830 nominator_account,
1831 (nominator_free_balance, nominator_total_stake),
1832 )]),
1833 );
1834
1835 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1836 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
1837
1838 let operator = Operators::<Test>::get(operator_id).unwrap();
1839 assert_eq!(operator.current_total_stake, operator_stake);
1840 assert_eq!(operator.current_total_shares, operator_stake);
1841 assert_eq!(
1842 operator.total_storage_fee_deposit,
1843 operator_storage_fee_deposit + nominator_storage_fee_deposit
1844 );
1845 assert_eq!(operator.deposits_in_epoch, nominator_stake);
1846
1847 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1848 .unwrap()
1849 .pending
1850 .unwrap();
1851 assert_eq!(pending_deposit.amount, nominator_stake);
1852 assert_eq!(
1853 pending_deposit.storage_fee_deposit,
1854 nominator_storage_fee_deposit
1855 );
1856 assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
1857
1858 assert_eq!(
1859 Balances::usable_balance(nominator_account),
1860 nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
1861 );
1862
1863 let addtional_nomination_total_stake = 40 * AI3;
1865 let addtional_nomination_stake = 32 * AI3;
1866 let addtional_nomination_storage_fee_deposit = 8 * AI3;
1867 let res = Domains::nominate_operator(
1868 RuntimeOrigin::signed(nominator_account),
1869 operator_id,
1870 addtional_nomination_total_stake,
1871 );
1872 assert_ok!(res);
1873 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
1874 .unwrap()
1875 .pending
1876 .unwrap();
1877 assert_eq!(
1878 pending_deposit.amount,
1879 nominator_stake + addtional_nomination_stake
1880 );
1881 assert_eq!(
1882 pending_deposit.storage_fee_deposit,
1883 nominator_storage_fee_deposit + addtional_nomination_storage_fee_deposit
1884 );
1885
1886 let operator = Operators::<Test>::get(operator_id).unwrap();
1887 assert_eq!(operator.current_total_stake, operator_stake);
1888 assert_eq!(
1889 operator.deposits_in_epoch,
1890 nominator_stake + addtional_nomination_stake
1891 );
1892 assert_eq!(
1893 operator.total_storage_fee_deposit,
1894 operator_storage_fee_deposit
1895 + nominator_storage_fee_deposit
1896 + addtional_nomination_storage_fee_deposit
1897 );
1898
1899 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
1901
1902 let operator = Operators::<Test>::get(operator_id).unwrap();
1903 assert_eq!(
1904 operator.current_total_stake,
1905 operator_stake + nominator_stake + addtional_nomination_stake
1906 );
1907
1908 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1909 assert_eq!(
1910 domain_staking_summary.current_total_stake,
1911 operator_stake + nominator_stake + addtional_nomination_stake
1912 );
1913 });
1914 }
1915
1916 #[test]
1917 fn operator_deregistration() {
1918 let domain_id = DomainId::new(0);
1919 let operator_account = 1;
1920 let operator_stake = 200 * AI3;
1921 let operator_free_balance = 250 * AI3;
1922 let pair = OperatorPair::from_seed(&[0; 32]);
1923 let mut ext = new_test_ext();
1924 ext.execute_with(|| {
1925 let (operator_id, _) = register_operator(
1926 domain_id,
1927 operator_account,
1928 operator_free_balance,
1929 operator_stake,
1930 AI3,
1931 pair.public(),
1932 BTreeMap::new(),
1933 );
1934
1935 let res =
1936 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
1937 assert_ok!(res);
1938
1939 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1940 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
1941
1942 let operator = Operators::<Test>::get(operator_id).unwrap();
1943 assert_eq!(
1944 *operator.status::<Test>(operator_id),
1945 OperatorStatus::Deregistered(
1946 (
1947 domain_id,
1948 domain_stake_summary.current_epoch_index,
1949 5
1951 )
1952 .into()
1953 )
1954 );
1955
1956 let new_domain_id = DomainId::new(1);
1958 let domain_config = DomainConfig {
1959 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1960 runtime_id: 0,
1961 max_bundle_size: u32::MAX,
1962 max_bundle_weight: Weight::MAX,
1963 bundle_slot_probability: (0, 0),
1964 operator_allow_list: OperatorAllowList::Anyone,
1965 initial_balances: Default::default(),
1966 };
1967
1968 let domain_obj = DomainObject {
1969 owner_account_id: 0,
1970 created_at: 0,
1971 genesis_receipt_hash: Default::default(),
1972 domain_config,
1973 domain_runtime_info: Default::default(),
1974 domain_instantiation_deposit: Default::default(),
1975 };
1976
1977 DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
1978 DomainStakingSummary::<Test>::insert(
1979 new_domain_id,
1980 StakingSummary {
1981 current_epoch_index: 0,
1982 current_total_stake: 0,
1983 current_operators: BTreeMap::new(),
1984 next_operators: BTreeSet::new(),
1985 current_epoch_rewards: BTreeMap::new(),
1986 },
1987 );
1988
1989 let nominator_account = 100;
1991 let nominator_stake = 100 * AI3;
1992 let res = Domains::nominate_operator(
1993 RuntimeOrigin::signed(nominator_account),
1994 operator_id,
1995 nominator_stake,
1996 );
1997 assert_err!(
1998 res,
1999 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2000 );
2001 });
2002 }
2003
2004 type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2005
2006 type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2010
2011 type StorageFundChange = (bool, u32);
2013
2014 pub(crate) type Share = <Test as Config>::Share;
2015
2016 struct WithdrawParams {
2017 minimum_nominator_stake: BalanceOf<Test>,
2019 nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2022 operator_reward: BalanceOf<Test>,
2024 nominator_id: NominatorId<Test>,
2026 withdraws: WithdrawWithResult,
2028 maybe_deposit: Option<BalanceOf<Test>>,
2030 expected_withdraw: ExpectedWithdrawAmount,
2033 expected_nominator_count_reduced_by: u32,
2035 storage_fund_change: StorageFundChange,
2037 }
2038
2039 fn withdraw_stake(params: WithdrawParams) {
2041 withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2042 }
2043
2044 fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2046 withdraw_stake_inner(params, true)
2047 }
2048
2049 fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2051 let WithdrawParams {
2052 minimum_nominator_stake,
2053 nominators,
2054 operator_reward,
2055 nominator_id,
2056 withdraws,
2057 maybe_deposit,
2058 expected_withdraw,
2059 expected_nominator_count_reduced_by,
2060 storage_fund_change,
2061 } = params;
2062 let domain_id = DomainId::new(0);
2063 let operator_account = 0;
2064 let pair = OperatorPair::from_seed(&[0; 32]);
2065 let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2066 + operator_reward
2067 + maybe_deposit.unwrap_or(0);
2068
2069 let mut nominators = BTreeMap::from_iter(
2070 nominators
2071 .into_iter()
2072 .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2073 .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2074 );
2075
2076 let mut ext = new_test_ext();
2077 ext.execute_with(|| {
2078 let (operator_free_balance, operator_stake) =
2079 nominators.remove(&operator_account).unwrap();
2080 let (operator_id, _) = register_operator(
2081 domain_id,
2082 operator_account,
2083 operator_free_balance,
2084 operator_stake,
2085 minimum_nominator_stake,
2086 pair.public(),
2087 nominators,
2088 );
2089
2090 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2091
2092 if !operator_reward.is_zero() {
2093 do_reward_operators::<Test>(
2094 domain_id,
2095 OperatorRewardSource::Dummy,
2096 vec![operator_id].into_iter(),
2097 operator_reward,
2098 )
2099 .unwrap();
2100 }
2101
2102 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2103
2104 if let Some(deposit_amount) = maybe_deposit {
2105 Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2106 let res = Domains::nominate_operator(
2107 RuntimeOrigin::signed(nominator_id),
2108 operator_id,
2109 deposit_amount,
2110 );
2111 assert_ok!(res);
2112 }
2113
2114 let operator = Operators::<Test>::get(operator_id).unwrap();
2115 let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2116 if is_storage_fund_increased {
2117 bundle_storage_fund::refund_storage_fee::<Test>(
2118 storage_fund_change_amount as u128 * AI3,
2119 BTreeMap::from_iter([(operator_id, 1)]),
2120 )
2121 .unwrap();
2122 assert_eq!(
2123 operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2124 bundle_storage_fund::total_balance::<Test>(operator_id)
2125 );
2126 total_balance += storage_fund_change_amount as u128 * AI3;
2127 } else {
2128 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2129 operator_id,
2130 storage_fund_change_amount,
2131 )
2132 .unwrap();
2133 assert_eq!(
2134 operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2135 bundle_storage_fund::total_balance::<Test>(operator_id)
2136 );
2137 total_balance -= storage_fund_change_amount as u128 * AI3;
2138 }
2139
2140 for (withdraw, expected_result) in withdraws {
2141 let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2142 let res = Domains::withdraw_stake(
2143 RuntimeOrigin::signed(nominator_id),
2144 operator_id,
2145 withdraw_share_amount,
2146 );
2147 if is_proptest {
2148 prop_assert_eq!(
2149 res,
2150 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2151 "unexpected withdraw_stake result",
2152 );
2153 } else {
2154 assert_eq!(
2155 res,
2156 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2157 "unexpected withdraw_stake result",
2158 );
2159 }
2160 }
2161
2162 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2163
2164 if let Some((withdraw, include_ed)) = expected_withdraw {
2165 let previous_usable_balance = Balances::usable_balance(nominator_id);
2166
2167 HeadDomainNumber::<Test>::set(
2169 domain_id,
2170 head_domain_number
2171 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2172 );
2173 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2174
2175 let expected_balance = if include_ed {
2176 total_balance += crate::tests::ExistentialDeposit::get();
2177 previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get()
2178 } else {
2179 previous_usable_balance + withdraw
2180 };
2181
2182 if is_proptest {
2183 prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2184 } else {
2185 assert_eq!(
2186 Balances::usable_balance(nominator_id),
2187 expected_balance,
2188 "usable balance is not equal to expected balance:\n{} !=\n{}",
2189 Balances::usable_balance(nominator_id),
2190 expected_balance,
2191 );
2192 }
2193
2194 assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2196 }
2197
2198 if expected_nominator_count_reduced_by > 0 {
2201 if is_proptest {
2202 prop_assert!(
2203 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2204 "deposit exists for nominator: {nominator_id}",
2205 );
2206 prop_assert!(
2207 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2208 "deposit on hold exists for nominator: {nominator_id}",
2209 );
2210 } else {
2211 assert!(
2212 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2213 "deposit exists for nominator: {nominator_id}",
2214 );
2215 assert!(
2216 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2217 "deposit on hold exists for nominator: {nominator_id}",
2218 );
2219 }
2220 }
2221
2222 let operator = Operators::<Test>::get(operator_id).unwrap();
2224 if is_proptest {
2225 prop_assert_approx!(
2226 Balances::usable_balance(nominator_id)
2227 + operator.current_total_stake
2228 + bundle_storage_fund::total_balance::<Test>(operator_id),
2229 total_balance,
2230 "\n{} + {} + {} =",
2231 Balances::usable_balance(nominator_id),
2232 operator.current_total_stake,
2233 bundle_storage_fund::total_balance::<Test>(operator_id),
2234 );
2235 } else {
2236 assert_eq!(
2237 Balances::usable_balance(nominator_id)
2238 + operator.current_total_stake
2239 + bundle_storage_fund::total_balance::<Test>(operator_id),
2240 total_balance,
2241 "initial total balance is not equal to final total balance:\n\
2242 {} + {} + {} =\n{} !=\n{}",
2243 Balances::usable_balance(nominator_id),
2244 operator.current_total_stake,
2245 bundle_storage_fund::total_balance::<Test>(operator_id),
2246 Balances::usable_balance(nominator_id)
2247 + operator.current_total_stake
2248 + bundle_storage_fund::total_balance::<Test>(operator_id),
2249 total_balance,
2250 );
2251 }
2252
2253 Ok(())
2254 })
2255 }
2256
2257 macro_rules! prop_assert_approx {
2263 ($actual:expr, $expected:expr $(,)?) => {
2264 prop_assert_approx!($actual, $expected, "");
2265 };
2266
2267 ($actual:expr, $expected:expr, $fmt:expr) => {
2268 prop_assert_approx!($actual, $expected, $fmt,);
2269 };
2270
2271 ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2272 let actual = $actual;
2273 let expected = $expected;
2274 let extra = format!($fmt, $($args)*);
2275 prop_test::proptest::prop_assert!(
2276 actual <= expected,
2277 "extra minting: actual amount is greater than expected amount:{}{}\
2278 \n{} >\
2279 \n{}",
2280 if extra.is_empty() { "" } else { "\n" },
2281 extra,
2282 actual,
2283 expected,
2284 );
2285 let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2286 .mul_floor(expected)
2287 .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2288 prop_test::proptest::prop_assert!(
2289 actual >= expected_rounded_down,
2290 "excess rounding losses: actual amount is less than expected amount \
2291 rounded down:{}{}\
2292 \n{} <\
2293 \n{} (from\
2294 \n{})",
2295 if extra.is_empty() { "" } else { "\n" },
2296 extra,
2297 actual,
2298 expected_rounded_down,
2299 expected,
2300 );
2301 }};
2302 }
2303
2304 pub(crate) use prop_assert_approx;
2306
2307 pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2311 Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2312
2313 pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2316
2317 pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2325
2326 pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2329
2330 pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2332 <Test as Config>::MinNominatorStake::get() + 1;
2333
2334 pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2336 MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2337
2338 pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2340 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2341
2342 pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2345 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2346
2347 pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2349
2350 pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2352 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2353
2354 #[test]
2360 fn prop_withdraw_excess_operator_stake() {
2361 prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2362 let mut excess_stake =
2363 operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2364
2365 excess_stake = Perquintill::from_parts(1)
2367 .left_from_one()
2368 .mul_ceil(excess_stake);
2369 excess_stake -= 1;
2370
2371 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2372
2373 let expected_withdraw = (excess_stake, false);
2374
2375 withdraw_stake_prop(WithdrawParams {
2376 minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2377 nominators: vec![(0, operator_stake)],
2378 operator_reward: 0,
2379 nominator_id: 0,
2380 withdraws: vec![(excess_stake, Ok(()))],
2381 maybe_deposit: None,
2382 expected_withdraw: Some(expected_withdraw),
2383 expected_nominator_count_reduced_by: 0,
2384 storage_fund_change: (true, 0),
2385 })
2386 });
2387 }
2388
2389 #[test]
2392 fn prop_withdraw_excess_operator_stake_with_nominator() {
2393 prop_test!(
2394 &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2395 |(operator_stake, nominator_stake)| {
2396 prop_assert!(
2398 [operator_stake, nominator_stake]
2399 .into_iter()
2400 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2401 .is_some()
2402 );
2403
2404 let expected_withdraw = (nominator_stake, true);
2405
2406 let excess_stake = expected_withdraw
2407 .0
2408 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2409
2410 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2411
2412 withdraw_stake_prop(WithdrawParams {
2413 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2414 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2415 operator_reward: 0,
2416 nominator_id: 1,
2417 withdraws: vec![(excess_stake, Ok(()))],
2418 maybe_deposit: None,
2419 expected_withdraw: Some(expected_withdraw),
2420 expected_nominator_count_reduced_by: 1,
2421 storage_fund_change: (true, 0),
2422 })
2423 }
2424 );
2425 }
2426
2427 #[test]
2430 fn prop_withdraw_excess_operator_stake_with_deposit() {
2431 prop_test!(
2432 &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2433 |(mut operator_stake, maybe_deposit)| {
2434 operator_stake = operator_stake.saturating_add(maybe_deposit);
2435
2436 prop_assert!(
2437 operator_stake.saturating_sub(maybe_deposit)
2438 >= <Test as Config>::MinOperatorStake::get(),
2439 "would cause MinimumOperatorStake error"
2440 );
2441
2442 prop_assert!(
2444 [operator_stake, maybe_deposit]
2445 .into_iter()
2446 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2447 .is_some()
2448 );
2449
2450 let expected_withdraw = (
2451 STORAGE_FEE_RESERVE
2453 .left_from_one()
2454 .mul_ceil(operator_stake)
2455 .saturating_sub(maybe_deposit),
2456 true,
2457 );
2458
2459 let excess_stake = expected_withdraw
2460 .0
2461 .saturating_sub(<Test as Config>::MinOperatorStake::get());
2462
2463 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2464
2465 let maybe_deposit = if maybe_deposit == 0 {
2467 None
2468 } else {
2469 Some(maybe_deposit)
2470 };
2471
2472 withdraw_stake_prop(WithdrawParams {
2473 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2474 nominators: vec![(0, operator_stake)],
2475 operator_reward: 0,
2476 nominator_id: 0,
2477 withdraws: vec![(excess_stake, Ok(()))],
2478 maybe_deposit,
2479 expected_withdraw: Some(expected_withdraw),
2480 expected_nominator_count_reduced_by: 0,
2481 storage_fund_change: (true, 0),
2482 })
2483 }
2484 );
2485 }
2486
2487 #[test]
2491 fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
2492 prop_test!(
2493 &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2494 |(mut nominator_stake, maybe_deposit)| {
2495 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2496
2497 prop_assert!(
2498 nominator_stake.saturating_sub(maybe_deposit)
2499 >= <Test as Config>::MinNominatorStake::get(),
2500 "would cause MinimumNominatorStake error"
2501 );
2502
2503 let operator_stake = <Test as Config>::MinOperatorStake::get();
2505
2506 prop_assert!(
2508 [operator_stake, nominator_stake, maybe_deposit]
2509 .into_iter()
2510 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2511 .is_some()
2512 );
2513
2514 let expected_withdraw = (
2515 STORAGE_FEE_RESERVE
2516 .left_from_one()
2517 .mul_ceil(nominator_stake)
2518 .saturating_sub(maybe_deposit),
2519 true,
2520 );
2521
2522 let excess_stake = expected_withdraw
2523 .0
2524 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2525
2526 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2527
2528 let maybe_deposit = if maybe_deposit == 0 {
2530 None
2531 } else {
2532 Some(maybe_deposit)
2533 };
2534
2535 withdraw_stake_prop(WithdrawParams {
2536 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2537 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2538 operator_reward: 0,
2539 nominator_id: 1,
2540 withdraws: vec![(excess_stake, Ok(()))],
2541 maybe_deposit,
2542 expected_withdraw: Some(expected_withdraw),
2543 expected_nominator_count_reduced_by: 0,
2544 storage_fund_change: (true, 0),
2545 })
2546 }
2547 );
2548 }
2549
2550 #[test]
2553 fn prop_withdraw_excess_nominator_stake_with_deposit() {
2554 prop_test!(
2555 &(
2556 PROP_OPERATOR_STAKE_RANGE,
2557 PROP_NOMINATOR_STAKE_RANGE,
2558 PROP_DEPOSIT_RANGE,
2559 ),
2560 |(operator_stake, mut nominator_stake, maybe_deposit)| {
2561 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
2562
2563 prop_assert!(
2564 nominator_stake.saturating_sub(maybe_deposit)
2565 >= <Test as Config>::MinNominatorStake::get(),
2566 "would cause MinimumNominatorStake error"
2567 );
2568
2569 prop_assert!(
2571 [operator_stake, nominator_stake, maybe_deposit]
2572 .into_iter()
2573 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2574 .is_some()
2575 );
2576
2577 let expected_withdraw = (
2579 STORAGE_FEE_RESERVE
2580 .left_from_one()
2581 .mul_ceil(nominator_stake)
2582 .saturating_sub(maybe_deposit),
2583 true,
2584 );
2585
2586 let excess_stake = expected_withdraw
2587 .0
2588 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2589
2590 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2591
2592 let maybe_deposit = if maybe_deposit == 0 {
2594 None
2595 } else {
2596 Some(maybe_deposit)
2597 };
2598
2599 withdraw_stake_prop(WithdrawParams {
2600 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2601 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2602 operator_reward: 0,
2603 nominator_id: 1,
2604 withdraws: vec![(excess_stake, Ok(()))],
2605 maybe_deposit,
2606 expected_withdraw: Some(expected_withdraw),
2607 expected_nominator_count_reduced_by: 0,
2608 storage_fund_change: (true, 0),
2609 })
2610 }
2611 );
2612 }
2613
2614 #[test]
2617 fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
2618 prop_test!(
2619 &(
2620 PROP_OPERATOR_STAKE_RANGE,
2621 PROP_NOMINATOR_STAKE_RANGE,
2622 PROP_REWARD_RANGE,
2623 ),
2624 |(operator_stake, nominator_stake, operator_reward)| {
2625 prop_assert!(
2627 [operator_stake, nominator_stake, operator_reward]
2628 .into_iter()
2629 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2630 .is_some()
2631 );
2632
2633 let total_stake = operator_stake.saturating_add(nominator_stake);
2635 let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
2636 .mul_floor(operator_reward);
2637
2638 let excess_stake =
2641 operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
2642
2643 let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
2644
2645 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2646
2647 withdraw_stake_prop(WithdrawParams {
2648 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2649 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2650 operator_reward,
2651 nominator_id: 0,
2652 withdraws: vec![(excess_stake, Ok(()))],
2653 maybe_deposit: None,
2654 expected_withdraw: Some(expected_withdraw),
2655 expected_nominator_count_reduced_by: 0,
2656 storage_fund_change: (true, 0),
2657 })
2658 }
2659 );
2660 }
2661
2662 #[test]
2665 fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
2666 prop_test!(
2667 &(
2668 PROP_OPERATOR_STAKE_RANGE,
2669 PROP_DEPOSIT_RANGE,
2670 PROP_REWARD_RANGE,
2671 ),
2672 |(mut operator_stake, maybe_deposit, operator_reward)| {
2673 operator_stake = operator_stake.saturating_add(maybe_deposit);
2674
2675 prop_assert!(
2676 operator_stake.saturating_sub(maybe_deposit)
2677 >= <Test as Config>::MinOperatorStake::get(),
2678 "would cause MinimumOperatorStake error"
2679 );
2680
2681 prop_assert!(
2683 [operator_stake, maybe_deposit, operator_reward]
2684 .into_iter()
2685 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2686 .is_some()
2687 );
2688
2689 let reserve = 2 * <Test as Config>::MinOperatorStake::get();
2692 let excess_stake = operator_stake.saturating_sub(reserve);
2693
2694 let expected_withdraw = (
2696 operator_stake
2697 .saturating_add(operator_reward)
2698 .saturating_sub(reserve),
2699 true,
2700 );
2701
2702 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
2703
2704 let maybe_deposit = if maybe_deposit == 0 {
2706 None
2707 } else {
2708 Some(maybe_deposit)
2709 };
2710
2711 withdraw_stake_prop(WithdrawParams {
2712 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2713 nominators: vec![(0, operator_stake)],
2714 operator_reward,
2715 nominator_id: 0,
2716 withdraws: vec![(excess_stake, Ok(()))],
2717 maybe_deposit,
2718 expected_withdraw: Some(expected_withdraw),
2719 expected_nominator_count_reduced_by: 0,
2720 storage_fund_change: (true, 0),
2721 })
2722 }
2723 );
2724 }
2725
2726 #[test]
2727 fn withdraw_stake_operator_all() {
2728 withdraw_stake(WithdrawParams {
2729 minimum_nominator_stake: 10 * AI3,
2730 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2731 operator_reward: 20 * AI3,
2732 nominator_id: 0,
2733 withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
2734 maybe_deposit: None,
2735 expected_withdraw: None,
2736 expected_nominator_count_reduced_by: 0,
2737 storage_fund_change: (true, 0),
2738 })
2739 }
2740
2741 #[test]
2742 fn withdraw_stake_operator_below_minimum() {
2743 withdraw_stake(WithdrawParams {
2744 minimum_nominator_stake: 10 * AI3,
2745 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2746 operator_reward: 20 * AI3,
2747 nominator_id: 0,
2748 withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
2749 maybe_deposit: None,
2750 expected_withdraw: None,
2751 expected_nominator_count_reduced_by: 0,
2752 storage_fund_change: (true, 0),
2753 })
2754 }
2755
2756 #[test]
2757 fn withdraw_stake_operator_below_minimum_no_rewards() {
2758 withdraw_stake(WithdrawParams {
2759 minimum_nominator_stake: 10 * AI3,
2760 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2761 operator_reward: Zero::zero(),
2762 nominator_id: 0,
2763 withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
2764 maybe_deposit: None,
2765 expected_withdraw: None,
2766 expected_nominator_count_reduced_by: 0,
2767 storage_fund_change: (true, 0),
2768 })
2769 }
2770
2771 #[test]
2772 fn withdraw_stake_operator_above_minimum() {
2773 withdraw_stake(WithdrawParams {
2774 minimum_nominator_stake: 10 * AI3,
2775 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2776 operator_reward: 20 * AI3,
2777 nominator_id: 0,
2778 withdraws: vec![(58 * AI3, Ok(()))],
2779 maybe_deposit: None,
2782 expected_withdraw: Some((63523809523809523770, false)),
2783 expected_nominator_count_reduced_by: 0,
2784 storage_fund_change: (true, 0),
2785 })
2786 }
2787
2788 #[test]
2789 fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
2790 withdraw_stake(WithdrawParams {
2791 minimum_nominator_stake: 10 * AI3,
2792 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2793 operator_reward: 20 * AI3,
2794 nominator_id: 0,
2795 withdraws: vec![
2796 (58 * AI3, Ok(())),
2797 (5 * AI3, Err(StakingError::MinimumOperatorStake)),
2798 ],
2799 maybe_deposit: None,
2800 expected_withdraw: Some((63523809523809523770, false)),
2801 expected_nominator_count_reduced_by: 0,
2802 storage_fund_change: (true, 0),
2803 })
2804 }
2805
2806 #[test]
2807 fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
2808 withdraw_stake(WithdrawParams {
2809 minimum_nominator_stake: 10 * AI3,
2810 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2811 operator_reward: 20 * AI3,
2812 nominator_id: 0,
2813 withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
2814 maybe_deposit: None,
2815 expected_withdraw: Some((63523809523809523769, false)),
2816 expected_nominator_count_reduced_by: 0,
2817 storage_fund_change: (true, 0),
2818 })
2819 }
2820
2821 #[test]
2822 fn withdraw_stake_operator_above_minimum_no_rewards() {
2823 withdraw_stake(WithdrawParams {
2824 minimum_nominator_stake: 10 * AI3,
2825 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2826 operator_reward: Zero::zero(),
2827 nominator_id: 0,
2828 withdraws: vec![(49 * AI3, Ok(()))],
2829 maybe_deposit: None,
2830 expected_withdraw: Some((48999999999999999980, false)),
2831 expected_nominator_count_reduced_by: 0,
2832 storage_fund_change: (true, 0),
2833 })
2834 }
2835
2836 #[test]
2837 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
2838 withdraw_stake(WithdrawParams {
2839 minimum_nominator_stake: 10 * AI3,
2840 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2841 operator_reward: Zero::zero(),
2842 nominator_id: 0,
2843 withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
2844 maybe_deposit: None,
2845 expected_withdraw: Some((48999999999999999981, false)),
2846 expected_nominator_count_reduced_by: 0,
2847 storage_fund_change: (true, 0),
2848 })
2849 }
2850
2851 #[test]
2852 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
2853 withdraw_stake(WithdrawParams {
2854 minimum_nominator_stake: 10 * AI3,
2855 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2856 operator_reward: Zero::zero(),
2857 nominator_id: 0,
2858 withdraws: vec![
2859 (29 * AI3, Ok(())),
2860 (20 * AI3, Ok(())),
2861 (20 * AI3, Err(StakingError::MinimumOperatorStake)),
2862 ],
2863 maybe_deposit: None,
2864 expected_withdraw: Some((48999999999999999981, false)),
2865 expected_nominator_count_reduced_by: 0,
2866 storage_fund_change: (true, 0),
2867 })
2868 }
2869
2870 #[test]
2871 fn withdraw_stake_nominator_below_minimum_with_rewards() {
2872 withdraw_stake(WithdrawParams {
2873 minimum_nominator_stake: 10 * AI3,
2874 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2875 operator_reward: 20 * AI3,
2876 nominator_id: 1,
2877 withdraws: vec![(45 * AI3, Ok(()))],
2878 maybe_deposit: None,
2882 expected_withdraw: Some((54761904761904761888, true)),
2883 expected_nominator_count_reduced_by: 1,
2884 storage_fund_change: (true, 0),
2885 })
2886 }
2887
2888 #[test]
2889 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
2890 withdraw_stake(WithdrawParams {
2891 minimum_nominator_stake: 10 * AI3,
2892 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2893 operator_reward: 20 * AI3,
2894 nominator_id: 1,
2895 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2896 maybe_deposit: None,
2900 expected_withdraw: Some((54761904761904761888, true)),
2901 expected_nominator_count_reduced_by: 1,
2902 storage_fund_change: (true, 0),
2903 })
2904 }
2905
2906 #[test]
2907 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
2908 withdraw_stake(WithdrawParams {
2909 minimum_nominator_stake: 10 * AI3,
2910 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2911 operator_reward: 20 * AI3,
2912 nominator_id: 1,
2913 withdraws: vec![
2914 (25 * AI3, Ok(())),
2915 (20 * AI3, Ok(())),
2916 (20 * AI3, Err(StakingError::InsufficientShares)),
2917 ],
2918 maybe_deposit: None,
2922 expected_withdraw: Some((54761904761904761888, true)),
2923 expected_nominator_count_reduced_by: 1,
2924 storage_fund_change: (true, 0),
2925 })
2926 }
2927
2928 #[test]
2929 fn withdraw_stake_nominator_below_minimum_no_reward() {
2930 withdraw_stake(WithdrawParams {
2931 minimum_nominator_stake: 10 * AI3,
2932 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2933 operator_reward: Zero::zero(),
2934 nominator_id: 1,
2935 withdraws: vec![(45 * AI3, Ok(()))],
2936 maybe_deposit: None,
2937 expected_withdraw: Some((50 * AI3, true)),
2938 expected_nominator_count_reduced_by: 1,
2939 storage_fund_change: (true, 0),
2940 })
2941 }
2942
2943 #[test]
2944 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
2945 withdraw_stake(WithdrawParams {
2946 minimum_nominator_stake: 10 * AI3,
2947 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2948 operator_reward: Zero::zero(),
2949 nominator_id: 1,
2950 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
2951 maybe_deposit: None,
2952 expected_withdraw: Some((50 * AI3, true)),
2953 expected_nominator_count_reduced_by: 1,
2954 storage_fund_change: (true, 0),
2955 })
2956 }
2957
2958 #[test]
2959 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
2960 withdraw_stake(WithdrawParams {
2961 minimum_nominator_stake: 10 * AI3,
2962 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2963 operator_reward: Zero::zero(),
2964 nominator_id: 1,
2965 withdraws: vec![
2966 (25 * AI3, Ok(())),
2967 (20 * AI3, Ok(())),
2968 (20 * AI3, Err(StakingError::InsufficientShares)),
2969 ],
2970 maybe_deposit: None,
2971 expected_withdraw: Some((50 * AI3, true)),
2972 expected_nominator_count_reduced_by: 1,
2973 storage_fund_change: (true, 0),
2974 })
2975 }
2976
2977 #[test]
2978 fn withdraw_stake_nominator_above_minimum() {
2979 withdraw_stake(WithdrawParams {
2980 minimum_nominator_stake: 10 * AI3,
2981 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2982 operator_reward: 20 * AI3,
2983 nominator_id: 1,
2984 withdraws: vec![(40 * AI3, Ok(()))],
2985 maybe_deposit: None,
2986 expected_withdraw: Some((43809523809523809511, false)),
2987 expected_nominator_count_reduced_by: 0,
2988 storage_fund_change: (true, 0),
2989 })
2990 }
2991
2992 #[test]
2993 fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
2994 withdraw_stake(WithdrawParams {
2995 minimum_nominator_stake: 10 * AI3,
2996 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
2997 operator_reward: 20 * AI3,
2998 nominator_id: 1,
2999 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3000 maybe_deposit: None,
3001 expected_withdraw: Some((43809523809523809510, false)),
3002 expected_nominator_count_reduced_by: 0,
3003 storage_fund_change: (true, 0),
3004 })
3005 }
3006
3007 #[test]
3008 fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3009 withdraw_stake(WithdrawParams {
3010 minimum_nominator_stake: 10 * AI3,
3011 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3012 operator_reward: 20 * AI3,
3013 nominator_id: 1,
3014 withdraws: vec![
3015 (35 * AI3, Ok(())),
3016 (5 * AI3, Ok(())),
3017 (15 * AI3, Err(StakingError::InsufficientShares)),
3018 ],
3019 maybe_deposit: None,
3020 expected_withdraw: Some((43809523809523809510, false)),
3021 expected_nominator_count_reduced_by: 0,
3022 storage_fund_change: (true, 0),
3023 })
3024 }
3025
3026 #[test]
3027 fn withdraw_stake_nominator_above_minimum_no_rewards() {
3028 withdraw_stake(WithdrawParams {
3029 minimum_nominator_stake: 10 * AI3,
3030 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3031 operator_reward: Zero::zero(),
3032 nominator_id: 1,
3033 withdraws: vec![(39 * AI3, Ok(()))],
3034 maybe_deposit: None,
3035 expected_withdraw: Some((39 * AI3, false)),
3036 expected_nominator_count_reduced_by: 0,
3037 storage_fund_change: (true, 0),
3038 })
3039 }
3040
3041 #[test]
3042 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3043 withdraw_stake(WithdrawParams {
3044 minimum_nominator_stake: 10 * AI3,
3045 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3046 operator_reward: Zero::zero(),
3047 nominator_id: 1,
3048 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3049 maybe_deposit: None,
3050 expected_withdraw: Some((39999999899999999998, false)),
3051 expected_nominator_count_reduced_by: 0,
3052 storage_fund_change: (true, 0),
3053 })
3054 }
3055
3056 #[test]
3057 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3058 withdraw_stake(WithdrawParams {
3059 minimum_nominator_stake: 10 * AI3,
3060 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3061 operator_reward: Zero::zero(),
3062 nominator_id: 1,
3063 withdraws: vec![
3064 (35 * AI3, Ok(())),
3065 (5 * AI3 - 100000000000, Ok(())),
3066 (15 * AI3, Err(StakingError::InsufficientShares)),
3067 ],
3068 maybe_deposit: None,
3069 expected_withdraw: Some((39999999899999999998, false)),
3070 expected_nominator_count_reduced_by: 0,
3071 storage_fund_change: (true, 0),
3072 })
3073 }
3074
3075 #[test]
3076 fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3077 withdraw_stake(WithdrawParams {
3078 minimum_nominator_stake: 10 * AI3,
3079 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3080 operator_reward: Zero::zero(),
3081 nominator_id: 1,
3082 withdraws: vec![
3083 (35 * AI3, Ok(())),
3084 (5 * AI3 - 100000000000, Ok(())),
3085 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3086 ],
3087 maybe_deposit: Some(2 * AI3),
3088 expected_withdraw: Some((39999999899999999998, false)),
3089 expected_nominator_count_reduced_by: 0,
3090 storage_fund_change: (true, 0),
3091 })
3092 }
3093
3094 #[test]
3095 fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3096 withdraw_stake(WithdrawParams {
3097 minimum_nominator_stake: 10 * AI3,
3098 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3099 operator_reward: 20 * AI3,
3100 nominator_id: 1,
3101 withdraws: vec![
3102 (35 * AI3, Ok(())),
3103 (5 * AI3, Ok(())),
3104 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3105 ],
3106 maybe_deposit: Some(2 * AI3),
3110 expected_withdraw: Some((43809523809523809510, false)),
3111 expected_nominator_count_reduced_by: 0,
3112 storage_fund_change: (true, 0),
3113 })
3114 }
3115
3116 #[test]
3117 fn withdraw_stake_nominator_zero_amount() {
3118 withdraw_stake(WithdrawParams {
3119 minimum_nominator_stake: 10 * AI3,
3120 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3121 operator_reward: Zero::zero(),
3122 nominator_id: 1,
3123 withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3124 maybe_deposit: None,
3125 expected_withdraw: None,
3126 expected_nominator_count_reduced_by: 0,
3127 storage_fund_change: (true, 0),
3128 })
3129 }
3130
3131 #[test]
3132 fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3133 withdraw_stake(WithdrawParams {
3134 minimum_nominator_stake: 10 * AI3,
3135 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3136 operator_reward: Zero::zero(),
3137 nominator_id: 1,
3138 withdraws: vec![(50 * AI3, Ok(()))],
3139 maybe_deposit: None,
3140 storage_fund_change: (true, 21),
3143 expected_withdraw: Some((54999999999999999985, true)),
3144 expected_nominator_count_reduced_by: 1,
3145 })
3146 }
3147
3148 #[test]
3149 fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3150 withdraw_stake(WithdrawParams {
3151 minimum_nominator_stake: 10 * AI3,
3152 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3153 operator_reward: Zero::zero(),
3154 nominator_id: 1,
3155 withdraws: vec![(50 * AI3, Ok(()))],
3156 maybe_deposit: None,
3157 storage_fund_change: (false, 21),
3160 expected_withdraw: Some((44999999999999999995, true)),
3161 expected_nominator_count_reduced_by: 1,
3162 })
3163 }
3164
3165 #[test]
3166 fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3167 withdraw_stake(WithdrawParams {
3168 minimum_nominator_stake: 10 * AI3,
3169 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3170 operator_reward: Zero::zero(),
3171 nominator_id: 1,
3172 withdraws: vec![(50 * AI3, Ok(()))],
3173 maybe_deposit: None,
3174 storage_fund_change: (false, 42),
3177 expected_withdraw: Some((40 * AI3, true)),
3178 expected_nominator_count_reduced_by: 1,
3179 })
3180 }
3181
3182 #[test]
3183 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3184 withdraw_stake(WithdrawParams {
3185 minimum_nominator_stake: 10 * AI3,
3186 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3187 operator_reward: Zero::zero(),
3188 nominator_id: 1,
3189 withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3190 maybe_deposit: None,
3191 storage_fund_change: (true, 21),
3195 expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3196 expected_nominator_count_reduced_by: 0,
3197 })
3198 }
3199
3200 #[test]
3201 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3202 withdraw_stake(WithdrawParams {
3203 minimum_nominator_stake: 10 * AI3,
3204 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3205 operator_reward: Zero::zero(),
3206 nominator_id: 1,
3207 withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3208 maybe_deposit: None,
3209 storage_fund_change: (false, 21),
3213 expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3214 expected_nominator_count_reduced_by: 0,
3215 })
3216 }
3217
3218 #[test]
3219 fn unlock_multiple_withdrawals() {
3220 let domain_id = DomainId::new(0);
3221 let operator_account = 1;
3222 let operator_free_balance = 250 * AI3;
3223 let operator_stake = 200 * AI3;
3224 let pair = OperatorPair::from_seed(&[0; 32]);
3225 let nominator_account = 2;
3226 let nominator_free_balance = 150 * AI3;
3227 let nominator_stake = 100 * AI3;
3228
3229 let nominators = vec![
3230 (operator_account, (operator_free_balance, operator_stake)),
3231 (nominator_account, (nominator_free_balance, nominator_stake)),
3232 ];
3233
3234 let total_deposit = 300 * AI3;
3235 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3236 let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3237
3238 let mut ext = new_test_ext();
3239 ext.execute_with(|| {
3240 let (operator_id, _) = register_operator(
3241 domain_id,
3242 operator_account,
3243 operator_free_balance,
3244 operator_stake,
3245 10 * AI3,
3246 pair.public(),
3247 BTreeMap::from_iter(nominators),
3248 );
3249
3250 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3251 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3252 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3253
3254 let operator = Operators::<Test>::get(operator_id).unwrap();
3255 assert_eq!(operator.current_total_stake, init_total_stake);
3256 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3257 assert_eq!(
3258 operator.total_storage_fee_deposit,
3259 bundle_storage_fund::total_balance::<Test>(operator_id)
3260 );
3261
3262 let shares_per_withdraw = init_total_stake / 100;
3264 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3265
3266 for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3268 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3269 .unwrap();
3270 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3271 }
3272 HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3274
3275 for _ in 0..5 {
3278 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3279 .unwrap();
3280 }
3281 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3282
3283 assert_err!(
3285 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3286 StakingError::TooManyWithdrawals
3287 );
3288 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3289 Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3290 let withdrawal = maybe_withdrawal.as_mut().unwrap();
3291 do_convert_previous_epoch_withdrawal::<Test>(
3292 operator_id,
3293 withdrawal,
3294 domain_stake_summary.current_epoch_index,
3295 )
3296 .unwrap();
3297 assert_eq!(
3298 withdrawal.withdrawals.len() as u32,
3299 <Test as crate::Config>::WithdrawalLimit::get()
3300 );
3301 Ok::<(), StakingError>(())
3302 })
3303 .unwrap();
3304
3305 HeadDomainNumber::<Test>::set(
3307 domain_id,
3308 head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3309 );
3310 let total_balance = Balances::usable_balance(nominator_account);
3311 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3312 assert_eq!(
3313 Balances::usable_balance(nominator_account) + 74, total_balance
3315 + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3316 / 100
3317 );
3318 let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3319 assert_eq!(withdrawal.withdrawals.len(), 1);
3320
3321 HeadDomainNumber::<Test>::set(
3323 domain_id,
3324 head_domain_number
3325 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3326 + 1,
3327 );
3328 let total_balance = Balances::usable_balance(nominator_account);
3329 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3330 assert_eq!(
3331 Balances::usable_balance(nominator_account) - 2, total_balance + 5 * total_deposit / 100
3333 );
3334 assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3335 });
3336 }
3337
3338 #[test]
3339 fn slash_operator() {
3340 let domain_id = DomainId::new(0);
3341 let operator_account = 1;
3342 let operator_free_balance = 250 * AI3;
3343 let operator_stake = 200 * AI3;
3344 let operator_extra_deposit = 40 * AI3;
3345 let pair = OperatorPair::from_seed(&[0; 32]);
3346 let nominator_account = 2;
3347 let nominator_free_balance = 150 * AI3;
3348 let nominator_stake = 100 * AI3;
3349 let nominator_extra_deposit = 40 * AI3;
3350
3351 let nominators = vec![
3352 (operator_account, (operator_free_balance, operator_stake)),
3353 (nominator_account, (nominator_free_balance, nominator_stake)),
3354 ];
3355
3356 let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3357
3358 let deposits = vec![
3359 (operator_account, operator_extra_deposit),
3360 (nominator_account, nominator_extra_deposit),
3361 ];
3362
3363 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3364 let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3365
3366 let mut ext = new_test_ext();
3367 ext.execute_with(|| {
3368 let (operator_id, _) = register_operator(
3369 domain_id,
3370 operator_account,
3371 operator_free_balance,
3372 operator_stake,
3373 10 * AI3,
3374 pair.public(),
3375 BTreeMap::from_iter(nominators),
3376 );
3377
3378 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3379 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3380 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3381
3382 let operator = Operators::<Test>::get(operator_id).unwrap();
3383 assert_eq!(operator.current_total_stake, init_total_stake);
3384 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3385 assert_eq!(
3386 operator.total_storage_fee_deposit,
3387 bundle_storage_fund::total_balance::<Test>(operator_id)
3388 );
3389
3390 for unlock in &unlocking {
3391 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3392 }
3393
3394 do_reward_operators::<Test>(
3395 domain_id,
3396 OperatorRewardSource::Dummy,
3397 vec![operator_id].into_iter(),
3398 20 * AI3,
3399 )
3400 .unwrap();
3401 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3402
3403 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3405 for id in [operator_account, nominator_account] {
3406 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3407 do_convert_previous_epoch_withdrawal::<Test>(
3408 operator_id,
3409 maybe_withdrawal.as_mut().unwrap(),
3410 domain_stake_summary.current_epoch_index,
3411 )
3412 })
3413 .unwrap();
3414 }
3415
3416 let operator = Operators::<Test>::get(operator_id).unwrap();
3419 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3420 let operator_withdrawal =
3421 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3422 let nominator_withdrawal =
3423 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3424
3425 let total_deposit =
3426 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3427 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3428 + nominator_withdrawal.total_withdrawal_amount;
3429 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3430 .storage_fee_refund
3431 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3432 assert_eq!(293333333333333333336, total_deposit,);
3433 assert_eq!(21666666666666666664, total_stake_withdrawal);
3434 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3435 assert_eq!(
3436 320 * AI3,
3437 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3438 );
3439 assert_eq!(
3440 operator.total_storage_fee_deposit,
3441 bundle_storage_fund::total_balance::<Test>(operator_id)
3442 );
3443
3444 for deposit in deposits {
3445 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3446 }
3447
3448 do_mark_operators_as_slashed::<Test>(
3449 vec![operator_id],
3450 SlashedReason::InvalidBundle(1),
3451 )
3452 .unwrap();
3453
3454 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3455 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3456
3457 let operator = Operators::<Test>::get(operator_id).unwrap();
3458 assert_eq!(
3459 *operator.status::<Test>(operator_id),
3460 OperatorStatus::Slashed
3461 );
3462
3463 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3464 assert!(pending_slashes.contains(&operator_id));
3465
3466 assert_eq!(
3467 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3468 0
3469 );
3470
3471 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3472 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3473 assert_eq!(Operators::<Test>::get(operator_id), None);
3474 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3475
3476 assert_eq!(
3477 Balances::total_balance(&operator_account),
3478 operator_free_balance - operator_stake
3479 );
3480 assert_eq!(
3481 Balances::total_balance(&nominator_account),
3482 nominator_free_balance - nominator_stake
3483 );
3484
3485 assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3);
3486 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3487 });
3488 }
3489
3490 #[test]
3491 fn slash_operator_with_more_than_max_nominators_to_slash() {
3492 let domain_id = DomainId::new(0);
3493 let operator_account = 1;
3494 let operator_free_balance = 250 * AI3;
3495 let operator_stake = 200 * AI3;
3496 let operator_extra_deposit = 40 * AI3;
3497 let operator_extra_withdraw = 5 * AI3;
3498 let pair = OperatorPair::from_seed(&[0; 32]);
3499
3500 let nominator_accounts: Vec<crate::tests::AccountId> = (2..22).collect();
3501 let nominator_free_balance = 150 * AI3;
3502 let nominator_stake = 100 * AI3;
3503 let nominator_extra_deposit = 40 * AI3;
3504 let nominator_extra_withdraw = 5 * AI3;
3505
3506 let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
3507 for nominator_account in nominator_accounts.clone() {
3508 nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
3509 }
3510
3511 let last_nominator_account = nominator_accounts.last().cloned().unwrap();
3512 let unlocking = vec![
3513 (operator_account, 10 * AI3),
3514 (last_nominator_account, 10 * AI3),
3515 ];
3516
3517 let deposits = vec![
3518 (operator_account, operator_extra_deposit),
3519 (last_nominator_account, nominator_extra_deposit),
3520 ];
3521 let withdrawals = vec![
3522 (operator_account, operator_extra_withdraw),
3523 (last_nominator_account, nominator_extra_withdraw),
3524 ];
3525
3526 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
3527 * (200 + (100 * nominator_accounts.len() as u128))
3528 * AI3;
3529 let init_total_storage_fund =
3530 STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
3531
3532 let mut ext = new_test_ext();
3533 ext.execute_with(|| {
3534 let (operator_id, _) = register_operator(
3535 domain_id,
3536 operator_account,
3537 operator_free_balance,
3538 operator_stake,
3539 10 * AI3,
3540 pair.public(),
3541 BTreeMap::from_iter(nominators),
3542 );
3543
3544 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3545 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3546 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3547
3548 let operator = Operators::<Test>::get(operator_id).unwrap();
3549 assert_eq!(operator.current_total_stake, init_total_stake);
3550 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3551 assert_eq!(
3552 operator.total_storage_fee_deposit,
3553 bundle_storage_fund::total_balance::<Test>(operator_id)
3554 );
3555
3556 for unlock in &unlocking {
3557 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3558 }
3559
3560 do_reward_operators::<Test>(
3561 domain_id,
3562 OperatorRewardSource::Dummy,
3563 vec![operator_id].into_iter(),
3564 20 * AI3,
3565 )
3566 .unwrap();
3567 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3568
3569 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3571 for id in [operator_account, last_nominator_account] {
3572 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3573 do_convert_previous_epoch_withdrawal::<Test>(
3574 operator_id,
3575 maybe_withdrawal.as_mut().unwrap(),
3576 domain_stake_summary.current_epoch_index,
3577 )
3578 })
3579 .unwrap();
3580 }
3581
3582 let operator = Operators::<Test>::get(operator_id).unwrap();
3585 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3586 let operator_withdrawal =
3587 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3588 let nominator_withdrawal =
3589 Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
3590
3591 let total_deposit =
3592 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3593 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3594 + nominator_withdrawal.total_withdrawal_amount;
3595 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3596 .storage_fee_refund
3597 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3598 assert_eq!(2194772727272727272734, total_deposit,);
3599 assert_eq!(20227272727272727266, total_stake_withdrawal);
3600 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3601 assert_eq!(
3602 2220 * AI3,
3603 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3604 );
3605
3606 assert_eq!(
3607 operator.total_storage_fee_deposit,
3608 bundle_storage_fund::total_balance::<Test>(operator_id)
3609 );
3610
3611 for deposit in deposits {
3612 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3613 }
3614 for withdrawal in withdrawals {
3615 do_withdraw_stake::<Test>(
3616 operator_id,
3617 withdrawal.0,
3618 withdrawal.1,
3621 )
3622 .unwrap();
3623 }
3624
3625 do_mark_operators_as_slashed::<Test>(
3626 vec![operator_id],
3627 SlashedReason::InvalidBundle(1),
3628 )
3629 .unwrap();
3630
3631 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3632
3633 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3634 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
3635
3636 let operator = Operators::<Test>::get(operator_id).unwrap();
3637 assert_eq!(
3638 *operator.status::<Test>(operator_id),
3639 OperatorStatus::Slashed
3640 );
3641
3642 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
3643 assert!(pending_slashes.contains(&operator_id));
3644
3645 assert_eq!(
3646 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3647 0
3648 );
3649
3650 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3653 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3654 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3655
3656 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3657 assert_eq!(Operators::<Test>::get(operator_id), None);
3658 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
3659
3660 assert_eq!(
3661 Balances::total_balance(&operator_account),
3662 operator_free_balance - operator_stake
3663 );
3664 for nominator_account in nominator_accounts {
3665 assert_eq!(
3666 Balances::total_balance(&nominator_account),
3667 nominator_free_balance - nominator_stake
3668 );
3669 }
3670
3671 assert_eq!(
3672 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3673 2220 * AI3
3674 );
3675 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3676 });
3677 }
3678
3679 #[test]
3680 fn slash_operators() {
3681 let domain_id = DomainId::new(0);
3682 let operator_free_balance = 250 * AI3;
3683 let operator_stake = 200 * AI3;
3684
3685 let operator_account_1 = 1;
3686 let operator_account_2 = 2;
3687 let operator_account_3 = 3;
3688
3689 let pair_1 = OperatorPair::from_seed(&[0; 32]);
3690 let pair_2 = OperatorPair::from_seed(&[1; 32]);
3691 let pair_3 = OperatorPair::from_seed(&[2; 32]);
3692
3693 let mut ext = new_test_ext();
3694 ext.execute_with(|| {
3695 let (operator_id_1, _) = register_operator(
3696 domain_id,
3697 operator_account_1,
3698 operator_free_balance,
3699 operator_stake,
3700 10 * AI3,
3701 pair_1.public(),
3702 Default::default(),
3703 );
3704
3705 let (operator_id_2, _) = register_operator(
3706 domain_id,
3707 operator_account_2,
3708 operator_free_balance,
3709 operator_stake,
3710 10 * AI3,
3711 pair_2.public(),
3712 Default::default(),
3713 );
3714
3715 let (operator_id_3, _) = register_operator(
3716 domain_id,
3717 operator_account_3,
3718 operator_free_balance,
3719 operator_stake,
3720 10 * AI3,
3721 pair_3.public(),
3722 Default::default(),
3723 );
3724
3725 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3726 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3727 assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
3728 assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
3729 assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
3730 assert_eq!(
3731 domain_stake_summary.current_total_stake,
3732 STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
3733 );
3734 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3735 let operator = Operators::<Test>::get(operator_id).unwrap();
3736 assert_eq!(
3737 operator.total_storage_fee_deposit,
3738 STORAGE_FEE_RESERVE * operator_stake
3739 );
3740 assert_eq!(
3741 operator.total_storage_fee_deposit,
3742 bundle_storage_fund::total_balance::<Test>(operator_id)
3743 );
3744 }
3745
3746 do_mark_operators_as_slashed::<Test>(
3747 vec![operator_id_1],
3748 SlashedReason::InvalidBundle(1),
3749 )
3750 .unwrap();
3751 do_mark_operators_as_slashed::<Test>(
3752 vec![operator_id_2],
3753 SlashedReason::InvalidBundle(2),
3754 )
3755 .unwrap();
3756 do_mark_operators_as_slashed::<Test>(
3757 vec![operator_id_3],
3758 SlashedReason::InvalidBundle(3),
3759 )
3760 .unwrap();
3761
3762 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3763 assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
3764 assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
3765 assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
3766
3767 let operator = Operators::<Test>::get(operator_id_1).unwrap();
3768 assert_eq!(
3769 *operator.status::<Test>(operator_id_1),
3770 OperatorStatus::Slashed
3771 );
3772
3773 let operator = Operators::<Test>::get(operator_id_2).unwrap();
3774 assert_eq!(
3775 *operator.status::<Test>(operator_id_2),
3776 OperatorStatus::Slashed
3777 );
3778
3779 let operator = Operators::<Test>::get(operator_id_3).unwrap();
3780 assert_eq!(
3781 *operator.status::<Test>(operator_id_3),
3782 OperatorStatus::Slashed
3783 );
3784
3785 assert_eq!(
3786 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3787 0
3788 );
3789
3790 let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
3791 slashed_operators.into_iter().for_each(|_| {
3792 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
3793 });
3794
3795 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
3796 assert_eq!(Operators::<Test>::get(operator_id_1), None);
3797 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
3798 assert_eq!(Operators::<Test>::get(operator_id_2), None);
3799 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
3800 assert_eq!(Operators::<Test>::get(operator_id_3), None);
3801 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
3802
3803 assert_eq!(
3804 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3805 600 * AI3
3806 );
3807 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
3808 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3809 }
3810 });
3811 }
3812
3813 #[test]
3814 fn bundle_storage_fund_charged_and_refund_storege_fee() {
3815 let domain_id = DomainId::new(0);
3816 let operator_account = 1;
3817 let operator_free_balance = 150 * AI3;
3818 let operator_total_stake = 100 * AI3;
3819 let operator_stake = 80 * AI3;
3820 let operator_storage_fee_deposit = 20 * AI3;
3821 let pair = OperatorPair::from_seed(&[0; 32]);
3822 let nominator_account = 2;
3823
3824 let mut ext = new_test_ext();
3825 ext.execute_with(|| {
3826 let (operator_id, _) = register_operator(
3827 domain_id,
3828 operator_account,
3829 operator_free_balance,
3830 operator_total_stake,
3831 AI3,
3832 pair.public(),
3833 BTreeMap::default(),
3834 );
3835
3836 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3837 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
3838
3839 let operator = Operators::<Test>::get(operator_id).unwrap();
3840 assert_eq!(operator.current_total_stake, operator_stake);
3841 assert_eq!(operator.current_total_shares, operator_stake);
3842 assert_eq!(
3843 operator.total_storage_fee_deposit,
3844 operator_storage_fee_deposit
3845 );
3846
3847 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
3849 operator_id,
3850 (operator_storage_fee_deposit / AI3) as u32,
3852 )
3853 .unwrap();
3854 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3855 assert_err!(
3856 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
3857 bundle_storage_fund::Error::BundleStorageFeePayment
3858 );
3859
3860 do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
3862 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3863
3864 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3865 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3866
3867 Balances::set_balance(&nominator_account, 100 * AI3);
3869 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
3870 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3871
3872 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3873 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3874
3875 bundle_storage_fund::refund_storage_fee::<Test>(
3877 10 * AI3,
3878 BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
3879 )
3880 .unwrap();
3881 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
3882
3883 assert_eq!(
3885 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
3886 9 * AI3
3887 );
3888
3889 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
3890 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
3891 });
3892 }
3893
3894 #[test]
3895 fn zero_amount_deposit_and_withdraw() {
3896 let domain_id = DomainId::new(0);
3897 let operator_account = 1;
3898 let operator_free_balance = 250 * AI3;
3899 let operator_stake = 200 * AI3;
3900 let pair = OperatorPair::from_seed(&[0; 32]);
3901 let nominator_account = 2;
3902 let nominator_free_balance = 150 * AI3;
3903 let nominator_stake = 100 * AI3;
3904
3905 let nominators = vec![
3906 (operator_account, (operator_free_balance, operator_stake)),
3907 (nominator_account, (nominator_free_balance, nominator_stake)),
3908 ];
3909
3910 let total_deposit = 300 * AI3;
3911 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3912
3913 let mut ext = new_test_ext();
3914 ext.execute_with(|| {
3915 let (operator_id, _) = register_operator(
3916 domain_id,
3917 operator_account,
3918 operator_free_balance,
3919 operator_stake,
3920 10 * AI3,
3921 pair.public(),
3922 BTreeMap::from_iter(nominators),
3923 );
3924
3925 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3926 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3927 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3928
3929 assert_err!(
3931 do_nominate_operator::<Test>(operator_id, nominator_account, 0),
3932 StakingError::ZeroDeposit
3933 );
3934
3935 assert_err!(
3937 do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
3938 StakingError::ZeroWithdraw
3939 );
3940
3941 do_withdraw_stake::<Test>(
3943 operator_id,
3944 nominator_account,
3945 STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
3947 )
3948 .unwrap();
3949 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3950 });
3951 }
3952
3953 #[test]
3954 fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
3955 let domain_id = DomainId::new(0);
3956 let operator_account = 1;
3957 let operator_free_balance = 250 * AI3;
3958 let operator_stake = 200 * AI3;
3959 let pair = OperatorPair::from_seed(&[0; 32]);
3960 let nominator_account = 2;
3961 let nominator_free_balance = 150 * AI3;
3962 let nominator_stake = 100 * AI3;
3963
3964 let nominators = vec![
3965 (operator_account, (operator_free_balance, operator_stake)),
3966 (nominator_account, (nominator_free_balance, nominator_stake)),
3967 ];
3968
3969 let total_deposit = 300 * AI3;
3970 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3971
3972 let mut ext = new_test_ext();
3973 ext.execute_with(|| {
3974 let (operator_id, _) = register_operator(
3975 domain_id,
3976 operator_account,
3977 operator_free_balance,
3978 operator_stake,
3979 10 * AI3,
3980 pair.public(),
3981 BTreeMap::from_iter(nominators),
3982 );
3983
3984 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3985 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3986 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3987
3988 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
3989 do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
3991
3992 let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3994 OperatorEpochSharePrice::<Test>::remove(
3996 operator_id,
3997 DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
3998 );
3999
4000 assert_err!(
4002 do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4003 StakingError::MissingOperatorEpochSharePrice
4004 );
4005 assert_err!(
4006 do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4007 StakingError::MissingOperatorEpochSharePrice
4008 );
4009 });
4010 }
4011
4012 #[test]
4013 fn test_share_price_deposit() {
4014 let total_shares = 45 * AI3;
4015 let total_stake = 45 * AI3 + 37;
4016 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4017
4018 let to_deposit_stakes = [
4020 5,
4021 7,
4022 9,
4023 11,
4024 17,
4025 23,
4026 934,
4027 24931,
4028 349083467,
4029 2 * AI3 + 32,
4030 52 * AI3 - 4729034,
4031 2732 * AI3 - 1720,
4032 1117 * AI3 + 1839832,
4033 31232 * AI3 - 987654321,
4034 ];
4035
4036 let mut deposited_share = 0;
4037 let mut deposited_stake = 0;
4038 for to_deposit_stake in to_deposit_stakes {
4039 let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4040
4041 deposited_stake += to_deposit_stake;
4043 deposited_share += to_deposit_share;
4046
4047 let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4052
4053 assert!(total_deposited_share >= deposited_share);
4058
4059 assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4062 }
4063 }
4064
4065 #[test]
4066 fn test_share_price_withdraw() {
4067 let total_shares = 123 * AI3;
4068 let total_stake = 123 * AI3 + 13;
4069 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4070
4071 let to_withdraw_shares = [
4073 1,
4074 3,
4075 7,
4076 13,
4077 17,
4078 123,
4079 43553,
4080 546393039,
4081 15 * AI3 + 1342,
4082 2 * AI3 - 423,
4083 31 * AI3 - 1321,
4084 42 * AI3 + 4564234,
4085 7 * AI3 - 987654321,
4086 3 * AI3 + 987654321123879,
4087 ];
4088
4089 let mut withdrawn_share = 0;
4090 let mut withdrawn_stake = 0;
4091 for to_withdraw_share in to_withdraw_shares {
4092 let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4093
4094 withdrawn_share += to_withdraw_share;
4096 withdrawn_stake += to_withdraw_stake;
4099
4100 let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4105
4106 assert!(total_withdrawn_stake >= withdrawn_stake);
4110
4111 assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4114 }
4115 }
4116
4117 #[test]
4118 fn test_share_price_unlock() {
4119 let mut total_shares = 20 * AI3;
4120 let mut total_stake = 20 * AI3 + 12;
4121
4122 for to_unlock_share in [
4128 AI3 + 123,
4129 2 * AI3 - 456,
4130 3 * AI3 - 789,
4131 4 * AI3 - 123 + 456,
4132 7 * AI3 + 789 - 987654321,
4133 3 * AI3 + 987654321,
4134 ] {
4135 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4136
4137 let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4138
4139 total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4140 total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4141 }
4142 assert_eq!(total_shares, 0);
4143 }
4144}