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, DeactivatedOperators, DepositOnHold, DeregisteredOperators,
15 DomainBlockNumberFor, DomainHashingFor, Event, ExecutionReceiptOf, HoldIdentifier,
16 InvalidBundleAuthors, NominatorId, OperatorEpochSharePrice, OperatorHighestSlot, Pallet,
17 ReceiptHashFor, SlashedReason, WeightInfo,
18};
19use frame_support::traits::fungible::{Inspect, MutateHold};
20use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
21use frame_support::{PalletError, StorageDoubleMap, ensure};
22use frame_system::pallet_prelude::BlockNumberFor;
23use parity_scale_codec::{Decode, Encode};
24use scale_info::TypeInfo;
25use sp_core::{Get, sr25519};
26use sp_domains::{DomainId, EpochIndex, OperatorId, OperatorPublicKey, OperatorRewardSource};
27use sp_runtime::traits::{CheckedAdd, CheckedSub, Zero};
28use sp_runtime::{PerThing, Percent, Perquintill, Saturating, Weight};
29use sp_std::collections::btree_map::BTreeMap;
30use sp_std::collections::btree_set::BTreeSet;
31use sp_std::collections::vec_deque::VecDeque;
32use sp_std::vec::IntoIter;
33
34#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
36pub(crate) struct Deposit<Share: Copy, Balance: Copy> {
37 pub(crate) known: KnownDeposit<Share, Balance>,
38 pub(crate) pending: Option<PendingDeposit<Balance>>,
39}
40
41#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
44pub struct SharePrice(pub Perquintill);
45
46impl SharePrice {
47 pub(crate) fn new<T: Config>(
50 total_shares: T::Share,
51 total_stake: BalanceOf<T>,
52 ) -> Result<Self, Error> {
53 if total_shares > total_stake.into() {
54 Err(Error::ShareOverflow)
56 } else if total_stake.is_zero() || total_shares.is_zero() {
57 Err(Error::ZeroSharePrice)
59 } else {
60 Ok(SharePrice(Perquintill::from_rational(
61 total_shares.into(),
62 total_stake,
63 )))
64 }
65 }
66
67 pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
70 self.0.mul_floor(stake).into()
71 }
72
73 pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
76 self.0
81 .plus_epsilon()
84 .saturating_reciprocal_mul_floor(shares.into())
85 }
86
87 pub(crate) fn one() -> Self {
89 Self(Perquintill::one())
90 }
91}
92
93#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
95pub struct DomainEpoch(DomainId, EpochIndex);
96
97impl DomainEpoch {
98 pub(crate) fn deconstruct(self) -> (DomainId, EpochIndex) {
99 (self.0, self.1)
100 }
101}
102
103impl From<(DomainId, EpochIndex)> for DomainEpoch {
104 fn from((domain_id, epoch_idx): (DomainId, EpochIndex)) -> Self {
105 Self(domain_id, epoch_idx)
106 }
107}
108
109pub struct NewDeposit<Balance> {
110 pub(crate) staking: Balance,
111 pub(crate) storage_fee_deposit: Balance,
112}
113
114#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq, Default)]
116pub(crate) struct KnownDeposit<Share: Copy, Balance: Copy> {
117 pub(crate) shares: Share,
118 pub(crate) storage_fee_deposit: Balance,
119}
120
121#[derive(TypeInfo, Debug, Encode, Decode, Copy, Clone, PartialEq, Eq)]
123pub(crate) struct PendingDeposit<Balance: Copy> {
124 pub(crate) effective_domain_epoch: DomainEpoch,
125 pub(crate) amount: Balance,
126 pub(crate) storage_fee_deposit: Balance,
127}
128
129impl<Balance: Copy + CheckedAdd> PendingDeposit<Balance> {
130 fn total(&self) -> Result<Balance, Error> {
131 self.amount
132 .checked_add(&self.storage_fee_deposit)
133 .ok_or(Error::BalanceOverflow)
134 }
135}
136
137#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
139pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
140 pub(crate) total_withdrawal_amount: Balance,
143 pub(crate) total_storage_fee_withdrawal: Balance,
145 pub(crate) withdrawals: VecDeque<WithdrawalInBalance<DomainBlockNumber, Balance>>,
147 pub(crate) withdrawal_in_shares: Option<WithdrawalInShares<DomainBlockNumber, Share, Balance>>,
150}
151
152#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
153pub(crate) struct WithdrawalInBalance<DomainBlockNumber, Balance> {
154 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
155 pub(crate) amount_to_unlock: Balance,
156 pub(crate) storage_fee_refund: Balance,
157}
158
159#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
160pub(crate) struct WithdrawalInShares<DomainBlockNumber, Share, Balance> {
161 pub(crate) domain_epoch: DomainEpoch,
162 pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
163 pub(crate) shares: Share,
164 pub(crate) storage_fee_refund: Balance,
165}
166
167#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
168pub struct OperatorDeregisteredInfo<DomainBlockNumber> {
169 pub domain_epoch: DomainEpoch,
170 pub unlock_at_confirmed_domain_block_number: DomainBlockNumber,
171}
172
173impl<DomainBlockNumber> From<(DomainId, EpochIndex, DomainBlockNumber)>
174 for OperatorDeregisteredInfo<DomainBlockNumber>
175{
176 fn from(value: (DomainId, EpochIndex, DomainBlockNumber)) -> Self {
177 OperatorDeregisteredInfo {
178 domain_epoch: (value.0, value.1).into(),
179 unlock_at_confirmed_domain_block_number: value.2,
180 }
181 }
182}
183
184#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
186pub enum OperatorStatus<DomainBlockNumber, ReceiptHash> {
187 #[codec(index = 0)]
188 Registered,
189 #[codec(index = 1)]
191 Deregistered(OperatorDeregisteredInfo<DomainBlockNumber>),
192 #[codec(index = 2)]
193 Slashed,
194 #[codec(index = 3)]
195 PendingSlash,
196 #[codec(index = 4)]
197 InvalidBundle(ReceiptHash),
198 #[codec(index = 5)]
201 Deactivated(EpochIndex),
202}
203
204#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
206pub struct Operator<Balance, Share, DomainBlockNumber, ReceiptHash> {
207 pub signing_key: OperatorPublicKey,
208 pub current_domain_id: DomainId,
209 pub next_domain_id: DomainId,
210 pub minimum_nominator_stake: Balance,
211 pub nomination_tax: Percent,
212 pub current_total_stake: Balance,
214 pub current_total_shares: Share,
216 partial_status: OperatorStatus<DomainBlockNumber, ReceiptHash>,
220 pub deposits_in_epoch: Balance,
222 pub withdrawals_in_epoch: Share,
224 pub total_storage_fee_deposit: Balance,
226}
227
228impl<Balance, Share, DomainBlockNumber, ReceiptHash>
229 Operator<Balance, Share, DomainBlockNumber, ReceiptHash>
230{
231 pub fn status<T: Config>(
232 &self,
233 operator_id: OperatorId,
234 ) -> &OperatorStatus<DomainBlockNumber, ReceiptHash> {
235 if matches!(self.partial_status, OperatorStatus::Slashed) {
236 &OperatorStatus::Slashed
237 } else if Pallet::<T>::is_operator_pending_to_slash(self.current_domain_id, operator_id) {
238 &OperatorStatus::PendingSlash
239 } else {
240 &self.partial_status
241 }
242 }
243
244 pub fn update_status(&mut self, new_status: OperatorStatus<DomainBlockNumber, ReceiptHash>) {
245 self.partial_status = new_status;
246 }
247
248 pub fn is_operator_registered_or_deactivated<T: Config>(
250 &self,
251 operator_id: OperatorId,
252 ) -> bool {
253 let status = self.status::<T>(operator_id);
254 matches!(
255 status,
256 OperatorStatus::Registered | OperatorStatus::Deactivated(_)
257 )
258 }
259}
260
261#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
262pub struct StakingSummary<OperatorId, Balance> {
263 pub current_epoch_index: EpochIndex,
265 pub current_total_stake: Balance,
267 pub current_operators: BTreeMap<OperatorId, Balance>,
269 pub next_operators: BTreeSet<OperatorId>,
271 pub current_epoch_rewards: BTreeMap<OperatorId, Balance>,
273}
274
275#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
276pub struct OperatorConfig<Balance> {
277 pub signing_key: OperatorPublicKey,
278 pub minimum_nominator_stake: Balance,
279 pub nomination_tax: Percent,
280}
281
282#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
283pub enum Error {
284 MaximumOperatorId,
285 DomainNotInitialized,
286 PendingOperatorSwitch,
287 InsufficientBalance,
288 InsufficientShares,
289 ZeroWithdraw,
290 BalanceFreeze,
291 MinimumOperatorStake,
292 UnknownOperator,
293 MinimumNominatorStake,
294 BalanceOverflow,
295 BalanceUnderflow,
296 NotOperatorOwner,
297 OperatorNotRegistered,
298 OperatorNotDeactivated,
299 UnknownNominator,
300 MissingOperatorOwner,
301 MintBalance,
302 BlockNumberOverflow,
303 RemoveLock,
304 EpochOverflow,
305 ShareUnderflow,
306 ShareOverflow,
307 TooManyPendingStakingOperation,
308 OperatorNotAllowed,
309 InvalidOperatorSigningKey,
310 MissingOperatorEpochSharePrice,
311 MissingWithdrawal,
312 EpochNotComplete,
313 UnlockPeriodNotComplete,
314 OperatorNotDeregistered,
315 BundleStorageFund(bundle_storage_fund::Error),
316 UnconfirmedER,
317 TooManyWithdrawals,
318 ZeroDeposit,
319 ZeroSharePrice,
320 ReactivationDelayPeriodIncomplete,
321 OperatorNotRegisterdOrDeactivated,
322}
323
324fn note_pending_staking_operation<T: Config>(domain_id: DomainId) -> Result<(), Error> {
327 let pending_op_count = PendingStakingOperationCount::<T>::get(domain_id);
328
329 ensure!(
330 pending_op_count < T::MaxPendingStakingOperation::get(),
331 Error::TooManyPendingStakingOperation
332 );
333
334 PendingStakingOperationCount::<T>::set(domain_id, pending_op_count.saturating_add(1));
335
336 Ok(())
337}
338
339pub fn do_register_operator<T: Config>(
340 operator_owner: T::AccountId,
341 domain_id: DomainId,
342 amount: BalanceOf<T>,
343 config: OperatorConfig<BalanceOf<T>>,
344) -> Result<(OperatorId, EpochIndex), Error> {
345 note_pending_staking_operation::<T>(domain_id)?;
346
347 DomainStakingSummary::<T>::try_mutate(domain_id, |maybe_domain_stake_summary| {
348 ensure!(
349 config.signing_key != OperatorPublicKey::from(sr25519::Public::default()),
350 Error::InvalidOperatorSigningKey
351 );
352
353 ensure!(
354 config.minimum_nominator_stake >= T::MinNominatorStake::get(),
355 Error::MinimumNominatorStake
356 );
357
358 let domain_obj = DomainRegistry::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
359 ensure!(
360 domain_obj
361 .domain_config
362 .operator_allow_list
363 .is_operator_allowed(&operator_owner),
364 Error::OperatorNotAllowed
365 );
366
367 let operator_id = NextOperatorId::<T>::get();
368 let next_operator_id = operator_id.checked_add(1).ok_or(Error::MaximumOperatorId)?;
369 NextOperatorId::<T>::set(next_operator_id);
370
371 OperatorIdOwner::<T>::insert(operator_id, operator_owner.clone());
372
373 ensure!(
375 amount >= T::MinOperatorStake::get(),
376 Error::MinimumOperatorStake
377 );
378
379 let new_deposit =
380 deposit_reserve_for_storage_fund::<T>(operator_id, &operator_owner, amount)
381 .map_err(Error::BundleStorageFund)?;
382
383 hold_deposit::<T>(&operator_owner, operator_id, new_deposit.staking)?;
384
385 let domain_stake_summary = maybe_domain_stake_summary
386 .as_mut()
387 .ok_or(Error::DomainNotInitialized)?;
388
389 let OperatorConfig {
390 signing_key,
391 minimum_nominator_stake,
392 nomination_tax,
393 } = config;
394
395 let first_share_price = SharePrice::one();
404 let operator = Operator {
405 signing_key: signing_key.clone(),
406 current_domain_id: domain_id,
407 next_domain_id: domain_id,
408 minimum_nominator_stake,
409 nomination_tax,
410 current_total_stake: new_deposit.staking,
411 current_total_shares: first_share_price.stake_to_shares::<T>(new_deposit.staking),
412 partial_status: OperatorStatus::Registered,
413 deposits_in_epoch: Zero::zero(),
415 withdrawals_in_epoch: Zero::zero(),
416 total_storage_fee_deposit: new_deposit.storage_fee_deposit,
417 };
418 Operators::<T>::insert(operator_id, operator);
419 OperatorEpochSharePrice::<T>::insert(
420 operator_id,
421 DomainEpoch::from((domain_id, domain_stake_summary.current_epoch_index)),
422 first_share_price,
423 );
424
425 domain_stake_summary.next_operators.insert(operator_id);
427 let current_domain_epoch = (domain_id, domain_stake_summary.current_epoch_index).into();
429 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
430 operator_id,
431 operator_owner,
432 current_domain_epoch,
433 new_deposit,
434 None,
435 )?;
436
437 Ok((operator_id, domain_stake_summary.current_epoch_index))
438 })
439}
440
441pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T: Config>(
446 operator_id: OperatorId,
447 nominator_id: NominatorId<T>,
448 current_domain_epoch: DomainEpoch,
449 new_deposit: NewDeposit<BalanceOf<T>>,
450 required_minimum_nominator_stake: Option<BalanceOf<T>>,
451) -> Result<(), Error> {
452 Deposits::<T>::try_mutate(operator_id, nominator_id, |maybe_deposit| {
453 let mut deposit = maybe_deposit.take().unwrap_or_default();
454 do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, current_domain_epoch.1)?;
455
456 let pending_deposit = match deposit.pending {
458 None => PendingDeposit {
459 effective_domain_epoch: current_domain_epoch,
460 amount: new_deposit.staking,
461 storage_fee_deposit: new_deposit.storage_fee_deposit,
462 },
463 Some(pending_deposit) => PendingDeposit {
464 effective_domain_epoch: current_domain_epoch,
465 amount: pending_deposit
466 .amount
467 .checked_add(&new_deposit.staking)
468 .ok_or(Error::BalanceOverflow)?,
469 storage_fee_deposit: pending_deposit
470 .storage_fee_deposit
471 .checked_add(&new_deposit.storage_fee_deposit)
472 .ok_or(Error::BalanceOverflow)?,
473 },
474 };
475
476 if deposit.known.shares.is_zero()
477 && let Some(minimum_nominator_stake) = required_minimum_nominator_stake
478 {
479 ensure!(
480 pending_deposit.total()? >= minimum_nominator_stake,
481 Error::MinimumNominatorStake
482 );
483 }
484
485 deposit.pending = Some(pending_deposit);
486 *maybe_deposit = Some(deposit);
487 Ok(())
488 })
489}
490
491pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
492 operator_id: OperatorId,
493 deposit: &mut Deposit<T::Share, BalanceOf<T>>,
494 current_domain_epoch_index: EpochIndex,
495) -> Result<(), Error> {
496 let epoch_share_price = match deposit.pending {
498 None => return Ok(()),
499 Some(pending_deposit) => {
500 match OperatorEpochSharePrice::<T>::get(
501 operator_id,
502 pending_deposit.effective_domain_epoch,
503 ) {
504 Some(p) => p,
505 None => {
506 ensure!(
507 pending_deposit.effective_domain_epoch.1 >= current_domain_epoch_index,
508 Error::MissingOperatorEpochSharePrice
509 );
510 return Ok(());
511 }
512 }
513 }
514 };
515
516 if let Some(PendingDeposit {
517 amount,
518 storage_fee_deposit,
519 ..
520 }) = deposit.pending.take()
521 {
522 let new_shares = epoch_share_price.stake_to_shares::<T>(amount);
523 deposit.known.shares = deposit
524 .known
525 .shares
526 .checked_add(&new_shares)
527 .ok_or(Error::ShareOverflow)?;
528 deposit.known.storage_fee_deposit = deposit
529 .known
530 .storage_fee_deposit
531 .checked_add(&storage_fee_deposit)
532 .ok_or(Error::BalanceOverflow)?;
533 }
534
535 Ok(())
536}
537
538pub(crate) fn do_convert_previous_epoch_withdrawal<T: Config>(
544 operator_id: OperatorId,
545 withdrawal: &mut Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
546 current_domain_epoch_index: EpochIndex,
547) -> Result<(), Error> {
548 let epoch_share_price = match withdrawal.withdrawal_in_shares.as_ref() {
549 None => return Ok(()),
550 Some(withdraw) => {
551 if withdraw.domain_epoch.1 >= current_domain_epoch_index {
553 return Ok(());
554 }
555
556 match OperatorEpochSharePrice::<T>::get(operator_id, withdraw.domain_epoch) {
557 Some(p) => p,
558 None => return Err(Error::MissingOperatorEpochSharePrice),
559 }
560 }
561 };
562
563 if let Some(WithdrawalInShares {
564 unlock_at_confirmed_domain_block_number,
565 shares,
566 storage_fee_refund,
567 domain_epoch: _,
568 }) = withdrawal.withdrawal_in_shares.take()
569 {
570 let withdrawal_amount = epoch_share_price.shares_to_stake::<T>(shares);
571 withdrawal.total_withdrawal_amount = withdrawal
572 .total_withdrawal_amount
573 .checked_add(&withdrawal_amount)
574 .ok_or(Error::BalanceOverflow)?;
575
576 let withdraw_in_balance = WithdrawalInBalance {
577 unlock_at_confirmed_domain_block_number,
578 amount_to_unlock: withdrawal_amount,
579 storage_fee_refund,
580 };
581 withdrawal.withdrawals.push_back(withdraw_in_balance);
582 }
583
584 Ok(())
585}
586
587pub(crate) fn do_nominate_operator<T: Config>(
588 operator_id: OperatorId,
589 nominator_id: T::AccountId,
590 amount: BalanceOf<T>,
591) -> Result<(), Error> {
592 ensure!(!amount.is_zero(), Error::ZeroDeposit);
593
594 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
595 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
596
597 ensure!(
598 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
599 Error::OperatorNotRegistered
600 );
601
602 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
604 note_pending_staking_operation::<T>(operator.current_domain_id)?;
605 }
606
607 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
608 .ok_or(Error::DomainNotInitialized)?;
609
610 let new_deposit = deposit_reserve_for_storage_fund::<T>(operator_id, &nominator_id, amount)
612 .map_err(Error::BundleStorageFund)?;
613
614 hold_deposit::<T>(&nominator_id, operator_id, new_deposit.staking)?;
615 Pallet::<T>::deposit_event(Event::OperatorNominated {
616 operator_id,
617 nominator_id: nominator_id.clone(),
618 amount: new_deposit.staking,
619 });
620
621 operator.deposits_in_epoch = operator
623 .deposits_in_epoch
624 .checked_add(&new_deposit.staking)
625 .ok_or(Error::BalanceOverflow)?;
626
627 operator.total_storage_fee_deposit = operator
629 .total_storage_fee_deposit
630 .checked_add(&new_deposit.storage_fee_deposit)
631 .ok_or(Error::BalanceOverflow)?;
632
633 let current_domain_epoch = (
634 operator.current_domain_id,
635 domain_stake_summary.current_epoch_index,
636 )
637 .into();
638
639 do_calculate_previous_epoch_deposit_shares_and_add_new_deposit::<T>(
640 operator_id,
641 nominator_id,
642 current_domain_epoch,
643 new_deposit,
644 Some(operator.minimum_nominator_stake),
645 )?;
646
647 Ok(())
648 })
649}
650
651pub(crate) fn hold_deposit<T: Config>(
652 who: &T::AccountId,
653 operator_id: OperatorId,
654 amount: BalanceOf<T>,
655) -> Result<(), Error> {
656 ensure!(
658 T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite) >= amount,
659 Error::InsufficientBalance
660 );
661
662 DepositOnHold::<T>::try_mutate((operator_id, who), |deposit_on_hold| {
663 *deposit_on_hold = deposit_on_hold
664 .checked_add(&amount)
665 .ok_or(Error::BalanceOverflow)?;
666 Ok(())
667 })?;
668
669 let pending_deposit_hold_id = T::HoldIdentifier::staking_staked();
670 T::Currency::hold(&pending_deposit_hold_id, who, amount).map_err(|_| Error::BalanceFreeze)?;
671
672 Ok(())
673}
674
675pub(crate) fn do_deregister_operator<T: Config>(
678 operator_owner: T::AccountId,
679 operator_id: OperatorId,
680) -> Result<Weight, Error> {
681 ensure!(
682 OperatorIdOwner::<T>::get(operator_id) == Some(operator_owner),
683 Error::NotOperatorOwner
684 );
685
686 let mut weight = T::WeightInfo::deregister_operator();
687 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
688 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
689
690 ensure!(
691 operator.is_operator_registered_or_deactivated::<T>(operator_id),
692 Error::OperatorNotRegisterdOrDeactivated
693 );
694
695 DomainStakingSummary::<T>::try_mutate(
696 operator.current_domain_id,
697 |maybe_domain_stake_summary| {
698 let stake_summary = maybe_domain_stake_summary
699 .as_mut()
700 .ok_or(Error::DomainNotInitialized)?;
701
702 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
703 let unlock_operator_at_domain_block_number = head_domain_number
704 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
705 .ok_or(Error::BlockNumberOverflow)?;
706 let operator_deregister_info = (
707 operator.current_domain_id,
708 stake_summary.current_epoch_index,
709 unlock_operator_at_domain_block_number,
710 )
711 .into();
712
713 if matches!(operator.partial_status, OperatorStatus::Deactivated(_)) {
717 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
718 operators.remove(&operator_id);
719 });
720 weight = T::WeightInfo::deregister_deactivated_operator();
721 }
722
723 operator.update_status(OperatorStatus::Deregistered(operator_deregister_info));
724
725 stake_summary.next_operators.remove(&operator_id);
726
727 DeregisteredOperators::<T>::mutate(operator.current_domain_id, |operators| {
728 operators.insert(operator_id)
729 });
730 Ok(())
731 },
732 )
733 })?;
734
735 Ok(weight)
736}
737
738pub(crate) fn do_deactivate_operator<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
743 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
744 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
745
746 ensure!(
747 *operator.status::<T>(operator_id) == OperatorStatus::Registered,
748 Error::OperatorNotRegistered
749 );
750
751 DomainStakingSummary::<T>::try_mutate(
752 operator.current_domain_id,
753 |maybe_domain_stake_summary| {
754 let stake_summary = maybe_domain_stake_summary
755 .as_mut()
756 .ok_or(Error::DomainNotInitialized)?;
757
758 let current_epoch = stake_summary.current_epoch_index;
759 let reactivation_delay = current_epoch
760 .checked_add(T::OperatorActivationDelayInEpochs::get())
761 .ok_or(Error::EpochOverflow)?;
762
763 operator.update_status(OperatorStatus::Deactivated(reactivation_delay));
764
765 if stake_summary
769 .current_operators
770 .remove(&operator_id)
771 .is_some()
772 {
773 stake_summary.current_total_stake = stake_summary
774 .current_total_stake
775 .checked_sub(&operator.current_total_stake)
776 .ok_or(Error::BalanceUnderflow)?;
777 }
778 stake_summary.next_operators.remove(&operator_id);
779
780 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
781 operators.insert(operator_id)
782 });
783 Pallet::<T>::deposit_event(Event::OperatorDeactivated {
784 domain_id: operator.current_domain_id,
785 operator_id,
786 reactivation_delay,
787 });
788
789 Ok(())
790 },
791 )
792 })
793}
794
795pub(crate) fn do_reactivate_operator<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
798 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
799 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
800 let operator_status = operator.status::<T>(operator_id);
801 let reactivation_delay =
802 if let OperatorStatus::Deactivated(reactivation_delay) = operator_status {
803 *reactivation_delay
804 } else {
805 return Err(Error::OperatorNotDeactivated);
806 };
807
808 DomainStakingSummary::<T>::try_mutate(
809 operator.current_domain_id,
810 |maybe_domain_stake_summary| {
811 let stake_summary = maybe_domain_stake_summary
812 .as_mut()
813 .ok_or(Error::DomainNotInitialized)?;
814
815 let current_epoch = stake_summary.current_epoch_index;
816 ensure!(
817 current_epoch >= reactivation_delay,
818 Error::ReactivationDelayPeriodIncomplete
819 );
820
821 operator.update_status(OperatorStatus::Registered);
822 stake_summary.next_operators.insert(operator_id);
823
824 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
828 operators.remove(&operator_id);
829 });
830
831 Pallet::<T>::deposit_event(Event::OperatorReactivated {
832 domain_id: operator.current_domain_id,
833 operator_id,
834 });
835
836 Ok(())
837 },
838 )
839 })
840}
841
842pub(crate) fn current_share_price<T: Config>(
845 operator_id: OperatorId,
846 operator: &Operator<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
847 domain_stake_summary: &StakingSummary<OperatorId, BalanceOf<T>>,
848) -> Result<SharePrice, Error> {
849 let total_stake = domain_stake_summary
851 .current_epoch_rewards
852 .get(&operator_id)
853 .and_then(|rewards| {
854 let operator_tax = operator.nomination_tax.mul_floor(*rewards);
855 operator
856 .current_total_stake
857 .checked_add(rewards)?
858 .checked_sub(&operator_tax)
860 })
861 .unwrap_or(operator.current_total_stake);
862
863 SharePrice::new::<T>(operator.current_total_shares, total_stake)
864}
865
866pub(crate) fn do_withdraw_stake<T: Config>(
875 operator_id: OperatorId,
876 nominator_id: NominatorId<T>,
877 to_withdraw: T::Share,
878) -> Result<Weight, Error> {
879 ensure!(!to_withdraw.is_zero(), Error::ZeroWithdraw);
882
883 let mut weight = T::WeightInfo::withdraw_stake();
884 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
885 let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?;
886 ensure!(
887 operator.is_operator_registered_or_deactivated::<T>(operator_id),
888 Error::OperatorNotRegisterdOrDeactivated
889 );
890
891 if operator.deposits_in_epoch.is_zero() && operator.withdrawals_in_epoch.is_zero() {
893 note_pending_staking_operation::<T>(operator.current_domain_id)?;
894 }
895
896 let domain_stake_summary = DomainStakingSummary::<T>::get(operator.current_domain_id)
898 .ok_or(Error::DomainNotInitialized)?;
899 let domain_current_epoch = (
900 operator.current_domain_id,
901 domain_stake_summary.current_epoch_index,
902 )
903 .into();
904
905 let known_shares =
906 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
907 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
908 do_convert_previous_epoch_deposits::<T>(
909 operator_id,
910 deposit,
911 domain_stake_summary.current_epoch_index,
912 )?;
913 Ok(deposit.known.shares)
914 })?;
915
916 Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
917 if let Some(withdrawal) = maybe_withdrawal {
918 do_convert_previous_epoch_withdrawal::<T>(
919 operator_id,
920 withdrawal,
921 domain_stake_summary.current_epoch_index,
922 )?;
923 if withdrawal.withdrawals.len() as u32 >= T::WithdrawalLimit::get() {
924 return Err(Error::TooManyWithdrawals);
925 }
926 }
927 Ok(())
928 })?;
929
930 if matches!(operator.partial_status, OperatorStatus::Deactivated(_)) {
933 DeactivatedOperators::<T>::mutate(operator.current_domain_id, |operators| {
934 operators.insert(operator_id)
935 });
936
937 weight = T::WeightInfo::withdraw_stake_from_deactivated_operator();
938 }
939
940 let operator_owner =
941 OperatorIdOwner::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
942
943 let is_operator_owner = operator_owner == nominator_id;
944
945 Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
946 let deposit = maybe_deposit.as_mut().ok_or(Error::UnknownNominator)?;
947
948 let (remaining_shares, shares_withdrew) = {
949 let remaining_shares = known_shares
950 .checked_sub(&to_withdraw)
951 .ok_or(Error::InsufficientShares)?;
952
953 if remaining_shares.is_zero() {
955 if is_operator_owner {
956 return Err(Error::MinimumOperatorStake);
957 }
958
959 (remaining_shares, to_withdraw)
960 } else {
961 let share_price =
962 current_share_price::<T>(operator_id, operator, &domain_stake_summary)?;
963
964 let remaining_storage_fee =
965 Perquintill::from_rational(remaining_shares.into(), known_shares.into())
966 .mul_floor(deposit.known.storage_fee_deposit);
967
968 let remaining_stake = share_price
969 .shares_to_stake::<T>(remaining_shares)
970 .checked_add(&remaining_storage_fee)
971 .ok_or(Error::BalanceOverflow)?;
972
973 if is_operator_owner && remaining_stake.lt(&T::MinOperatorStake::get()) {
976 return Err(Error::MinimumOperatorStake);
977 }
978
979 if !is_operator_owner && remaining_stake.lt(&operator.minimum_nominator_stake) {
981 (T::Share::zero(), known_shares)
982 } else {
983 (remaining_shares, to_withdraw)
984 }
985 }
986 };
987
988 let storage_fee_to_withdraw =
991 Perquintill::from_rational(shares_withdrew.into(), known_shares.into())
992 .mul_floor(deposit.known.storage_fee_deposit);
993
994 let withdraw_storage_fee = {
995 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
996 operator_id,
997 operator.total_storage_fee_deposit,
998 );
999 bundle_storage_fund::withdraw_and_hold::<T>(
1000 operator_id,
1001 &nominator_id,
1002 storage_fund_redeem_price.redeem(storage_fee_to_withdraw),
1003 )
1004 .map_err(Error::BundleStorageFund)?
1005 };
1006
1007 deposit.known.storage_fee_deposit = deposit
1008 .known
1009 .storage_fee_deposit
1010 .checked_sub(&storage_fee_to_withdraw)
1011 .ok_or(Error::BalanceOverflow)?;
1012
1013 operator.total_storage_fee_deposit = operator
1014 .total_storage_fee_deposit
1015 .checked_sub(&storage_fee_to_withdraw)
1016 .ok_or(Error::BalanceOverflow)?;
1017
1018 operator.withdrawals_in_epoch = operator
1020 .withdrawals_in_epoch
1021 .checked_add(&shares_withdrew)
1022 .ok_or(Error::ShareOverflow)?;
1023
1024 deposit.known.shares = remaining_shares;
1025 if remaining_shares.is_zero()
1026 && let Some(pending_deposit) = deposit.pending
1027 {
1028 ensure!(
1031 pending_deposit.total()? >= operator.minimum_nominator_stake,
1032 Error::MinimumNominatorStake
1033 );
1034 }
1035
1036 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
1037 let unlock_at_confirmed_domain_block_number = head_domain_number
1038 .checked_add(&T::StakeWithdrawalLockingPeriod::get())
1039 .ok_or(Error::BlockNumberOverflow)?;
1040
1041 Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
1042 let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
1043 let new_withdrawal_in_shares = match withdrawal.withdrawal_in_shares.take() {
1046 Some(WithdrawalInShares {
1047 shares,
1048 storage_fee_refund,
1049 ..
1050 }) => WithdrawalInShares {
1051 domain_epoch: domain_current_epoch,
1052 shares: shares
1053 .checked_add(&shares_withdrew)
1054 .ok_or(Error::ShareOverflow)?,
1055 unlock_at_confirmed_domain_block_number,
1056 storage_fee_refund: storage_fee_refund
1057 .checked_add(&withdraw_storage_fee)
1058 .ok_or(Error::BalanceOverflow)?,
1059 },
1060 None => WithdrawalInShares {
1061 domain_epoch: domain_current_epoch,
1062 unlock_at_confirmed_domain_block_number,
1063 shares: shares_withdrew,
1064 storage_fee_refund: withdraw_storage_fee,
1065 },
1066 };
1067 withdrawal.withdrawal_in_shares = Some(new_withdrawal_in_shares);
1068 withdrawal.total_storage_fee_withdrawal = withdrawal
1069 .total_storage_fee_withdrawal
1070 .checked_add(&withdraw_storage_fee)
1071 .ok_or(Error::BalanceOverflow)?;
1072
1073 *maybe_withdrawal = Some(withdrawal);
1074 Ok(())
1075 })
1076 })
1077 })?;
1078
1079 Ok(weight)
1080}
1081
1082pub(crate) fn do_unlock_funds<T: Config>(
1086 operator_id: OperatorId,
1087 nominator_id: NominatorId<T>,
1088) -> Result<u32, Error> {
1089 let operator = Operators::<T>::get(operator_id).ok_or(Error::UnknownOperator)?;
1090 ensure!(
1091 operator.is_operator_registered_or_deactivated::<T>(operator_id),
1092 Error::OperatorNotRegisterdOrDeactivated
1093 );
1094
1095 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1096 .ok_or(Error::DomainNotInitialized)?
1097 .current_epoch_index;
1098
1099 Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
1100 let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
1101 do_convert_previous_epoch_withdrawal::<T>(
1102 operator_id,
1103 withdrawal,
1104 current_domain_epoch_index,
1105 )?;
1106
1107 ensure!(!withdrawal.withdrawals.is_empty(), Error::MissingWithdrawal);
1108
1109 let head_domain_number = HeadDomainNumber::<T>::get(operator.current_domain_id);
1110
1111 let mut withdrawal_count = 0;
1112 let mut total_unlocked_amount = BalanceOf::<T>::zero();
1113 let mut total_storage_fee_refund = BalanceOf::<T>::zero();
1114 loop {
1115 if withdrawal
1116 .withdrawals
1117 .front()
1118 .map(|w| w.unlock_at_confirmed_domain_block_number > head_domain_number)
1119 .unwrap_or(true)
1120 {
1121 break;
1122 }
1123
1124 let WithdrawalInBalance {
1125 amount_to_unlock,
1126 storage_fee_refund,
1127 ..
1128 } = withdrawal
1129 .withdrawals
1130 .pop_front()
1131 .expect("Must not empty as checked above; qed");
1132
1133 total_unlocked_amount = total_unlocked_amount
1134 .checked_add(&amount_to_unlock)
1135 .ok_or(Error::BalanceOverflow)?;
1136
1137 total_storage_fee_refund = total_storage_fee_refund
1138 .checked_add(&storage_fee_refund)
1139 .ok_or(Error::BalanceOverflow)?;
1140
1141 withdrawal_count += 1;
1142 }
1143
1144 ensure!(
1147 !total_unlocked_amount.is_zero() || !total_storage_fee_refund.is_zero(),
1148 Error::UnlockPeriodNotComplete
1149 );
1150
1151 withdrawal.total_withdrawal_amount = withdrawal
1153 .total_withdrawal_amount
1154 .checked_sub(&total_unlocked_amount)
1155 .ok_or(Error::BalanceUnderflow)?;
1156
1157 withdrawal.total_storage_fee_withdrawal = withdrawal
1158 .total_storage_fee_withdrawal
1159 .checked_sub(&total_storage_fee_refund)
1160 .ok_or(Error::BalanceUnderflow)?;
1161
1162 let (amount_to_mint, amount_to_release) = DepositOnHold::<T>::try_mutate(
1165 (operator_id, nominator_id.clone()),
1166 |deposit_on_hold| {
1167 let amount_to_release = total_unlocked_amount.min(*deposit_on_hold);
1168 let amount_to_mint = total_unlocked_amount.saturating_sub(*deposit_on_hold);
1169
1170 *deposit_on_hold = deposit_on_hold.saturating_sub(amount_to_release);
1171
1172 Ok((amount_to_mint, amount_to_release))
1173 },
1174 )?;
1175
1176 if !amount_to_mint.is_zero() {
1178 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1179 }
1180 if !amount_to_release.is_zero() {
1182 let staked_hold_id = T::HoldIdentifier::staking_staked();
1183 T::Currency::release(
1184 &staked_hold_id,
1185 &nominator_id,
1186 amount_to_release,
1187 Precision::Exact,
1188 )
1189 .map_err(|_| Error::RemoveLock)?;
1190 }
1191
1192 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1193 operator_id,
1194 nominator_id: nominator_id.clone(),
1195 unlocked_amount: total_unlocked_amount,
1196 });
1197
1198 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1200 T::Currency::release(
1201 &storage_fund_hold_id,
1202 &nominator_id,
1203 total_storage_fee_refund,
1204 Precision::Exact,
1205 )
1206 .map_err(|_| Error::RemoveLock)?;
1207
1208 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1209 operator_id,
1210 nominator_id: nominator_id.clone(),
1211 storage_fee: total_storage_fee_refund,
1212 });
1213
1214 if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
1216 *maybe_withdrawal = None;
1217 Deposits::<T>::mutate_exists(operator_id, nominator_id.clone(), |maybe_deposit| {
1219 if let Some(deposit) = maybe_deposit
1220 && deposit.known.shares.is_zero()
1221 && deposit.pending.is_none()
1222 {
1223 *maybe_deposit = None;
1224
1225 DepositOnHold::<T>::mutate_exists(
1226 (operator_id, nominator_id),
1227 |maybe_deposit_on_hold| {
1228 if let Some(deposit_on_hold) = maybe_deposit_on_hold
1229 && deposit_on_hold.is_zero()
1230 {
1231 *maybe_deposit_on_hold = None
1232 }
1233 },
1234 );
1235 }
1236 });
1237 }
1238
1239 Ok(withdrawal_count)
1240 })
1241}
1242
1243pub(crate) fn do_unlock_nominator<T: Config>(
1245 operator_id: OperatorId,
1246 nominator_id: NominatorId<T>,
1247) -> Result<(), Error> {
1248 Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
1249 let mut operator = maybe_operator.take().ok_or(Error::UnknownOperator)?;
1251 let OperatorDeregisteredInfo {
1252 domain_epoch,
1253 unlock_at_confirmed_domain_block_number,
1254 } = match operator.status::<T>(operator_id) {
1255 OperatorStatus::Deregistered(operator_deregistered_info) => operator_deregistered_info,
1256 _ => return Err(Error::OperatorNotDeregistered),
1257 };
1258
1259 let (domain_id, _) = domain_epoch.deconstruct();
1260 let head_domain_number = HeadDomainNumber::<T>::get(domain_id);
1261 ensure!(
1262 *unlock_at_confirmed_domain_block_number <= head_domain_number,
1263 Error::UnlockPeriodNotComplete
1264 );
1265
1266 let current_domain_epoch_index = DomainStakingSummary::<T>::get(operator.current_domain_id)
1267 .ok_or(Error::DomainNotInitialized)?
1268 .current_epoch_index;
1269
1270 let mut total_shares = operator.current_total_shares;
1271 let mut total_stake = operator.current_total_stake;
1272 let share_price = SharePrice::new::<T>(total_shares, total_stake)?;
1273
1274 let mut total_storage_fee_deposit = operator.total_storage_fee_deposit;
1275 let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::<T>(
1276 operator_id,
1277 total_storage_fee_deposit,
1278 );
1279 let mut deposit = Deposits::<T>::take(operator_id, nominator_id.clone())
1280 .ok_or(Error::UnknownNominator)?;
1281
1282 do_convert_previous_epoch_deposits::<T>(
1289 operator_id,
1290 &mut deposit,
1291 current_domain_epoch_index,
1292 )?;
1293
1294 let (
1297 amount_ready_to_withdraw,
1298 total_storage_fee_withdrawal,
1299 shares_withdrew_in_current_epoch,
1300 ) = Withdrawals::<T>::take(operator_id, nominator_id.clone())
1301 .map(|mut withdrawal| {
1302 do_convert_previous_epoch_withdrawal::<T>(
1307 operator_id,
1308 &mut withdrawal,
1309 current_domain_epoch_index,
1310 )?;
1311 Ok((
1312 withdrawal.total_withdrawal_amount,
1313 withdrawal.total_storage_fee_withdrawal,
1314 withdrawal
1315 .withdrawal_in_shares
1316 .map(|WithdrawalInShares { shares, .. }| shares)
1317 .unwrap_or_default(),
1318 ))
1319 })
1320 .unwrap_or(Ok((Zero::zero(), Zero::zero(), Zero::zero())))?;
1321
1322 let nominator_shares = deposit
1324 .known
1325 .shares
1326 .checked_add(&shares_withdrew_in_current_epoch)
1327 .ok_or(Error::ShareOverflow)?;
1328 total_shares = total_shares
1329 .checked_sub(&nominator_shares)
1330 .ok_or(Error::ShareOverflow)?;
1331
1332 let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
1334 total_stake = total_stake
1335 .checked_sub(&nominator_staked_amount)
1336 .ok_or(Error::BalanceOverflow)?;
1337
1338 let amount_deposited_in_epoch = deposit
1340 .pending
1341 .map(|pending_deposit| pending_deposit.amount)
1342 .unwrap_or_default();
1343
1344 let total_amount_to_unlock = nominator_staked_amount
1345 .checked_add(&amount_ready_to_withdraw)
1346 .and_then(|amount| amount.checked_add(&amount_deposited_in_epoch))
1347 .ok_or(Error::BalanceOverflow)?;
1348
1349 let current_locked_amount = DepositOnHold::<T>::take((operator_id, nominator_id.clone()));
1351 if let Some(amount_to_mint) = total_amount_to_unlock.checked_sub(¤t_locked_amount) {
1352 mint_funds::<T>(&nominator_id, amount_to_mint)?;
1353 }
1354 if !current_locked_amount.is_zero() {
1355 let staked_hold_id = T::HoldIdentifier::staking_staked();
1356 T::Currency::release(
1357 &staked_hold_id,
1358 &nominator_id,
1359 current_locked_amount,
1360 Precision::Exact,
1361 )
1362 .map_err(|_| Error::RemoveLock)?;
1363 }
1364
1365 Pallet::<T>::deposit_event(Event::NominatedStakedUnlocked {
1366 operator_id,
1367 nominator_id: nominator_id.clone(),
1368 unlocked_amount: total_amount_to_unlock,
1369 });
1370
1371 let nominator_total_storage_fee_deposit = deposit
1373 .pending
1374 .map(|pending_deposit| pending_deposit.storage_fee_deposit)
1375 .unwrap_or(Zero::zero())
1376 .checked_add(&deposit.known.storage_fee_deposit)
1377 .ok_or(Error::BalanceOverflow)?;
1378
1379 bundle_storage_fund::withdraw_to::<T>(
1380 operator_id,
1381 &nominator_id,
1382 storage_fund_redeem_price.redeem(nominator_total_storage_fee_deposit),
1383 )
1384 .map_err(Error::BundleStorageFund)?;
1385
1386 let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal();
1388 T::Currency::release(
1389 &storage_fund_hold_id,
1390 &nominator_id,
1391 total_storage_fee_withdrawal,
1392 Precision::Exact,
1393 )
1394 .map_err(|_| Error::RemoveLock)?;
1395
1396 Pallet::<T>::deposit_event(Event::StorageFeeUnlocked {
1397 operator_id,
1398 nominator_id: nominator_id.clone(),
1399 storage_fee: total_storage_fee_withdrawal,
1400 });
1401
1402 total_storage_fee_deposit =
1404 total_storage_fee_deposit.saturating_sub(nominator_total_storage_fee_deposit);
1405
1406 let cleanup_operator = !Deposits::<T>::contains_prefix(operator_id)
1409 && !Withdrawals::<T>::contains_prefix(operator_id);
1410
1411 if cleanup_operator {
1412 do_cleanup_operator::<T>(operator_id, total_stake)?
1413 } else {
1414 operator.current_total_shares = total_shares;
1416 operator.current_total_stake = total_stake;
1417 operator.total_storage_fee_deposit = total_storage_fee_deposit;
1418
1419 *maybe_operator = Some(operator);
1420 }
1421
1422 Ok(())
1423 })
1424}
1425
1426pub(crate) fn do_cleanup_operator<T: Config>(
1428 operator_id: OperatorId,
1429 total_stake: BalanceOf<T>,
1430) -> Result<(), Error> {
1431 bundle_storage_fund::transfer_all_to_treasury::<T>(operator_id)
1433 .map_err(Error::BundleStorageFund)?;
1434
1435 mint_into_treasury::<T>(total_stake)?;
1437
1438 OperatorIdOwner::<T>::remove(operator_id);
1440
1441 OperatorHighestSlot::<T>::remove(operator_id);
1443
1444 let _ = OperatorEpochSharePrice::<T>::clear_prefix(operator_id, u32::MAX, None);
1446
1447 Ok(())
1448}
1449
1450pub(crate) fn do_reward_operators<T: Config>(
1452 domain_id: DomainId,
1453 source: OperatorRewardSource<BlockNumberFor<T>>,
1454 operators: IntoIter<OperatorId>,
1455 rewards: BalanceOf<T>,
1456) -> Result<(), Error> {
1457 if rewards.is_zero() {
1458 return Ok(());
1459 }
1460 DomainStakingSummary::<T>::mutate(domain_id, |maybe_stake_summary| {
1461 let stake_summary = maybe_stake_summary
1462 .as_mut()
1463 .ok_or(Error::DomainNotInitialized)?;
1464
1465 let total_count = operators.len() as u64;
1466 let operator_weights = operators.into_iter().fold(
1468 BTreeMap::<OperatorId, u64>::new(),
1469 |mut acc, operator_id| {
1470 acc.entry(operator_id)
1471 .and_modify(|weight| *weight += 1)
1472 .or_insert(1);
1473 acc
1474 },
1475 );
1476
1477 let mut allocated_rewards = BalanceOf::<T>::zero();
1478 for (operator_id, weight) in operator_weights {
1479 let operator_reward = {
1480 let distribution = Perquintill::from_rational(weight, total_count);
1481 distribution.mul_floor(rewards)
1482 };
1483
1484 stake_summary
1485 .current_epoch_rewards
1486 .entry(operator_id)
1487 .and_modify(|rewards| *rewards = rewards.saturating_add(operator_reward))
1488 .or_insert(operator_reward);
1489
1490 Pallet::<T>::deposit_event(Event::OperatorRewarded {
1491 source: source.clone(),
1492 operator_id,
1493 reward: operator_reward,
1494 });
1495
1496 allocated_rewards = allocated_rewards
1497 .checked_add(&operator_reward)
1498 .ok_or(Error::BalanceOverflow)?;
1499 }
1500
1501 mint_into_treasury::<T>(
1503 rewards
1504 .checked_sub(&allocated_rewards)
1505 .ok_or(Error::BalanceUnderflow)?,
1506 )
1507 })
1508}
1509
1510pub(crate) fn do_mark_operators_as_slashed<T: Config>(
1513 operator_ids: impl AsRef<[OperatorId]>,
1514 slash_reason: SlashedReason<DomainBlockNumberFor<T>, ReceiptHashFor<T>>,
1515) -> Result<(), Error> {
1516 for operator_id in operator_ids.as_ref() {
1517 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1518 let operator = match maybe_operator.as_mut() {
1519 None => return Ok(()),
1523 Some(operator) => operator,
1524 };
1525 let mut pending_slashes =
1526 PendingSlashes::<T>::get(operator.current_domain_id).unwrap_or_default();
1527
1528 if pending_slashes.contains(operator_id) {
1529 return Ok(());
1530 }
1531
1532 DomainStakingSummary::<T>::try_mutate(
1533 operator.current_domain_id,
1534 |maybe_domain_stake_summary| {
1535 let stake_summary = maybe_domain_stake_summary
1536 .as_mut()
1537 .ok_or(Error::DomainNotInitialized)?;
1538
1539 operator.update_status(OperatorStatus::Slashed);
1541
1542 if stake_summary
1545 .current_operators
1546 .remove(operator_id)
1547 .is_some()
1548 {
1549 stake_summary.current_total_stake = stake_summary
1550 .current_total_stake
1551 .checked_sub(&operator.current_total_stake)
1552 .ok_or(Error::BalanceUnderflow)?;
1553 }
1554 stake_summary.next_operators.remove(operator_id);
1555 pending_slashes.insert(*operator_id);
1556 PendingSlashes::<T>::insert(operator.current_domain_id, pending_slashes);
1557 Pallet::<T>::deposit_event(Event::OperatorSlashed {
1558 operator_id: *operator_id,
1559 reason: slash_reason.clone(),
1560 });
1561 Ok(())
1562 },
1563 )
1564 })?
1565 }
1566
1567 Ok(())
1568}
1569
1570pub(crate) fn do_mark_invalid_bundle_authors<T: Config>(
1572 domain_id: DomainId,
1573 er: &ExecutionReceiptOf<T>,
1574) -> Result<(), Error> {
1575 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1576 let er_hash = er.hash::<DomainHashingFor<T>>();
1577 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1578 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1579 let mut stake_summary =
1580 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1581
1582 for operator_id in invalid_bundle_authors {
1583 if pending_slashes.contains(&operator_id) {
1584 continue;
1585 }
1586
1587 mark_invalid_bundle_author::<T>(
1588 operator_id,
1589 er_hash,
1590 &mut stake_summary,
1591 &mut invalid_bundle_authors_in_epoch,
1592 )?;
1593 }
1594
1595 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1596 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1597 Ok(())
1598}
1599
1600pub(crate) fn mark_invalid_bundle_author<T: Config>(
1601 operator_id: OperatorId,
1602 er_hash: ReceiptHashFor<T>,
1603 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1604 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1605) -> Result<(), Error> {
1606 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1607 let operator = match maybe_operator.as_mut() {
1608 None => return Ok(()),
1612 Some(operator) => operator,
1613 };
1614
1615 if operator.status::<T>(operator_id) != &OperatorStatus::Registered {
1618 return Ok(());
1619 }
1620
1621 operator.update_status(OperatorStatus::InvalidBundle(er_hash));
1623 invalid_bundle_authors.insert(operator_id);
1624 if stake_summary
1625 .current_operators
1626 .remove(&operator_id)
1627 .is_some()
1628 {
1629 stake_summary.current_total_stake = stake_summary
1630 .current_total_stake
1631 .checked_sub(&operator.current_total_stake)
1632 .ok_or(Error::BalanceUnderflow)?;
1633 }
1634 stake_summary.next_operators.remove(&operator_id);
1635 Ok(())
1636 })
1637}
1638
1639pub(crate) fn do_unmark_invalid_bundle_authors<T: Config>(
1643 domain_id: DomainId,
1644 er: &ExecutionReceiptOf<T>,
1645) -> Result<(), Error> {
1646 let invalid_bundle_authors = invalid_bundle_authors_for_receipt::<T>(domain_id, er);
1647 let er_hash = er.hash::<DomainHashingFor<T>>();
1648 let pending_slashes = PendingSlashes::<T>::get(domain_id).unwrap_or_default();
1649 let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::<T>::get(domain_id);
1650 let mut stake_summary =
1651 DomainStakingSummary::<T>::get(domain_id).ok_or(Error::DomainNotInitialized)?;
1652
1653 for operator_id in invalid_bundle_authors {
1654 if pending_slashes.contains(&operator_id)
1655 || Pallet::<T>::is_operator_pending_to_slash(domain_id, operator_id)
1656 {
1657 continue;
1658 }
1659
1660 unmark_invalid_bundle_author::<T>(
1661 operator_id,
1662 er_hash,
1663 &mut stake_summary,
1664 &mut invalid_bundle_authors_in_epoch,
1665 )?;
1666 }
1667
1668 DomainStakingSummary::<T>::insert(domain_id, stake_summary);
1669 InvalidBundleAuthors::<T>::insert(domain_id, invalid_bundle_authors_in_epoch);
1670 Ok(())
1671}
1672
1673fn unmark_invalid_bundle_author<T: Config>(
1674 operator_id: OperatorId,
1675 er_hash: ReceiptHashFor<T>,
1676 stake_summary: &mut StakingSummary<OperatorId, BalanceOf<T>>,
1677 invalid_bundle_authors: &mut BTreeSet<OperatorId>,
1678) -> Result<(), Error> {
1679 Operators::<T>::try_mutate(operator_id, |maybe_operator| {
1680 let operator = match maybe_operator.as_mut() {
1681 None => return Ok(()),
1685 Some(operator) => operator,
1686 };
1687
1688 if operator.partial_status != OperatorStatus::InvalidBundle(er_hash) {
1690 return Ok(());
1691 }
1692
1693 operator.update_status(OperatorStatus::Registered);
1695 invalid_bundle_authors.remove(&operator_id);
1696 stake_summary.next_operators.insert(operator_id);
1697 Ok(())
1698 })
1699}
1700
1701#[cfg(test)]
1702pub(crate) mod tests {
1703 use crate::domain_registry::{DomainConfig, DomainObject};
1704 use crate::pallet::{
1705 Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber,
1706 NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals,
1707 };
1708 use crate::staking::{
1709 DomainEpoch, Error as StakingError, Operator, OperatorConfig, OperatorDeregisteredInfo,
1710 OperatorStatus, SharePrice, StakingSummary, do_convert_previous_epoch_withdrawal,
1711 do_mark_operators_as_slashed, do_nominate_operator, do_reward_operators, do_unlock_funds,
1712 do_withdraw_stake,
1713 };
1714 use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator};
1715 use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext};
1716 use crate::{
1717 BalanceOf, DeactivatedOperators, DeregisteredOperators, Error, MAX_NOMINATORS_TO_SLASH,
1718 NominatorId, OperatorEpochSharePrice, SlashedReason, bundle_storage_fund,
1719 };
1720 use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
1721 use frame_support::traits::Currency;
1722 use frame_support::traits::fungible::Mutate;
1723 use frame_support::weights::Weight;
1724 use frame_support::{assert_err, assert_ok};
1725 use prop_test::prelude::*;
1726 use prop_test::proptest::test_runner::TestCaseResult;
1727 use sp_core::{Pair, sr25519};
1728 use sp_domains::{
1729 DomainId, OperatorAllowList, OperatorId, OperatorPair, OperatorPublicKey,
1730 OperatorRewardSource,
1731 };
1732 use sp_runtime::traits::Zero;
1733 use sp_runtime::{PerThing, Percent, Perquintill};
1734 use std::collections::{BTreeMap, BTreeSet};
1735 use std::ops::RangeInclusive;
1736 use std::vec;
1737 use subspace_runtime_primitives::AI3;
1738
1739 type Balances = pallet_balances::Pallet<Test>;
1740 type Domains = crate::Pallet<Test>;
1741
1742 const STORAGE_FEE_RESERVE: Perquintill = Perquintill::from_percent(20);
1743
1744 #[allow(clippy::too_many_arguments)]
1745 pub(crate) fn register_operator(
1746 domain_id: DomainId,
1747 operator_account: <Test as frame_system::Config>::AccountId,
1748 operator_free_balance: BalanceOf<Test>,
1749 operator_stake: BalanceOf<Test>,
1750 minimum_nominator_stake: BalanceOf<Test>,
1751 signing_key: OperatorPublicKey,
1752 nomination_tax: Percent,
1753 mut nominators: BTreeMap<NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>)>,
1754 ) -> (OperatorId, OperatorConfig<BalanceOf<Test>>) {
1755 nominators.insert(operator_account, (operator_free_balance, operator_stake));
1756 for nominator in &nominators {
1757 Balances::set_balance(nominator.0, nominator.1.0);
1758 assert_eq!(Balances::usable_balance(nominator.0), nominator.1.0);
1759 }
1760 nominators.remove(&operator_account);
1761
1762 if !DomainRegistry::<Test>::contains_key(domain_id) {
1763 let domain_config = DomainConfig {
1764 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
1765 runtime_id: 0,
1766 max_bundle_size: u32::MAX,
1767 max_bundle_weight: Weight::MAX,
1768 bundle_slot_probability: (0, 0),
1769 operator_allow_list: OperatorAllowList::Anyone,
1770 initial_balances: Default::default(),
1771 };
1772
1773 let domain_obj = DomainObject {
1774 owner_account_id: 0,
1775 created_at: 0,
1776 genesis_receipt_hash: Default::default(),
1777 domain_config,
1778 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
1779 domain_instantiation_deposit: Default::default(),
1780 };
1781
1782 DomainRegistry::<Test>::insert(domain_id, domain_obj);
1783 }
1784
1785 if !DomainStakingSummary::<Test>::contains_key(domain_id) {
1786 DomainStakingSummary::<Test>::insert(
1787 domain_id,
1788 StakingSummary {
1789 current_epoch_index: 0,
1790 current_total_stake: 0,
1791 current_operators: BTreeMap::new(),
1792 next_operators: BTreeSet::new(),
1793 current_epoch_rewards: BTreeMap::new(),
1794 },
1795 );
1796 }
1797
1798 let operator_config = OperatorConfig {
1799 signing_key,
1800 minimum_nominator_stake,
1801 nomination_tax,
1802 };
1803
1804 let res = Domains::register_operator(
1805 RuntimeOrigin::signed(operator_account),
1806 domain_id,
1807 operator_stake,
1808 operator_config.clone(),
1809 );
1810 assert_ok!(res);
1811
1812 let operator_id = NextOperatorId::<Test>::get() - 1;
1813 for nominator in nominators {
1814 if nominator.1.1.is_zero() {
1815 continue;
1816 }
1817
1818 let res = Domains::nominate_operator(
1819 RuntimeOrigin::signed(nominator.0),
1820 operator_id,
1821 nominator.1.1,
1822 );
1823 assert_ok!(res);
1824 assert!(Deposits::<Test>::contains_key(operator_id, nominator.0));
1825 }
1826
1827 (operator_id, operator_config)
1828 }
1829
1830 #[test]
1831 fn test_register_operator_invalid_signing_key() {
1832 let domain_id = DomainId::new(0);
1833 let operator_account = 1;
1834
1835 let mut ext = new_test_ext();
1836 ext.execute_with(|| {
1837 let operator_config = OperatorConfig {
1838 signing_key: OperatorPublicKey::from(sr25519::Public::default()),
1839 minimum_nominator_stake: Default::default(),
1840 nomination_tax: Default::default(),
1841 };
1842
1843 let res = Domains::register_operator(
1844 RuntimeOrigin::signed(operator_account),
1845 domain_id,
1846 Default::default(),
1847 operator_config,
1848 );
1849 assert_err!(
1850 res,
1851 Error::<Test>::Staking(StakingError::InvalidOperatorSigningKey)
1852 );
1853 });
1854 }
1855
1856 #[test]
1857 fn test_register_operator_minimum_nominator_stake() {
1858 let domain_id = DomainId::new(0);
1859 let operator_account = 1;
1860 let pair = OperatorPair::from_seed(&[0; 32]);
1861
1862 let mut ext = new_test_ext();
1863 ext.execute_with(|| {
1864 let operator_config = OperatorConfig {
1865 signing_key: pair.public(),
1866 minimum_nominator_stake: Default::default(),
1867 nomination_tax: Default::default(),
1868 };
1869
1870 let res = Domains::register_operator(
1871 RuntimeOrigin::signed(operator_account),
1872 domain_id,
1873 Default::default(),
1874 operator_config,
1875 );
1876 assert_err!(
1877 res,
1878 Error::<Test>::Staking(StakingError::MinimumNominatorStake)
1879 );
1880 });
1881 }
1882
1883 #[test]
1884 fn test_register_operator() {
1885 let domain_id = DomainId::new(0);
1886 let operator_account = 1;
1887 let operator_free_balance = 2500 * AI3;
1888 let operator_total_stake = 1000 * AI3;
1889 let operator_stake = 800 * AI3;
1890 let operator_storage_fee_deposit = 200 * AI3;
1891 let pair = OperatorPair::from_seed(&[0; 32]);
1892
1893 let mut ext = new_test_ext();
1894 ext.execute_with(|| {
1895 let (operator_id, mut operator_config) = register_operator(
1896 domain_id,
1897 operator_account,
1898 operator_free_balance,
1899 operator_total_stake,
1900 AI3,
1901 pair.public(),
1902 Default::default(),
1903 BTreeMap::new(),
1904 );
1905
1906 assert_eq!(NextOperatorId::<Test>::get(), 1);
1907 assert_eq!(
1909 OperatorIdOwner::<Test>::get(operator_id).unwrap(),
1910 operator_account
1911 );
1912 assert_eq!(
1913 Operators::<Test>::get(operator_id).unwrap(),
1914 Operator {
1915 signing_key: pair.public(),
1916 current_domain_id: domain_id,
1917 next_domain_id: domain_id,
1918 minimum_nominator_stake: AI3,
1919 nomination_tax: Default::default(),
1920 current_total_stake: operator_stake,
1921 current_total_shares: operator_stake,
1922 partial_status: OperatorStatus::Registered,
1923 deposits_in_epoch: 0,
1924 withdrawals_in_epoch: 0,
1925 total_storage_fee_deposit: operator_storage_fee_deposit,
1926 }
1927 );
1928
1929 let stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1930 assert!(stake_summary.next_operators.contains(&operator_id));
1931 assert_eq!(stake_summary.current_total_stake, operator_stake);
1932
1933 assert_eq!(
1934 Balances::usable_balance(operator_account),
1935 operator_free_balance - operator_total_stake - ExistentialDeposit::get()
1936 );
1937
1938 let res = Domains::register_operator(
1940 RuntimeOrigin::signed(operator_account),
1941 domain_id,
1942 operator_stake,
1943 operator_config.clone(),
1944 );
1945 assert_ok!(res);
1946
1947 let new_pair = OperatorPair::from_seed(&[1; 32]);
1949 operator_config.signing_key = new_pair.public();
1950 let res = Domains::register_operator(
1951 RuntimeOrigin::signed(operator_account),
1952 domain_id,
1953 operator_stake,
1954 operator_config,
1955 );
1956 assert_err!(
1957 res,
1958 Error::<Test>::Staking(crate::staking::Error::InsufficientBalance)
1959 );
1960 });
1961 }
1962
1963 #[test]
1964 fn nominate_operator() {
1965 let domain_id = DomainId::new(0);
1966 let operator_account = 1;
1967 let operator_free_balance = 1500 * AI3;
1968 let operator_total_stake = 1000 * AI3;
1969 let operator_stake = 800 * AI3;
1970 let operator_storage_fee_deposit = 200 * AI3;
1971 let pair = OperatorPair::from_seed(&[0; 32]);
1972
1973 let nominator_account = 2;
1974 let nominator_free_balance = 150 * AI3;
1975 let nominator_total_stake = 100 * AI3;
1976 let nominator_stake = 80 * AI3;
1977 let nominator_storage_fee_deposit = 20 * AI3;
1978
1979 let mut ext = new_test_ext();
1980 ext.execute_with(|| {
1981 let (operator_id, _) = register_operator(
1982 domain_id,
1983 operator_account,
1984 operator_free_balance,
1985 operator_total_stake,
1986 10 * AI3,
1987 pair.public(),
1988 Default::default(),
1989 BTreeMap::from_iter(vec![(
1990 nominator_account,
1991 (nominator_free_balance, nominator_total_stake),
1992 )]),
1993 );
1994
1995 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
1996 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
1997
1998 let operator = Operators::<Test>::get(operator_id).unwrap();
1999 assert_eq!(operator.current_total_stake, operator_stake);
2000 assert_eq!(operator.current_total_shares, operator_stake);
2001 assert_eq!(
2002 operator.total_storage_fee_deposit,
2003 operator_storage_fee_deposit + nominator_storage_fee_deposit
2004 );
2005 assert_eq!(operator.deposits_in_epoch, nominator_stake);
2006
2007 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2008 .unwrap()
2009 .pending
2010 .unwrap();
2011 assert_eq!(pending_deposit.amount, nominator_stake);
2012 assert_eq!(
2013 pending_deposit.storage_fee_deposit,
2014 nominator_storage_fee_deposit
2015 );
2016 assert_eq!(pending_deposit.total().unwrap(), nominator_total_stake);
2017
2018 assert_eq!(
2019 Balances::usable_balance(nominator_account),
2020 nominator_free_balance - nominator_total_stake - ExistentialDeposit::get()
2021 );
2022
2023 let additional_nomination_total_stake = 40 * AI3;
2025 let additional_nomination_stake = 32 * AI3;
2026 let additional_nomination_storage_fee_deposit = 8 * AI3;
2027 let res = Domains::nominate_operator(
2028 RuntimeOrigin::signed(nominator_account),
2029 operator_id,
2030 additional_nomination_total_stake,
2031 );
2032 assert_ok!(res);
2033 let pending_deposit = Deposits::<Test>::get(0, nominator_account)
2034 .unwrap()
2035 .pending
2036 .unwrap();
2037 assert_eq!(
2038 pending_deposit.amount,
2039 nominator_stake + additional_nomination_stake
2040 );
2041 assert_eq!(
2042 pending_deposit.storage_fee_deposit,
2043 nominator_storage_fee_deposit + additional_nomination_storage_fee_deposit
2044 );
2045
2046 let operator = Operators::<Test>::get(operator_id).unwrap();
2047 assert_eq!(operator.current_total_stake, operator_stake);
2048 assert_eq!(
2049 operator.deposits_in_epoch,
2050 nominator_stake + additional_nomination_stake
2051 );
2052 assert_eq!(
2053 operator.total_storage_fee_deposit,
2054 operator_storage_fee_deposit
2055 + nominator_storage_fee_deposit
2056 + additional_nomination_storage_fee_deposit
2057 );
2058
2059 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2061
2062 let operator = Operators::<Test>::get(operator_id).unwrap();
2063 assert_eq!(
2064 operator.current_total_stake,
2065 operator_stake + nominator_stake + additional_nomination_stake
2066 );
2067
2068 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2069 assert_eq!(
2070 domain_staking_summary.current_total_stake,
2071 operator_stake + nominator_stake + additional_nomination_stake
2072 );
2073 });
2074 }
2075
2076 #[test]
2077 fn operator_deregistration() {
2078 let domain_id = DomainId::new(0);
2079 let operator_account = 1;
2080 let operator_stake = 200 * AI3;
2081 let operator_free_balance = 250 * AI3;
2082 let pair = OperatorPair::from_seed(&[0; 32]);
2083 let mut ext = new_test_ext();
2084 ext.execute_with(|| {
2085 let (operator_id, _) = register_operator(
2086 domain_id,
2087 operator_account,
2088 operator_free_balance,
2089 operator_stake,
2090 AI3,
2091 pair.public(),
2092 Default::default(),
2093 BTreeMap::new(),
2094 );
2095
2096 let res =
2097 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2098 assert_ok!(res);
2099
2100 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2101 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2102
2103 let operator = Operators::<Test>::get(operator_id).unwrap();
2104 assert_eq!(
2105 *operator.status::<Test>(operator_id),
2106 OperatorStatus::Deregistered(
2107 (
2108 domain_id,
2109 domain_stake_summary.current_epoch_index,
2110 5
2112 )
2113 .into()
2114 )
2115 );
2116
2117 let new_domain_id = DomainId::new(1);
2119 let domain_config = DomainConfig {
2120 domain_name: String::from_utf8(vec![0; 1024]).unwrap(),
2121 runtime_id: 0,
2122 max_bundle_size: u32::MAX,
2123 max_bundle_weight: Weight::MAX,
2124 bundle_slot_probability: (0, 0),
2125 operator_allow_list: OperatorAllowList::Anyone,
2126 initial_balances: Default::default(),
2127 };
2128
2129 let domain_obj = DomainObject {
2130 owner_account_id: 0,
2131 created_at: 0,
2132 genesis_receipt_hash: Default::default(),
2133 domain_config,
2134 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
2135 domain_instantiation_deposit: Default::default(),
2136 };
2137
2138 DomainRegistry::<Test>::insert(new_domain_id, domain_obj);
2139 DomainStakingSummary::<Test>::insert(
2140 new_domain_id,
2141 StakingSummary {
2142 current_epoch_index: 0,
2143 current_total_stake: 0,
2144 current_operators: BTreeMap::new(),
2145 next_operators: BTreeSet::new(),
2146 current_epoch_rewards: BTreeMap::new(),
2147 },
2148 );
2149
2150 let nominator_account = 100;
2152 let nominator_stake = 100 * AI3;
2153 let res = Domains::nominate_operator(
2154 RuntimeOrigin::signed(nominator_account),
2155 operator_id,
2156 nominator_stake,
2157 );
2158 assert_err!(
2159 res,
2160 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2161 );
2162 });
2163 }
2164
2165 #[test]
2166 fn operator_deactivation() {
2167 let domain_id = DomainId::new(0);
2168 let operator_account = 1;
2169 let operator_stake = 200 * AI3;
2170 let operator_free_balance = 250 * AI3;
2171 let pair = OperatorPair::from_seed(&[0; 32]);
2172 let mut ext = new_test_ext();
2173 ext.execute_with(|| {
2174 let (operator_id, _) = register_operator(
2175 domain_id,
2176 operator_account,
2177 operator_free_balance,
2178 operator_stake,
2179 AI3,
2180 pair.public(),
2181 Default::default(),
2182 BTreeMap::new(),
2183 );
2184
2185 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2186 let total_current_stake = domain_stake_summary.current_total_stake;
2187
2188 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2189 assert_ok!(res);
2190
2191 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2192 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2193 assert!(
2194 !domain_stake_summary
2195 .current_operators
2196 .contains_key(&operator_id)
2197 );
2198
2199 let current_epoch_index = domain_stake_summary.current_epoch_index;
2200 let reactivation_delay =
2201 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2202 let operator = Operators::<Test>::get(operator_id).unwrap();
2203 assert_eq!(
2204 *operator.status::<Test>(operator_id),
2205 OperatorStatus::Deactivated(reactivation_delay)
2206 );
2207
2208 let operator_stake = operator.current_total_stake;
2209 assert_eq!(
2210 total_current_stake,
2211 domain_stake_summary.current_total_stake + operator_stake
2212 );
2213
2214 let nominator_account = 100;
2216 let nominator_stake = 100 * AI3;
2217 let res = Domains::nominate_operator(
2218 RuntimeOrigin::signed(nominator_account),
2219 operator_id,
2220 nominator_stake,
2221 );
2222 assert_err!(
2223 res,
2224 Error::<Test>::Staking(crate::staking::Error::OperatorNotRegistered)
2225 );
2226 });
2227 }
2228
2229 #[test]
2230 fn operator_deactivation_withdraw_stake() {
2231 let domain_id = DomainId::new(0);
2232 let operator_account = 1;
2233 let operator_stake = 200 * AI3;
2234 let operator_free_balance = 250 * AI3;
2235 let pair = OperatorPair::from_seed(&[0; 32]);
2236 let nominator_account = 100;
2237 let nominator_free_balance = 150 * AI3;
2238 let nominator_total_stake = 100 * AI3;
2239 let mut ext = new_test_ext();
2240 ext.execute_with(|| {
2241 let (operator_id, _) = register_operator(
2242 domain_id,
2243 operator_account,
2244 operator_free_balance,
2245 operator_stake,
2246 AI3,
2247 pair.public(),
2248 Default::default(),
2249 BTreeMap::from_iter(vec![(
2250 nominator_account,
2251 (nominator_free_balance, nominator_total_stake),
2252 )]),
2253 );
2254
2255 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2256 let total_current_stake = domain_stake_summary.current_total_stake;
2257
2258 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2260 assert_ok!(res);
2261
2262 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2263 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2264 assert!(
2265 !domain_stake_summary
2266 .current_operators
2267 .contains_key(&operator_id)
2268 );
2269
2270 let current_epoch_index = domain_stake_summary.current_epoch_index;
2271 let reactivation_delay =
2272 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2273 let operator = Operators::<Test>::get(operator_id).unwrap();
2274 assert_eq!(
2275 *operator.status::<Test>(operator_id),
2276 OperatorStatus::Deactivated(reactivation_delay)
2277 );
2278
2279 let operator_stake = operator.current_total_stake;
2280 assert_eq!(
2281 total_current_stake,
2282 domain_stake_summary.current_total_stake + operator_stake
2283 );
2284
2285 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2287
2288 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2290
2291 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2293
2294 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2296 let res = Domains::withdraw_stake(
2297 RuntimeOrigin::signed(nominator_account),
2298 operator_id,
2299 nominator_shares,
2300 );
2301 assert_ok!(res);
2302
2303 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2306
2307 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2308 let current_epoch_index = domain_stake_summary.current_epoch_index;
2309
2310 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2312
2313 let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2315 assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2316
2317 let nominator_withdrawals =
2318 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
2319 let unlock_funds_at = nominator_withdrawals
2320 .withdrawal_in_shares
2321 .unwrap()
2322 .unlock_at_confirmed_domain_block_number;
2323 HeadDomainNumber::<Test>::insert(domain_id, unlock_funds_at + 1);
2324
2325 let res = Domains::unlock_funds(RuntimeOrigin::signed(nominator_account), operator_id);
2327 assert_ok!(res);
2328 });
2329 }
2330
2331 #[test]
2332 fn operator_deactivation_deregister_operator() {
2333 let domain_id = DomainId::new(0);
2334 let operator_account = 1;
2335 let operator_stake = 200 * AI3;
2336 let operator_free_balance = 250 * AI3;
2337 let pair = OperatorPair::from_seed(&[0; 32]);
2338 let nominator_account = 100;
2339 let nominator_free_balance = 150 * AI3;
2340 let nominator_total_stake = 100 * AI3;
2341 let mut ext = new_test_ext();
2342 ext.execute_with(|| {
2343 let (operator_id, _) = register_operator(
2344 domain_id,
2345 operator_account,
2346 operator_free_balance,
2347 operator_stake,
2348 AI3,
2349 pair.public(),
2350 Default::default(),
2351 BTreeMap::from_iter(vec![(
2352 nominator_account,
2353 (nominator_free_balance, nominator_total_stake),
2354 )]),
2355 );
2356
2357 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2358 let total_current_stake = domain_stake_summary.current_total_stake;
2359
2360 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2362 assert_ok!(res);
2363
2364 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2365 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2366 assert!(
2367 !domain_stake_summary
2368 .current_operators
2369 .contains_key(&operator_id)
2370 );
2371
2372 let current_epoch_index = domain_stake_summary.current_epoch_index;
2373 let reactivation_delay =
2374 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2375 let operator = Operators::<Test>::get(operator_id).unwrap();
2376 assert_eq!(
2377 *operator.status::<Test>(operator_id),
2378 OperatorStatus::Deactivated(reactivation_delay)
2379 );
2380
2381 let operator_stake = operator.current_total_stake;
2382 assert_eq!(
2383 total_current_stake,
2384 domain_stake_summary.current_total_stake + operator_stake
2385 );
2386
2387 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2389
2390 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2392
2393 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2395
2396 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2398 let res = Domains::withdraw_stake(
2399 RuntimeOrigin::signed(nominator_account),
2400 operator_id,
2401 nominator_shares,
2402 );
2403 assert_ok!(res);
2404
2405 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2408
2409 let res =
2411 Domains::deregister_operator(RuntimeOrigin::signed(operator_account), operator_id);
2412 assert_ok!(res);
2413
2414 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2417 assert!(DeregisteredOperators::<Test>::get(domain_id).contains(&operator_id));
2418
2419 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2420 let current_epoch_index = domain_stake_summary.current_epoch_index;
2421
2422 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2424
2425 let domain_epoch: DomainEpoch = (domain_id, current_epoch_index).into();
2427 assert!(OperatorEpochSharePrice::<Test>::get(operator_id, domain_epoch).is_some());
2428
2429 let unlock_operator_at = HeadDomainNumber::<Test>::get(domain_id)
2430 + <Test as Config>::StakeWithdrawalLockingPeriod::get();
2431
2432 let operator = Operators::<Test>::get(operator_id).unwrap();
2433 assert_eq!(
2434 operator.partial_status,
2435 OperatorStatus::Deregistered(OperatorDeregisteredInfo {
2436 domain_epoch,
2437 unlock_at_confirmed_domain_block_number: unlock_operator_at
2438 })
2439 );
2440
2441 HeadDomainNumber::<Test>::insert(domain_id, unlock_operator_at);
2442
2443 let res =
2445 Domains::unlock_nominator(RuntimeOrigin::signed(nominator_account), operator_id);
2446 assert_ok!(res);
2447
2448 let res =
2450 Domains::unlock_nominator(RuntimeOrigin::signed(operator_account), operator_id);
2451 assert_ok!(res);
2452
2453 assert!(OperatorIdOwner::<Test>::get(operator_id).is_none());
2455 });
2456 }
2457
2458 #[test]
2459 fn operator_reactivation() {
2460 let domain_id = DomainId::new(0);
2461 let operator_account = 1;
2462 let operator_stake = 200 * AI3;
2463 let operator_free_balance = 250 * AI3;
2464 let nominator_account = 100;
2465 let nominator_free_balance = 150 * AI3;
2466 let nominator_total_stake = 100 * AI3;
2467 let pair = OperatorPair::from_seed(&[0; 32]);
2468 let mut ext = new_test_ext();
2469 ext.execute_with(|| {
2470 let (operator_id, _) = register_operator(
2471 domain_id,
2472 operator_account,
2473 operator_free_balance,
2474 operator_stake,
2475 AI3,
2476 pair.public(),
2477 Default::default(),
2478 BTreeMap::from_iter(vec![(
2479 nominator_account,
2480 (nominator_free_balance, nominator_total_stake),
2481 )]),
2482 );
2483
2484 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id);
2485 assert_ok!(res);
2486
2487 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2488 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
2489 assert!(
2490 !domain_stake_summary
2491 .current_operators
2492 .contains_key(&operator_id)
2493 );
2494
2495 let current_epoch_index = domain_stake_summary.current_epoch_index;
2496 let reactivation_delay =
2497 current_epoch_index + <Test as Config>::OperatorActivationDelayInEpochs::get();
2498 let operator = Operators::<Test>::get(operator_id).unwrap();
2499 assert_eq!(
2500 *operator.status::<Test>(operator_id),
2501 OperatorStatus::Deactivated(reactivation_delay)
2502 );
2503
2504 let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2506 assert_err!(
2507 res,
2508 Error::<Test>::Staking(crate::staking::Error::ReactivationDelayPeriodIncomplete)
2509 );
2510
2511 for expected_epoch in (current_epoch_index + 1)..=reactivation_delay {
2512 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2513 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2514 assert_eq!(domain_stake_summary.current_epoch_index, expected_epoch);
2515 }
2516
2517 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2519
2520 let nominator_shares = Perquintill::from_percent(80).mul_floor(nominator_total_stake);
2522 let res = Domains::withdraw_stake(
2523 RuntimeOrigin::signed(nominator_account),
2524 operator_id,
2525 nominator_shares,
2526 );
2527 assert_ok!(res);
2528
2529 assert!(DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2531
2532 let res = Domains::reactivate_operator(RuntimeOrigin::root(), operator_id);
2533 assert_ok!(res);
2534
2535 assert!(!DeactivatedOperators::<Test>::get(domain_id).contains(&operator_id));
2538
2539 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
2540 assert!(domain_stake_summary.next_operators.contains(&operator_id));
2541
2542 let operator = Operators::<Test>::get(operator_id).unwrap();
2543 assert_eq!(
2544 *operator.status::<Test>(operator_id),
2545 OperatorStatus::Registered
2546 );
2547 });
2548 }
2549
2550 type WithdrawWithResult = Vec<(Share, Result<(), StakingError>)>;
2551
2552 type ExpectedWithdrawAmount = Option<(BalanceOf<Test>, bool)>;
2556
2557 type StorageFundChange = (bool, u32);
2559
2560 pub(crate) type Share = <Test as Config>::Share;
2561
2562 struct WithdrawParams {
2563 minimum_nominator_stake: BalanceOf<Test>,
2565 nominators: Vec<(NominatorId<Test>, BalanceOf<Test>)>,
2568 operator_reward: BalanceOf<Test>,
2570 nominator_id: NominatorId<Test>,
2572 withdraws: WithdrawWithResult,
2574 maybe_deposit: Option<BalanceOf<Test>>,
2576 expected_withdraw: ExpectedWithdrawAmount,
2579 expected_nominator_count_reduced_by: u32,
2581 storage_fund_change: StorageFundChange,
2583 }
2584
2585 fn withdraw_stake(params: WithdrawParams) {
2587 withdraw_stake_inner(params, false).expect("always panics rather than returning an error");
2588 }
2589
2590 fn withdraw_stake_prop(params: WithdrawParams) -> TestCaseResult {
2592 withdraw_stake_inner(params, true)
2593 }
2594
2595 fn withdraw_stake_inner(params: WithdrawParams, is_proptest: bool) -> TestCaseResult {
2597 let WithdrawParams {
2598 minimum_nominator_stake,
2599 nominators,
2600 operator_reward,
2601 nominator_id,
2602 withdraws,
2603 maybe_deposit,
2604 expected_withdraw,
2605 expected_nominator_count_reduced_by,
2606 storage_fund_change,
2607 } = params;
2608 let domain_id = DomainId::new(0);
2609 let operator_account = 0;
2610 let pair = OperatorPair::from_seed(&[0; 32]);
2611 let mut total_balance = nominators.iter().map(|n| n.1).sum::<BalanceOf<Test>>()
2612 + operator_reward
2613 + maybe_deposit.unwrap_or(0);
2614
2615 let mut nominators = BTreeMap::from_iter(
2616 nominators
2617 .into_iter()
2618 .map(|(id, bal)| (id, (bal + ExistentialDeposit::get(), bal)))
2619 .collect::<Vec<(NominatorId<Test>, (BalanceOf<Test>, BalanceOf<Test>))>>(),
2620 );
2621
2622 let mut ext = new_test_ext();
2623 ext.execute_with(|| {
2624 let (operator_free_balance, operator_stake) =
2625 nominators.remove(&operator_account).unwrap();
2626 let (operator_id, _) = register_operator(
2627 domain_id,
2628 operator_account,
2629 operator_free_balance,
2630 operator_stake,
2631 minimum_nominator_stake,
2632 pair.public(),
2633 Default::default(),
2634 nominators,
2635 );
2636
2637 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2638
2639 if !operator_reward.is_zero() {
2640 do_reward_operators::<Test>(
2641 domain_id,
2642 OperatorRewardSource::Dummy,
2643 vec![operator_id].into_iter(),
2644 operator_reward,
2645 )
2646 .unwrap();
2647 }
2648
2649 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
2650
2651 if let Some(deposit_amount) = maybe_deposit {
2652 Balances::mint_into(&nominator_id, deposit_amount).unwrap();
2653 let res = Domains::nominate_operator(
2654 RuntimeOrigin::signed(nominator_id),
2655 operator_id,
2656 deposit_amount,
2657 );
2658 assert_ok!(res);
2659 }
2660
2661 let operator = Operators::<Test>::get(operator_id).unwrap();
2662 let (is_storage_fund_increased, storage_fund_change_amount) = storage_fund_change;
2663 if is_storage_fund_increased {
2664 bundle_storage_fund::refund_storage_fee::<Test>(
2665 storage_fund_change_amount as u128 * AI3,
2666 BTreeMap::from_iter([(operator_id, 1)]),
2667 )
2668 .unwrap();
2669 assert_eq!(
2670 operator.total_storage_fee_deposit + storage_fund_change_amount as u128 * AI3,
2671 bundle_storage_fund::total_balance::<Test>(operator_id)
2672 );
2673 total_balance += storage_fund_change_amount as u128 * AI3;
2674 } else {
2675 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
2676 operator_id,
2677 storage_fund_change_amount,
2678 )
2679 .unwrap();
2680 assert_eq!(
2681 operator.total_storage_fee_deposit - storage_fund_change_amount as u128 * AI3,
2682 bundle_storage_fund::total_balance::<Test>(operator_id)
2683 );
2684 total_balance -= storage_fund_change_amount as u128 * AI3;
2685 }
2686
2687 for (withdraw, expected_result) in withdraws {
2688 let withdraw_share_amount = STORAGE_FEE_RESERVE.left_from_one().mul_ceil(withdraw);
2689 let res = Domains::withdraw_stake(
2690 RuntimeOrigin::signed(nominator_id),
2691 operator_id,
2692 withdraw_share_amount,
2693 )
2694 .map(|_| ());
2695 if is_proptest {
2696 prop_assert_eq!(
2697 res,
2698 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2699 "unexpected withdraw_stake result",
2700 );
2701 } else {
2702 assert_eq!(
2703 res,
2704 expected_result.map_err(|err| Error::<Test>::Staking(err).into()),
2705 "unexpected withdraw_stake result",
2706 );
2707 }
2708 }
2709
2710 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
2711
2712 if let Some((withdraw, include_ed)) = expected_withdraw {
2713 let previous_usable_balance = Balances::usable_balance(nominator_id);
2714
2715 HeadDomainNumber::<Test>::set(
2717 domain_id,
2718 head_domain_number
2719 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
2720 );
2721 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_id));
2722
2723 let expected_balance = if include_ed {
2724 total_balance += crate::tests::ExistentialDeposit::get();
2725 previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get()
2726 } else {
2727 previous_usable_balance + withdraw
2728 };
2729
2730 if is_proptest {
2731 prop_assert_approx!(Balances::usable_balance(nominator_id), expected_balance);
2732 } else {
2733 assert_eq!(
2734 Balances::usable_balance(nominator_id),
2735 expected_balance,
2736 "usable balance is not equal to expected balance:\n{} !=\n{}",
2737 Balances::usable_balance(nominator_id),
2738 expected_balance,
2739 );
2740 }
2741
2742 assert!(Withdrawals::<Test>::get(operator_id, nominator_id).is_none());
2744 }
2745
2746 if expected_nominator_count_reduced_by > 0 {
2749 if is_proptest {
2750 prop_assert!(
2751 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2752 "deposit exists for nominator: {nominator_id}",
2753 );
2754 prop_assert!(
2755 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2756 "deposit on hold exists for nominator: {nominator_id}",
2757 );
2758 } else {
2759 assert!(
2760 Deposits::<Test>::get(operator_id, nominator_id).is_none(),
2761 "deposit exists for nominator: {nominator_id}",
2762 );
2763 assert!(
2764 !DepositOnHold::<Test>::contains_key((operator_id, nominator_id)),
2765 "deposit on hold exists for nominator: {nominator_id}",
2766 );
2767 }
2768 }
2769
2770 let operator = Operators::<Test>::get(operator_id).unwrap();
2772 if is_proptest {
2773 prop_assert_approx!(
2774 Balances::usable_balance(nominator_id)
2775 + operator.current_total_stake
2776 + bundle_storage_fund::total_balance::<Test>(operator_id),
2777 total_balance,
2778 "\n{} + {} + {} =",
2779 Balances::usable_balance(nominator_id),
2780 operator.current_total_stake,
2781 bundle_storage_fund::total_balance::<Test>(operator_id),
2782 );
2783 } else {
2784 assert_eq!(
2785 Balances::usable_balance(nominator_id)
2786 + operator.current_total_stake
2787 + bundle_storage_fund::total_balance::<Test>(operator_id),
2788 total_balance,
2789 "initial total balance is not equal to final total balance:\n\
2790 {} + {} + {} =\n{} !=\n{}",
2791 Balances::usable_balance(nominator_id),
2792 operator.current_total_stake,
2793 bundle_storage_fund::total_balance::<Test>(operator_id),
2794 Balances::usable_balance(nominator_id)
2795 + operator.current_total_stake
2796 + bundle_storage_fund::total_balance::<Test>(operator_id),
2797 total_balance,
2798 );
2799 }
2800
2801 Ok(())
2802 })
2803 }
2804
2805 macro_rules! prop_assert_approx {
2811 ($actual:expr, $expected:expr $(,)?) => {
2812 prop_assert_approx!($actual, $expected, "");
2813 };
2814
2815 ($actual:expr, $expected:expr, $fmt:expr) => {
2816 prop_assert_approx!($actual, $expected, $fmt,);
2817 };
2818
2819 ($actual:expr, $expected:expr, $fmt:expr, $($args:tt)*) => {{
2820 let actual = $actual;
2821 let expected = $expected;
2822 let extra = format!($fmt, $($args)*);
2823 prop_test::proptest::prop_assert!(
2824 actual <= expected,
2825 "extra minting: actual amount is greater than expected amount:{}{}\
2826 \n{} >\
2827 \n{}",
2828 if extra.is_empty() { "" } else { "\n" },
2829 extra,
2830 actual,
2831 expected,
2832 );
2833 let expected_rounded_down = $crate::staking::tests::PROP_ROUNDING_DOWN_FACTOR
2834 .mul_floor(expected)
2835 .saturating_sub($crate::staking::tests::PROP_ABSOLUTE_ROUNDING_ERROR);
2836 prop_test::proptest::prop_assert!(
2837 actual >= expected_rounded_down,
2838 "excess rounding losses: actual amount is less than expected amount \
2839 rounded down:{}{}\
2840 \n{} <\
2841 \n{} (from\
2842 \n{})",
2843 if extra.is_empty() { "" } else { "\n" },
2844 extra,
2845 actual,
2846 expected_rounded_down,
2847 expected,
2848 );
2849 }};
2850 }
2851
2852 pub(crate) use prop_assert_approx;
2854
2855 pub(crate) const PROP_ROUNDING_DOWN_FACTOR: Perquintill =
2859 Perquintill::from_parts(1_000_000_000_000_000_000 - 1_000_000_000);
2860
2861 pub(crate) const PROP_ABSOLUTE_ROUNDING_ERROR: u128 = 1000;
2864
2865 pub(crate) const MAX_PROP_BALANCE: u128 = 2u128.pow(122);
2873
2874 pub(crate) const MIN_PROP_OPERATOR_STAKE: u128 = 3 * <Test as Config>::MinOperatorStake::get();
2877
2878 pub(crate) const MIN_PROP_NOMINATOR_STAKE: u128 =
2880 <Test as Config>::MinNominatorStake::get() + 1;
2881
2882 pub(crate) const PROP_OPERATOR_STAKE_RANGE: RangeInclusive<u128> =
2884 MIN_PROP_OPERATOR_STAKE..=MAX_PROP_BALANCE;
2885
2886 pub(crate) const PROP_NOMINATOR_STAKE_RANGE: RangeInclusive<u128> =
2888 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2889
2890 pub(crate) const PROP_DEPOSIT_RANGE: RangeInclusive<u128> =
2893 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2894
2895 pub(crate) const PROP_REWARD_RANGE: RangeInclusive<u128> = 0..=MAX_PROP_BALANCE;
2897
2898 pub(crate) const PROP_FREE_BALANCE_RANGE: RangeInclusive<u128> =
2900 MIN_PROP_NOMINATOR_STAKE..=MAX_PROP_BALANCE;
2901
2902 #[test]
2908 fn prop_withdraw_excess_operator_stake() {
2909 prop_test!(&PROP_OPERATOR_STAKE_RANGE, |operator_stake| {
2910 let mut excess_stake =
2911 operator_stake.saturating_sub(<Test as Config>::MinOperatorStake::get());
2912
2913 excess_stake = Perquintill::from_parts(1)
2915 .left_from_one()
2916 .mul_ceil(excess_stake);
2917 excess_stake -= 1;
2918
2919 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2920
2921 let expected_withdraw = (excess_stake, false);
2922
2923 withdraw_stake_prop(WithdrawParams {
2924 minimum_nominator_stake: <Test as Config>::MinOperatorStake::get(),
2925 nominators: vec![(0, operator_stake)],
2926 operator_reward: 0,
2927 nominator_id: 0,
2928 withdraws: vec![(excess_stake, Ok(()))],
2929 maybe_deposit: None,
2930 expected_withdraw: Some(expected_withdraw),
2931 expected_nominator_count_reduced_by: 0,
2932 storage_fund_change: (true, 0),
2933 })
2934 });
2935 }
2936
2937 #[test]
2940 fn prop_withdraw_excess_operator_stake_with_nominator() {
2941 prop_test!(
2942 &(PROP_OPERATOR_STAKE_RANGE, PROP_NOMINATOR_STAKE_RANGE,),
2943 |(operator_stake, nominator_stake)| {
2944 prop_assert!(
2946 [operator_stake, nominator_stake]
2947 .into_iter()
2948 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2949 .is_some()
2950 );
2951
2952 let expected_withdraw = (nominator_stake, true);
2953
2954 let excess_stake = expected_withdraw
2955 .0
2956 .saturating_sub(<Test as Config>::MinNominatorStake::get());
2957
2958 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
2959
2960 withdraw_stake_prop(WithdrawParams {
2961 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
2962 nominators: vec![(0, operator_stake), (1, nominator_stake)],
2963 operator_reward: 0,
2964 nominator_id: 1,
2965 withdraws: vec![(excess_stake, Ok(()))],
2966 maybe_deposit: None,
2967 expected_withdraw: Some(expected_withdraw),
2968 expected_nominator_count_reduced_by: 1,
2969 storage_fund_change: (true, 0),
2970 })
2971 }
2972 );
2973 }
2974
2975 #[test]
2978 fn prop_withdraw_excess_operator_stake_with_deposit() {
2979 prop_test!(
2980 &(PROP_OPERATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
2981 |(mut operator_stake, maybe_deposit)| {
2982 operator_stake = operator_stake.saturating_add(maybe_deposit);
2983
2984 prop_assert!(
2985 operator_stake.saturating_sub(maybe_deposit)
2986 >= <Test as Config>::MinOperatorStake::get(),
2987 "would cause MinimumOperatorStake error"
2988 );
2989
2990 prop_assert!(
2992 [operator_stake, maybe_deposit]
2993 .into_iter()
2994 .try_fold(0_u128, |acc, value| acc.checked_add(value))
2995 .is_some()
2996 );
2997
2998 let expected_withdraw = (
2999 STORAGE_FEE_RESERVE
3001 .left_from_one()
3002 .mul_ceil(operator_stake)
3003 .saturating_sub(maybe_deposit),
3004 true,
3005 );
3006
3007 let excess_stake = expected_withdraw
3008 .0
3009 .saturating_sub(<Test as Config>::MinOperatorStake::get());
3010
3011 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3012
3013 let maybe_deposit = if maybe_deposit == 0 {
3015 None
3016 } else {
3017 Some(maybe_deposit)
3018 };
3019
3020 withdraw_stake_prop(WithdrawParams {
3021 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3022 nominators: vec![(0, operator_stake)],
3023 operator_reward: 0,
3024 nominator_id: 0,
3025 withdraws: vec![(excess_stake, Ok(()))],
3026 maybe_deposit,
3027 expected_withdraw: Some(expected_withdraw),
3028 expected_nominator_count_reduced_by: 0,
3029 storage_fund_change: (true, 0),
3030 })
3031 }
3032 );
3033 }
3034
3035 #[test]
3039 fn prop_withdraw_excess_nominator_stake_with_deposit_and_fixed_operator_stake() {
3040 prop_test!(
3041 &(PROP_NOMINATOR_STAKE_RANGE, PROP_DEPOSIT_RANGE,),
3042 |(mut nominator_stake, maybe_deposit)| {
3043 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3044
3045 prop_assert!(
3046 nominator_stake.saturating_sub(maybe_deposit)
3047 >= <Test as Config>::MinNominatorStake::get(),
3048 "would cause MinimumNominatorStake error"
3049 );
3050
3051 let operator_stake = <Test as Config>::MinOperatorStake::get();
3053
3054 prop_assert!(
3056 [operator_stake, nominator_stake, maybe_deposit]
3057 .into_iter()
3058 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3059 .is_some()
3060 );
3061
3062 let expected_withdraw = (
3063 STORAGE_FEE_RESERVE
3064 .left_from_one()
3065 .mul_ceil(nominator_stake)
3066 .saturating_sub(maybe_deposit),
3067 true,
3068 );
3069
3070 let excess_stake = expected_withdraw
3071 .0
3072 .saturating_sub(<Test as Config>::MinNominatorStake::get());
3073
3074 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3075
3076 let maybe_deposit = if maybe_deposit == 0 {
3078 None
3079 } else {
3080 Some(maybe_deposit)
3081 };
3082
3083 withdraw_stake_prop(WithdrawParams {
3084 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3085 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3086 operator_reward: 0,
3087 nominator_id: 1,
3088 withdraws: vec![(excess_stake, Ok(()))],
3089 maybe_deposit,
3090 expected_withdraw: Some(expected_withdraw),
3091 expected_nominator_count_reduced_by: 0,
3092 storage_fund_change: (true, 0),
3093 })
3094 }
3095 );
3096 }
3097
3098 #[test]
3101 fn prop_withdraw_excess_nominator_stake_with_deposit() {
3102 prop_test!(
3103 &(
3104 PROP_OPERATOR_STAKE_RANGE,
3105 PROP_NOMINATOR_STAKE_RANGE,
3106 PROP_DEPOSIT_RANGE,
3107 ),
3108 |(operator_stake, mut nominator_stake, maybe_deposit)| {
3109 nominator_stake = nominator_stake.saturating_add(maybe_deposit);
3110
3111 prop_assert!(
3112 nominator_stake.saturating_sub(maybe_deposit)
3113 >= <Test as Config>::MinNominatorStake::get(),
3114 "would cause MinimumNominatorStake error"
3115 );
3116
3117 prop_assert!(
3119 [operator_stake, nominator_stake, maybe_deposit]
3120 .into_iter()
3121 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3122 .is_some()
3123 );
3124
3125 let expected_withdraw = (
3127 STORAGE_FEE_RESERVE
3128 .left_from_one()
3129 .mul_ceil(nominator_stake)
3130 .saturating_sub(maybe_deposit),
3131 true,
3132 );
3133
3134 let excess_stake = expected_withdraw
3135 .0
3136 .saturating_sub(<Test as Config>::MinNominatorStake::get());
3137
3138 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3139
3140 let maybe_deposit = if maybe_deposit == 0 {
3142 None
3143 } else {
3144 Some(maybe_deposit)
3145 };
3146
3147 withdraw_stake_prop(WithdrawParams {
3148 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3149 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3150 operator_reward: 0,
3151 nominator_id: 1,
3152 withdraws: vec![(excess_stake, Ok(()))],
3153 maybe_deposit,
3154 expected_withdraw: Some(expected_withdraw),
3155 expected_nominator_count_reduced_by: 0,
3156 storage_fund_change: (true, 0),
3157 })
3158 }
3159 );
3160 }
3161
3162 #[test]
3165 fn prop_withdraw_excess_operator_stake_with_nominator_and_reward() {
3166 prop_test!(
3167 &(
3168 PROP_OPERATOR_STAKE_RANGE,
3169 PROP_NOMINATOR_STAKE_RANGE,
3170 PROP_REWARD_RANGE,
3171 ),
3172 |(operator_stake, nominator_stake, operator_reward)| {
3173 prop_assert!(
3175 [operator_stake, nominator_stake, operator_reward]
3176 .into_iter()
3177 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3178 .is_some()
3179 );
3180
3181 let total_stake = operator_stake.saturating_add(nominator_stake);
3183 let operator_reward_split = Perquintill::from_rational(operator_stake, total_stake)
3184 .mul_floor(operator_reward);
3185
3186 let excess_stake =
3189 operator_stake.saturating_sub(2 * <Test as Config>::MinOperatorStake::get());
3190
3191 let expected_withdraw = (excess_stake.saturating_add(operator_reward_split), true);
3192
3193 prop_assert!(excess_stake > 0, "would cause ZeroWithdraw error");
3194
3195 withdraw_stake_prop(WithdrawParams {
3196 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3197 nominators: vec![(0, operator_stake), (1, nominator_stake)],
3198 operator_reward,
3199 nominator_id: 0,
3200 withdraws: vec![(excess_stake, Ok(()))],
3201 maybe_deposit: None,
3202 expected_withdraw: Some(expected_withdraw),
3203 expected_nominator_count_reduced_by: 0,
3204 storage_fund_change: (true, 0),
3205 })
3206 }
3207 );
3208 }
3209
3210 #[test]
3213 fn prop_withdraw_excess_operator_stake_with_deposit_and_reward() {
3214 prop_test!(
3215 &(
3216 PROP_OPERATOR_STAKE_RANGE,
3217 PROP_DEPOSIT_RANGE,
3218 PROP_REWARD_RANGE,
3219 ),
3220 |(mut operator_stake, maybe_deposit, operator_reward)| {
3221 operator_stake = operator_stake.saturating_add(maybe_deposit);
3222
3223 prop_assert!(
3224 operator_stake.saturating_sub(maybe_deposit)
3225 >= <Test as Config>::MinOperatorStake::get(),
3226 "would cause MinimumOperatorStake error"
3227 );
3228
3229 prop_assert!(
3231 [operator_stake, maybe_deposit, operator_reward]
3232 .into_iter()
3233 .try_fold(0_u128, |acc, value| acc.checked_add(value))
3234 .is_some()
3235 );
3236
3237 let reserve = 2 * <Test as Config>::MinOperatorStake::get();
3240 let excess_stake = operator_stake.saturating_sub(reserve);
3241
3242 let expected_withdraw = (
3244 operator_stake
3245 .saturating_add(operator_reward)
3246 .saturating_sub(reserve),
3247 true,
3248 );
3249
3250 prop_assume!(excess_stake > 0, "would cause ZeroWithdraw error");
3251
3252 let maybe_deposit = if maybe_deposit == 0 {
3254 None
3255 } else {
3256 Some(maybe_deposit)
3257 };
3258
3259 withdraw_stake_prop(WithdrawParams {
3260 minimum_nominator_stake: <Test as Config>::MinNominatorStake::get(),
3261 nominators: vec![(0, operator_stake)],
3262 operator_reward,
3263 nominator_id: 0,
3264 withdraws: vec![(excess_stake, Ok(()))],
3265 maybe_deposit,
3266 expected_withdraw: Some(expected_withdraw),
3267 expected_nominator_count_reduced_by: 0,
3268 storage_fund_change: (true, 0),
3269 })
3270 }
3271 );
3272 }
3273
3274 #[test]
3275 fn withdraw_stake_operator_all() {
3276 withdraw_stake(WithdrawParams {
3277 minimum_nominator_stake: 10 * AI3,
3278 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3279 operator_reward: 20 * AI3,
3280 nominator_id: 0,
3281 withdraws: vec![(150 * AI3, Err(StakingError::MinimumOperatorStake))],
3282 maybe_deposit: None,
3283 expected_withdraw: None,
3284 expected_nominator_count_reduced_by: 0,
3285 storage_fund_change: (true, 0),
3286 })
3287 }
3288
3289 #[test]
3290 fn withdraw_stake_operator_below_minimum() {
3291 withdraw_stake(WithdrawParams {
3292 minimum_nominator_stake: 10 * AI3,
3293 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3294 operator_reward: 20 * AI3,
3295 nominator_id: 0,
3296 withdraws: vec![(65 * AI3, Err(StakingError::MinimumOperatorStake))],
3297 maybe_deposit: None,
3298 expected_withdraw: None,
3299 expected_nominator_count_reduced_by: 0,
3300 storage_fund_change: (true, 0),
3301 })
3302 }
3303
3304 #[test]
3305 fn withdraw_stake_operator_below_minimum_no_rewards() {
3306 withdraw_stake(WithdrawParams {
3307 minimum_nominator_stake: 10 * AI3,
3308 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3309 operator_reward: Zero::zero(),
3310 nominator_id: 0,
3311 withdraws: vec![(51 * AI3, Err(StakingError::MinimumOperatorStake))],
3312 maybe_deposit: None,
3313 expected_withdraw: None,
3314 expected_nominator_count_reduced_by: 0,
3315 storage_fund_change: (true, 0),
3316 })
3317 }
3318
3319 #[test]
3320 fn withdraw_stake_operator_above_minimum() {
3321 withdraw_stake(WithdrawParams {
3322 minimum_nominator_stake: 10 * AI3,
3323 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3324 operator_reward: 20 * AI3,
3325 nominator_id: 0,
3326 withdraws: vec![(58 * AI3, Ok(()))],
3327 maybe_deposit: None,
3330 expected_withdraw: Some((63523809523809523770, false)),
3331 expected_nominator_count_reduced_by: 0,
3332 storage_fund_change: (true, 0),
3333 })
3334 }
3335
3336 #[test]
3337 fn withdraw_stake_operator_above_minimum_multiple_withdraws_error() {
3338 withdraw_stake(WithdrawParams {
3339 minimum_nominator_stake: 10 * AI3,
3340 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3341 operator_reward: 20 * AI3,
3342 nominator_id: 0,
3343 withdraws: vec![
3344 (58 * AI3, Ok(())),
3345 (5 * AI3, Err(StakingError::MinimumOperatorStake)),
3346 ],
3347 maybe_deposit: None,
3348 expected_withdraw: Some((63523809523809523770, false)),
3349 expected_nominator_count_reduced_by: 0,
3350 storage_fund_change: (true, 0),
3351 })
3352 }
3353
3354 #[test]
3355 fn withdraw_stake_operator_above_minimum_multiple_withdraws() {
3356 withdraw_stake(WithdrawParams {
3357 minimum_nominator_stake: 10 * AI3,
3358 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3359 operator_reward: 20 * AI3,
3360 nominator_id: 0,
3361 withdraws: vec![(53 * AI3, Ok(())), (5 * AI3, Ok(()))],
3362 maybe_deposit: None,
3363 expected_withdraw: Some((63523809523809523769, false)),
3364 expected_nominator_count_reduced_by: 0,
3365 storage_fund_change: (true, 0),
3366 })
3367 }
3368
3369 #[test]
3370 fn withdraw_stake_operator_above_minimum_no_rewards() {
3371 withdraw_stake(WithdrawParams {
3372 minimum_nominator_stake: 10 * AI3,
3373 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3374 operator_reward: Zero::zero(),
3375 nominator_id: 0,
3376 withdraws: vec![(49 * AI3, Ok(()))],
3377 maybe_deposit: None,
3378 expected_withdraw: Some((48999999999999999980, false)),
3379 expected_nominator_count_reduced_by: 0,
3380 storage_fund_change: (true, 0),
3381 })
3382 }
3383
3384 #[test]
3385 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards() {
3386 withdraw_stake(WithdrawParams {
3387 minimum_nominator_stake: 10 * AI3,
3388 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3389 operator_reward: Zero::zero(),
3390 nominator_id: 0,
3391 withdraws: vec![(29 * AI3, Ok(())), (20 * AI3, Ok(()))],
3392 maybe_deposit: None,
3393 expected_withdraw: Some((48999999999999999981, false)),
3394 expected_nominator_count_reduced_by: 0,
3395 storage_fund_change: (true, 0),
3396 })
3397 }
3398
3399 #[test]
3400 fn withdraw_stake_operator_above_minimum_multiple_withdraws_no_rewards_with_errors() {
3401 withdraw_stake(WithdrawParams {
3402 minimum_nominator_stake: 10 * AI3,
3403 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3404 operator_reward: Zero::zero(),
3405 nominator_id: 0,
3406 withdraws: vec![
3407 (29 * AI3, Ok(())),
3408 (20 * AI3, Ok(())),
3409 (20 * AI3, Err(StakingError::MinimumOperatorStake)),
3410 ],
3411 maybe_deposit: None,
3412 expected_withdraw: Some((48999999999999999981, false)),
3413 expected_nominator_count_reduced_by: 0,
3414 storage_fund_change: (true, 0),
3415 })
3416 }
3417
3418 #[test]
3419 fn withdraw_stake_nominator_below_minimum_with_rewards() {
3420 withdraw_stake(WithdrawParams {
3421 minimum_nominator_stake: 10 * AI3,
3422 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3423 operator_reward: 20 * AI3,
3424 nominator_id: 1,
3425 withdraws: vec![(45 * AI3, Ok(()))],
3426 maybe_deposit: None,
3430 expected_withdraw: Some((54761904761904761888, true)),
3431 expected_nominator_count_reduced_by: 1,
3432 storage_fund_change: (true, 0),
3433 })
3434 }
3435
3436 #[test]
3437 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws() {
3438 withdraw_stake(WithdrawParams {
3439 minimum_nominator_stake: 10 * AI3,
3440 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3441 operator_reward: 20 * AI3,
3442 nominator_id: 1,
3443 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3444 maybe_deposit: None,
3448 expected_withdraw: Some((54761904761904761888, true)),
3449 expected_nominator_count_reduced_by: 1,
3450 storage_fund_change: (true, 0),
3451 })
3452 }
3453
3454 #[test]
3455 fn withdraw_stake_nominator_below_minimum_with_rewards_multiple_withdraws_with_errors() {
3456 withdraw_stake(WithdrawParams {
3457 minimum_nominator_stake: 10 * AI3,
3458 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3459 operator_reward: 20 * AI3,
3460 nominator_id: 1,
3461 withdraws: vec![
3462 (25 * AI3, Ok(())),
3463 (20 * AI3, Ok(())),
3464 (20 * AI3, Err(StakingError::InsufficientShares)),
3465 ],
3466 maybe_deposit: None,
3470 expected_withdraw: Some((54761904761904761888, true)),
3471 expected_nominator_count_reduced_by: 1,
3472 storage_fund_change: (true, 0),
3473 })
3474 }
3475
3476 #[test]
3477 fn withdraw_stake_nominator_below_minimum_no_reward() {
3478 withdraw_stake(WithdrawParams {
3479 minimum_nominator_stake: 10 * AI3,
3480 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3481 operator_reward: Zero::zero(),
3482 nominator_id: 1,
3483 withdraws: vec![(45 * AI3, Ok(()))],
3484 maybe_deposit: None,
3485 expected_withdraw: Some((50 * AI3, true)),
3486 expected_nominator_count_reduced_by: 1,
3487 storage_fund_change: (true, 0),
3488 })
3489 }
3490
3491 #[test]
3492 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards() {
3493 withdraw_stake(WithdrawParams {
3494 minimum_nominator_stake: 10 * AI3,
3495 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3496 operator_reward: Zero::zero(),
3497 nominator_id: 1,
3498 withdraws: vec![(25 * AI3, Ok(())), (20 * AI3, Ok(()))],
3499 maybe_deposit: None,
3500 expected_withdraw: Some((50 * AI3, true)),
3501 expected_nominator_count_reduced_by: 1,
3502 storage_fund_change: (true, 0),
3503 })
3504 }
3505
3506 #[test]
3507 fn withdraw_stake_nominator_below_minimum_no_reward_multiple_rewards_with_errors() {
3508 withdraw_stake(WithdrawParams {
3509 minimum_nominator_stake: 10 * AI3,
3510 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3511 operator_reward: Zero::zero(),
3512 nominator_id: 1,
3513 withdraws: vec![
3514 (25 * AI3, Ok(())),
3515 (20 * AI3, Ok(())),
3516 (20 * AI3, Err(StakingError::InsufficientShares)),
3517 ],
3518 maybe_deposit: None,
3519 expected_withdraw: Some((50 * AI3, true)),
3520 expected_nominator_count_reduced_by: 1,
3521 storage_fund_change: (true, 0),
3522 })
3523 }
3524
3525 #[test]
3526 fn withdraw_stake_nominator_above_minimum() {
3527 withdraw_stake(WithdrawParams {
3528 minimum_nominator_stake: 10 * AI3,
3529 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3530 operator_reward: 20 * AI3,
3531 nominator_id: 1,
3532 withdraws: vec![(40 * AI3, Ok(()))],
3533 maybe_deposit: None,
3534 expected_withdraw: Some((43809523809523809511, false)),
3535 expected_nominator_count_reduced_by: 0,
3536 storage_fund_change: (true, 0),
3537 })
3538 }
3539
3540 #[test]
3541 fn withdraw_stake_nominator_above_minimum_multiple_withdraws() {
3542 withdraw_stake(WithdrawParams {
3543 minimum_nominator_stake: 10 * AI3,
3544 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3545 operator_reward: 20 * AI3,
3546 nominator_id: 1,
3547 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3, Ok(()))],
3548 maybe_deposit: None,
3549 expected_withdraw: Some((43809523809523809510, false)),
3550 expected_nominator_count_reduced_by: 0,
3551 storage_fund_change: (true, 0),
3552 })
3553 }
3554
3555 #[test]
3556 fn withdraw_stake_nominator_above_minimum_withdraw_all_multiple_withdraws_error() {
3557 withdraw_stake(WithdrawParams {
3558 minimum_nominator_stake: 10 * AI3,
3559 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3560 operator_reward: 20 * AI3,
3561 nominator_id: 1,
3562 withdraws: vec![
3563 (35 * AI3, Ok(())),
3564 (5 * AI3, Ok(())),
3565 (15 * AI3, Err(StakingError::InsufficientShares)),
3566 ],
3567 maybe_deposit: None,
3568 expected_withdraw: Some((43809523809523809510, false)),
3569 expected_nominator_count_reduced_by: 0,
3570 storage_fund_change: (true, 0),
3571 })
3572 }
3573
3574 #[test]
3575 fn withdraw_stake_nominator_above_minimum_no_rewards() {
3576 withdraw_stake(WithdrawParams {
3577 minimum_nominator_stake: 10 * AI3,
3578 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3579 operator_reward: Zero::zero(),
3580 nominator_id: 1,
3581 withdraws: vec![(39 * AI3, Ok(()))],
3582 maybe_deposit: None,
3583 expected_withdraw: Some((39 * AI3, false)),
3584 expected_nominator_count_reduced_by: 0,
3585 storage_fund_change: (true, 0),
3586 })
3587 }
3588
3589 #[test]
3590 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws() {
3591 withdraw_stake(WithdrawParams {
3592 minimum_nominator_stake: 10 * AI3,
3593 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3594 operator_reward: Zero::zero(),
3595 nominator_id: 1,
3596 withdraws: vec![(35 * AI3, Ok(())), (5 * AI3 - 100000000000, Ok(()))],
3597 maybe_deposit: None,
3598 expected_withdraw: Some((39999999899999999998, false)),
3599 expected_nominator_count_reduced_by: 0,
3600 storage_fund_change: (true, 0),
3601 })
3602 }
3603
3604 #[test]
3605 fn withdraw_stake_nominator_above_minimum_no_rewards_multiple_withdraws_with_errors() {
3606 withdraw_stake(WithdrawParams {
3607 minimum_nominator_stake: 10 * AI3,
3608 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3609 operator_reward: Zero::zero(),
3610 nominator_id: 1,
3611 withdraws: vec![
3612 (35 * AI3, Ok(())),
3613 (5 * AI3 - 100000000000, Ok(())),
3614 (15 * AI3, Err(StakingError::InsufficientShares)),
3615 ],
3616 maybe_deposit: None,
3617 expected_withdraw: Some((39999999899999999998, false)),
3618 expected_nominator_count_reduced_by: 0,
3619 storage_fund_change: (true, 0),
3620 })
3621 }
3622
3623 #[test]
3624 fn withdraw_stake_nominator_no_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3625 withdraw_stake(WithdrawParams {
3626 minimum_nominator_stake: 10 * AI3,
3627 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3628 operator_reward: Zero::zero(),
3629 nominator_id: 1,
3630 withdraws: vec![
3631 (35 * AI3, Ok(())),
3632 (5 * AI3 - 100000000000, Ok(())),
3633 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3634 ],
3635 maybe_deposit: Some(2 * AI3),
3636 expected_withdraw: Some((39999999899999999998, false)),
3637 expected_nominator_count_reduced_by: 0,
3638 storage_fund_change: (true, 0),
3639 })
3640 }
3641
3642 #[test]
3643 fn withdraw_stake_nominator_with_rewards_multiple_withdraws_with_error_min_nominator_stake() {
3644 withdraw_stake(WithdrawParams {
3645 minimum_nominator_stake: 10 * AI3,
3646 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3647 operator_reward: 20 * AI3,
3648 nominator_id: 1,
3649 withdraws: vec![
3650 (35 * AI3, Ok(())),
3651 (5 * AI3, Ok(())),
3652 (10 * AI3, Err(StakingError::MinimumNominatorStake)),
3653 ],
3654 maybe_deposit: Some(2 * AI3),
3658 expected_withdraw: Some((43809523809523809510, false)),
3659 expected_nominator_count_reduced_by: 0,
3660 storage_fund_change: (true, 0),
3661 })
3662 }
3663
3664 #[test]
3665 fn withdraw_stake_nominator_zero_amount() {
3666 withdraw_stake(WithdrawParams {
3667 minimum_nominator_stake: 10 * AI3,
3668 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3669 operator_reward: Zero::zero(),
3670 nominator_id: 1,
3671 withdraws: vec![(0, Err(StakingError::ZeroWithdraw))],
3672 maybe_deposit: None,
3673 expected_withdraw: None,
3674 expected_nominator_count_reduced_by: 0,
3675 storage_fund_change: (true, 0),
3676 })
3677 }
3678
3679 #[test]
3680 fn withdraw_stake_nominator_all_with_storage_fee_profit() {
3681 withdraw_stake(WithdrawParams {
3682 minimum_nominator_stake: 10 * AI3,
3683 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3684 operator_reward: Zero::zero(),
3685 nominator_id: 1,
3686 withdraws: vec![(50 * AI3, Ok(()))],
3687 maybe_deposit: None,
3688 storage_fund_change: (true, 21),
3691 expected_withdraw: Some((54999999999999999985, true)),
3692 expected_nominator_count_reduced_by: 1,
3693 })
3694 }
3695
3696 #[test]
3697 fn withdraw_stake_nominator_all_with_storage_fee_loss() {
3698 withdraw_stake(WithdrawParams {
3699 minimum_nominator_stake: 10 * AI3,
3700 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3701 operator_reward: Zero::zero(),
3702 nominator_id: 1,
3703 withdraws: vec![(50 * AI3, Ok(()))],
3704 maybe_deposit: None,
3705 storage_fund_change: (false, 21),
3708 expected_withdraw: Some((44999999999999999995, true)),
3709 expected_nominator_count_reduced_by: 1,
3710 })
3711 }
3712
3713 #[test]
3714 fn withdraw_stake_nominator_all_with_storage_fee_loss_all() {
3715 withdraw_stake(WithdrawParams {
3716 minimum_nominator_stake: 10 * AI3,
3717 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3718 operator_reward: Zero::zero(),
3719 nominator_id: 1,
3720 withdraws: vec![(50 * AI3, Ok(()))],
3721 maybe_deposit: None,
3722 storage_fund_change: (false, 42),
3725 expected_withdraw: Some((40 * AI3, true)),
3726 expected_nominator_count_reduced_by: 1,
3727 })
3728 }
3729
3730 #[test]
3731 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_profit() {
3732 withdraw_stake(WithdrawParams {
3733 minimum_nominator_stake: 10 * AI3,
3734 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3735 operator_reward: Zero::zero(),
3736 nominator_id: 1,
3737 withdraws: vec![(5 * AI3, Ok(())), (10 * AI3, Ok(())), (15 * AI3, Ok(()))],
3738 maybe_deposit: None,
3739 storage_fund_change: (true, 21),
3743 expected_withdraw: Some((30 * AI3 + 2999999999999999863, false)),
3744 expected_nominator_count_reduced_by: 0,
3745 })
3746 }
3747
3748 #[test]
3749 fn withdraw_stake_nominator_multiple_withdraws_with_storage_fee_loss() {
3750 withdraw_stake(WithdrawParams {
3751 minimum_nominator_stake: 10 * AI3,
3752 nominators: vec![(0, 150 * AI3), (1, 50 * AI3), (2, 10 * AI3)],
3753 operator_reward: Zero::zero(),
3754 nominator_id: 1,
3755 withdraws: vec![(5 * AI3, Ok(())), (5 * AI3, Ok(())), (10 * AI3, Ok(()))],
3756 maybe_deposit: None,
3757 storage_fund_change: (false, 21),
3761 expected_withdraw: Some((20 * AI3 - 2 * AI3 - 39, false)),
3762 expected_nominator_count_reduced_by: 0,
3763 })
3764 }
3765
3766 #[test]
3767 fn unlock_multiple_withdrawals() {
3768 let domain_id = DomainId::new(0);
3769 let operator_account = 1;
3770 let operator_free_balance = 250 * AI3;
3771 let operator_stake = 200 * AI3;
3772 let pair = OperatorPair::from_seed(&[0; 32]);
3773 let nominator_account = 2;
3774 let nominator_free_balance = 150 * AI3;
3775 let nominator_stake = 100 * AI3;
3776
3777 let nominators = vec![
3778 (operator_account, (operator_free_balance, operator_stake)),
3779 (nominator_account, (nominator_free_balance, nominator_stake)),
3780 ];
3781
3782 let total_deposit = 300 * AI3;
3783 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
3784 let init_total_storage_fund = STORAGE_FEE_RESERVE * total_deposit;
3785
3786 let mut ext = new_test_ext();
3787 ext.execute_with(|| {
3788 let (operator_id, _) = register_operator(
3789 domain_id,
3790 operator_account,
3791 operator_free_balance,
3792 operator_stake,
3793 10 * AI3,
3794 pair.public(),
3795 Default::default(),
3796 BTreeMap::from_iter(nominators),
3797 );
3798
3799 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3800 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3801 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3802
3803 let operator = Operators::<Test>::get(operator_id).unwrap();
3804 assert_eq!(operator.current_total_stake, init_total_stake);
3805 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3806 assert_eq!(
3807 operator.total_storage_fee_deposit,
3808 bundle_storage_fund::total_balance::<Test>(operator_id)
3809 );
3810
3811 let shares_per_withdraw = init_total_stake / 100;
3813 let head_domain_number = HeadDomainNumber::<Test>::get(domain_id);
3814
3815 for _ in 1..<Test as crate::Config>::WithdrawalLimit::get() {
3817 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3818 .unwrap();
3819 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3820 }
3821 HeadDomainNumber::<Test>::set(domain_id, head_domain_number + 1);
3823
3824 for _ in 0..5 {
3827 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw)
3828 .unwrap();
3829 }
3830 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3831
3832 assert_err!(
3834 do_withdraw_stake::<Test>(operator_id, nominator_account, shares_per_withdraw,),
3835 StakingError::TooManyWithdrawals
3836 );
3837 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3838 Withdrawals::<Test>::try_mutate(operator_id, nominator_account, |maybe_withdrawal| {
3839 let withdrawal = maybe_withdrawal.as_mut().unwrap();
3840 do_convert_previous_epoch_withdrawal::<Test>(
3841 operator_id,
3842 withdrawal,
3843 domain_stake_summary.current_epoch_index,
3844 )
3845 .unwrap();
3846 assert_eq!(
3847 withdrawal.withdrawals.len() as u32,
3848 <Test as crate::Config>::WithdrawalLimit::get()
3849 );
3850 Ok::<(), StakingError>(())
3851 })
3852 .unwrap();
3853
3854 HeadDomainNumber::<Test>::set(
3856 domain_id,
3857 head_domain_number + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get(),
3858 );
3859 let total_balance = Balances::usable_balance(nominator_account);
3860 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3861 assert_eq!(
3862 Balances::usable_balance(nominator_account) + 74, total_balance
3864 + (<Test as crate::Config>::WithdrawalLimit::get() as u128 - 1) * total_deposit
3865 / 100
3866 );
3867 let withdrawal = Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3868 assert_eq!(withdrawal.withdrawals.len(), 1);
3869
3870 HeadDomainNumber::<Test>::set(
3872 domain_id,
3873 head_domain_number
3874 + <Test as crate::Config>::StakeWithdrawalLockingPeriod::get()
3875 + 1,
3876 );
3877 let total_balance = Balances::usable_balance(nominator_account);
3878 assert_ok!(do_unlock_funds::<Test>(operator_id, nominator_account));
3879 assert_eq!(
3880 Balances::usable_balance(nominator_account) - 2, total_balance + 5 * total_deposit / 100
3882 );
3883 assert!(Withdrawals::<Test>::get(operator_id, nominator_account).is_none());
3884 });
3885 }
3886
3887 #[test]
3888 fn slash_operator() {
3889 let domain_id = DomainId::new(0);
3890 let operator_account = 1;
3891 let operator_free_balance = 250 * AI3;
3892 let operator_stake = 200 * AI3;
3893 let operator_extra_deposit = 40 * AI3;
3894 let pair = OperatorPair::from_seed(&[0; 32]);
3895 let nominator_account = 2;
3896 let nominator_free_balance = 150 * AI3;
3897 let nominator_stake = 100 * AI3;
3898 let nominator_extra_deposit = 40 * AI3;
3899
3900 let nominators = vec![
3901 (operator_account, (operator_free_balance, operator_stake)),
3902 (nominator_account, (nominator_free_balance, nominator_stake)),
3903 ];
3904
3905 let unlocking = vec![(operator_account, 10 * AI3), (nominator_account, 10 * AI3)];
3906
3907 let deposits = vec![
3908 (operator_account, operator_extra_deposit),
3909 (nominator_account, nominator_extra_deposit),
3910 ];
3911
3912 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * 300 * AI3;
3913 let init_total_storage_fund = STORAGE_FEE_RESERVE * 300 * AI3;
3914
3915 let mut ext = new_test_ext();
3916 ext.execute_with(|| {
3917 let (operator_id, _) = register_operator(
3918 domain_id,
3919 operator_account,
3920 operator_free_balance,
3921 operator_stake,
3922 10 * AI3,
3923 pair.public(),
3924 Default::default(),
3925 BTreeMap::from_iter(nominators),
3926 );
3927
3928 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3929 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3930 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
3931
3932 let operator = Operators::<Test>::get(operator_id).unwrap();
3933 assert_eq!(operator.current_total_stake, init_total_stake);
3934 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
3935 assert_eq!(
3936 operator.total_storage_fee_deposit,
3937 bundle_storage_fund::total_balance::<Test>(operator_id)
3938 );
3939
3940 for unlock in &unlocking {
3941 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
3942 }
3943
3944 do_reward_operators::<Test>(
3945 domain_id,
3946 OperatorRewardSource::Dummy,
3947 vec![operator_id].into_iter(),
3948 20 * AI3,
3949 )
3950 .unwrap();
3951 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
3952
3953 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3955 for id in [operator_account, nominator_account] {
3956 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
3957 do_convert_previous_epoch_withdrawal::<Test>(
3958 operator_id,
3959 maybe_withdrawal.as_mut().unwrap(),
3960 domain_stake_summary.current_epoch_index,
3961 )
3962 })
3963 .unwrap();
3964 }
3965
3966 let operator = Operators::<Test>::get(operator_id).unwrap();
3969 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
3970 let operator_withdrawal =
3971 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
3972 let nominator_withdrawal =
3973 Withdrawals::<Test>::get(operator_id, nominator_account).unwrap();
3974
3975 let total_deposit =
3976 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
3977 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
3978 + nominator_withdrawal.total_withdrawal_amount;
3979 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
3980 .storage_fee_refund
3981 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
3982 assert_eq!(293333333333333333336, total_deposit,);
3983 assert_eq!(21666666666666666664, total_stake_withdrawal);
3984 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
3985 assert_eq!(
3986 320 * AI3,
3987 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
3988 );
3989 assert_eq!(
3990 operator.total_storage_fee_deposit,
3991 bundle_storage_fund::total_balance::<Test>(operator_id)
3992 );
3993
3994 for deposit in deposits {
3995 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
3996 }
3997
3998 do_mark_operators_as_slashed::<Test>(
3999 vec![operator_id],
4000 SlashedReason::InvalidBundle(1),
4001 )
4002 .unwrap();
4003
4004 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4005 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4006
4007 let operator = Operators::<Test>::get(operator_id).unwrap();
4008 assert_eq!(
4009 *operator.status::<Test>(operator_id),
4010 OperatorStatus::Slashed
4011 );
4012
4013 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4014 assert!(pending_slashes.contains(&operator_id));
4015
4016 assert_eq!(
4017 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4018 0
4019 );
4020
4021 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4022 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4023 assert_eq!(Operators::<Test>::get(operator_id), None);
4024 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4025
4026 assert_eq!(
4027 Balances::total_balance(&operator_account),
4028 operator_free_balance - operator_stake
4029 );
4030 assert_eq!(
4031 Balances::total_balance(&nominator_account),
4032 nominator_free_balance - nominator_stake
4033 );
4034
4035 assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3);
4036 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4037 });
4038 }
4039
4040 #[test]
4041 fn slash_operator_with_more_than_max_nominators_to_slash() {
4042 let domain_id = DomainId::new(0);
4043 let operator_account = 1;
4044 let operator_free_balance = 250 * AI3;
4045 let operator_stake = 200 * AI3;
4046 let operator_extra_deposit = 40 * AI3;
4047 let operator_extra_withdraw = 5 * AI3;
4048 let pair = OperatorPair::from_seed(&[0; 32]);
4049
4050 let nominator_accounts: Vec<crate::tests::AccountId> = (2..22).collect();
4051 let nominator_free_balance = 150 * AI3;
4052 let nominator_stake = 100 * AI3;
4053 let nominator_extra_deposit = 40 * AI3;
4054 let nominator_extra_withdraw = 5 * AI3;
4055
4056 let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))];
4057 for nominator_account in nominator_accounts.clone() {
4058 nominators.push((nominator_account, (nominator_free_balance, nominator_stake)))
4059 }
4060
4061 let last_nominator_account = nominator_accounts.last().cloned().unwrap();
4062 let unlocking = vec![
4063 (operator_account, 10 * AI3),
4064 (last_nominator_account, 10 * AI3),
4065 ];
4066
4067 let deposits = vec![
4068 (operator_account, operator_extra_deposit),
4069 (last_nominator_account, nominator_extra_deposit),
4070 ];
4071 let withdrawals = vec![
4072 (operator_account, operator_extra_withdraw),
4073 (last_nominator_account, nominator_extra_withdraw),
4074 ];
4075
4076 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one()
4077 * (200 + (100 * nominator_accounts.len() as u128))
4078 * AI3;
4079 let init_total_storage_fund =
4080 STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * AI3;
4081
4082 let mut ext = new_test_ext();
4083 ext.execute_with(|| {
4084 let (operator_id, _) = register_operator(
4085 domain_id,
4086 operator_account,
4087 operator_free_balance,
4088 operator_stake,
4089 10 * AI3,
4090 pair.public(),
4091 Default::default(),
4092 BTreeMap::from_iter(nominators),
4093 );
4094
4095 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4096 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4097 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4098
4099 let operator = Operators::<Test>::get(operator_id).unwrap();
4100 assert_eq!(operator.current_total_stake, init_total_stake);
4101 assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund);
4102 assert_eq!(
4103 operator.total_storage_fee_deposit,
4104 bundle_storage_fund::total_balance::<Test>(operator_id)
4105 );
4106
4107 for unlock in &unlocking {
4108 do_withdraw_stake::<Test>(operator_id, unlock.0, unlock.1).unwrap();
4109 }
4110
4111 do_reward_operators::<Test>(
4112 domain_id,
4113 OperatorRewardSource::Dummy,
4114 vec![operator_id].into_iter(),
4115 20 * AI3,
4116 )
4117 .unwrap();
4118 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4119
4120 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4122 for id in [operator_account, last_nominator_account] {
4123 Withdrawals::<Test>::try_mutate(operator_id, id, |maybe_withdrawal| {
4124 do_convert_previous_epoch_withdrawal::<Test>(
4125 operator_id,
4126 maybe_withdrawal.as_mut().unwrap(),
4127 domain_stake_summary.current_epoch_index,
4128 )
4129 })
4130 .unwrap();
4131 }
4132
4133 let operator = Operators::<Test>::get(operator_id).unwrap();
4136 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4137 let operator_withdrawal =
4138 Withdrawals::<Test>::get(operator_id, operator_account).unwrap();
4139 let nominator_withdrawal =
4140 Withdrawals::<Test>::get(operator_id, last_nominator_account).unwrap();
4141
4142 let total_deposit =
4143 domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit;
4144 let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount
4145 + nominator_withdrawal.total_withdrawal_amount;
4146 let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0]
4147 .storage_fee_refund
4148 + nominator_withdrawal.withdrawals[0].storage_fee_refund;
4149 assert_eq!(2194772727272727272734, total_deposit,);
4150 assert_eq!(20227272727272727266, total_stake_withdrawal);
4151 assert_eq!(5000000000000000000, total_storage_fee_withdrawal);
4152 assert_eq!(
4153 2220 * AI3,
4154 total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal
4155 );
4156
4157 assert_eq!(
4158 operator.total_storage_fee_deposit,
4159 bundle_storage_fund::total_balance::<Test>(operator_id)
4160 );
4161
4162 for deposit in deposits {
4163 do_nominate_operator::<Test>(operator_id, deposit.0, deposit.1).unwrap();
4164 }
4165 for withdrawal in withdrawals {
4166 do_withdraw_stake::<Test>(
4167 operator_id,
4168 withdrawal.0,
4169 withdrawal.1,
4172 )
4173 .unwrap();
4174 }
4175
4176 do_mark_operators_as_slashed::<Test>(
4177 vec![operator_id],
4178 SlashedReason::InvalidBundle(1),
4179 )
4180 .unwrap();
4181
4182 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4183
4184 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4185 assert!(!domain_stake_summary.next_operators.contains(&operator_id));
4186
4187 let operator = Operators::<Test>::get(operator_id).unwrap();
4188 assert_eq!(
4189 *operator.status::<Test>(operator_id),
4190 OperatorStatus::Slashed
4191 );
4192
4193 let pending_slashes = PendingSlashes::<Test>::get(domain_id).unwrap();
4194 assert!(pending_slashes.contains(&operator_id));
4195
4196 assert_eq!(
4197 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4198 0
4199 );
4200
4201 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4204 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4205 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4206
4207 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4208 assert_eq!(Operators::<Test>::get(operator_id), None);
4209 assert_eq!(OperatorIdOwner::<Test>::get(operator_id), None);
4210
4211 assert_eq!(
4212 Balances::total_balance(&operator_account),
4213 operator_free_balance - operator_stake
4214 );
4215 for nominator_account in nominator_accounts {
4216 assert_eq!(
4217 Balances::total_balance(&nominator_account),
4218 nominator_free_balance - nominator_stake
4219 );
4220 }
4221
4222 assert_eq!(
4223 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4224 2220 * AI3
4225 );
4226 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4227 });
4228 }
4229
4230 #[test]
4231 fn slash_operators() {
4232 let domain_id = DomainId::new(0);
4233 let operator_free_balance = 250 * AI3;
4234 let operator_stake = 200 * AI3;
4235
4236 let operator_account_1 = 1;
4237 let operator_account_2 = 2;
4238 let operator_account_3 = 3;
4239
4240 let pair_1 = OperatorPair::from_seed(&[0; 32]);
4241 let pair_2 = OperatorPair::from_seed(&[1; 32]);
4242 let pair_3 = OperatorPair::from_seed(&[2; 32]);
4243
4244 let mut ext = new_test_ext();
4245 ext.execute_with(|| {
4246 let (operator_id_1, _) = register_operator(
4247 domain_id,
4248 operator_account_1,
4249 operator_free_balance,
4250 operator_stake,
4251 10 * AI3,
4252 pair_1.public(),
4253 Default::default(),
4254 Default::default(),
4255 );
4256
4257 let (operator_id_2, _) = register_operator(
4258 domain_id,
4259 operator_account_2,
4260 operator_free_balance,
4261 operator_stake,
4262 10 * AI3,
4263 pair_2.public(),
4264 Default::default(),
4265 Default::default(),
4266 );
4267
4268 let (operator_id_3, _) = register_operator(
4269 domain_id,
4270 operator_account_3,
4271 operator_free_balance,
4272 operator_stake,
4273 10 * AI3,
4274 pair_3.public(),
4275 Default::default(),
4276 Default::default(),
4277 );
4278
4279 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4280 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4281 assert!(domain_stake_summary.next_operators.contains(&operator_id_1));
4282 assert!(domain_stake_summary.next_operators.contains(&operator_id_2));
4283 assert!(domain_stake_summary.next_operators.contains(&operator_id_3));
4284 assert_eq!(
4285 domain_stake_summary.current_total_stake,
4286 STORAGE_FEE_RESERVE.left_from_one() * 600 * AI3
4287 );
4288 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4289 let operator = Operators::<Test>::get(operator_id).unwrap();
4290 assert_eq!(
4291 operator.total_storage_fee_deposit,
4292 STORAGE_FEE_RESERVE * operator_stake
4293 );
4294 assert_eq!(
4295 operator.total_storage_fee_deposit,
4296 bundle_storage_fund::total_balance::<Test>(operator_id)
4297 );
4298 }
4299
4300 let res = Domains::deactivate_operator(RuntimeOrigin::root(), operator_id_3);
4302 assert_ok!(res);
4303 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4304
4305 let operator = Operators::<Test>::get(operator_id_3).unwrap();
4306 assert_eq!(
4307 *operator.status::<Test>(operator_id_3),
4308 OperatorStatus::Deactivated(7)
4309 );
4310
4311 do_mark_operators_as_slashed::<Test>(
4312 vec![operator_id_1],
4313 SlashedReason::InvalidBundle(1),
4314 )
4315 .unwrap();
4316 do_mark_operators_as_slashed::<Test>(
4317 vec![operator_id_2],
4318 SlashedReason::InvalidBundle(2),
4319 )
4320 .unwrap();
4321 do_mark_operators_as_slashed::<Test>(
4322 vec![operator_id_3],
4323 SlashedReason::InvalidBundle(3),
4324 )
4325 .unwrap();
4326
4327 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4328 assert!(!domain_stake_summary.next_operators.contains(&operator_id_1));
4329 assert!(!domain_stake_summary.next_operators.contains(&operator_id_2));
4330 assert!(!domain_stake_summary.next_operators.contains(&operator_id_3));
4331
4332 let operator = Operators::<Test>::get(operator_id_1).unwrap();
4333 assert_eq!(
4334 *operator.status::<Test>(operator_id_1),
4335 OperatorStatus::Slashed
4336 );
4337
4338 let operator = Operators::<Test>::get(operator_id_2).unwrap();
4339 assert_eq!(
4340 *operator.status::<Test>(operator_id_2),
4341 OperatorStatus::Slashed
4342 );
4343
4344 let operator = Operators::<Test>::get(operator_id_3).unwrap();
4345 assert_eq!(
4346 *operator.status::<Test>(operator_id_3),
4347 OperatorStatus::Slashed
4348 );
4349
4350 assert_eq!(
4351 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4352 0
4353 );
4354
4355 let slashed_operators = PendingSlashes::<Test>::get(domain_id).unwrap();
4356 slashed_operators.into_iter().for_each(|_| {
4357 do_slash_operator::<Test>(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap();
4358 });
4359
4360 assert_eq!(PendingSlashes::<Test>::get(domain_id), None);
4361 assert_eq!(Operators::<Test>::get(operator_id_1), None);
4362 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_1), None);
4363 assert_eq!(Operators::<Test>::get(operator_id_2), None);
4364 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_2), None);
4365 assert_eq!(Operators::<Test>::get(operator_id_3), None);
4366 assert_eq!(OperatorIdOwner::<Test>::get(operator_id_3), None);
4367
4368 assert_eq!(
4369 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4370 600 * AI3
4371 );
4372 for operator_id in [operator_id_1, operator_id_2, operator_id_3] {
4373 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4374 }
4375 });
4376 }
4377
4378 #[test]
4379 fn bundle_storage_fund_charged_and_refund_storage_fee() {
4380 let domain_id = DomainId::new(0);
4381 let operator_account = 1;
4382 let operator_free_balance = 150 * AI3;
4383 let operator_total_stake = 100 * AI3;
4384 let operator_stake = 80 * AI3;
4385 let operator_storage_fee_deposit = 20 * AI3;
4386 let pair = OperatorPair::from_seed(&[0; 32]);
4387 let nominator_account = 2;
4388
4389 let mut ext = new_test_ext();
4390 ext.execute_with(|| {
4391 let (operator_id, _) = register_operator(
4392 domain_id,
4393 operator_account,
4394 operator_free_balance,
4395 operator_total_stake,
4396 AI3,
4397 pair.public(),
4398 Default::default(),
4399 BTreeMap::default(),
4400 );
4401
4402 let domain_staking_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4403 assert_eq!(domain_staking_summary.current_total_stake, operator_stake);
4404
4405 let operator = Operators::<Test>::get(operator_id).unwrap();
4406 assert_eq!(operator.current_total_stake, operator_stake);
4407 assert_eq!(operator.current_total_shares, operator_stake);
4408 assert_eq!(
4409 operator.total_storage_fee_deposit,
4410 operator_storage_fee_deposit
4411 );
4412
4413 bundle_storage_fund::charge_bundle_storage_fee::<Test>(
4415 operator_id,
4416 (operator_storage_fee_deposit / AI3) as u32,
4418 )
4419 .unwrap();
4420 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4421 assert_err!(
4422 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1,),
4423 bundle_storage_fund::Error::BundleStorageFeePayment
4424 );
4425
4426 do_nominate_operator::<Test>(operator_id, operator_account, 5 * AI3).unwrap();
4428 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4429
4430 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4431 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4432
4433 Balances::set_balance(&nominator_account, 100 * AI3);
4435 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4436 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4437
4438 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4439 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4440
4441 bundle_storage_fund::refund_storage_fee::<Test>(
4443 10 * AI3,
4444 BTreeMap::from_iter([(operator_id, 1), (operator_id + 1, 9)]),
4445 )
4446 .unwrap();
4447 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), AI3);
4448
4449 assert_eq!(
4451 Balances::total_balance(&crate::tests::TreasuryAccount::get()),
4452 9 * AI3
4453 );
4454
4455 bundle_storage_fund::charge_bundle_storage_fee::<Test>(operator_id, 1).unwrap();
4456 assert_eq!(bundle_storage_fund::total_balance::<Test>(operator_id), 0);
4457 });
4458 }
4459
4460 #[test]
4461 fn zero_amount_deposit_and_withdraw() {
4462 let domain_id = DomainId::new(0);
4463 let operator_account = 1;
4464 let operator_free_balance = 250 * AI3;
4465 let operator_stake = 200 * AI3;
4466 let pair = OperatorPair::from_seed(&[0; 32]);
4467 let nominator_account = 2;
4468 let nominator_free_balance = 150 * AI3;
4469 let nominator_stake = 100 * AI3;
4470
4471 let nominators = vec![
4472 (operator_account, (operator_free_balance, operator_stake)),
4473 (nominator_account, (nominator_free_balance, nominator_stake)),
4474 ];
4475
4476 let total_deposit = 300 * AI3;
4477 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4478
4479 let mut ext = new_test_ext();
4480 ext.execute_with(|| {
4481 let (operator_id, _) = register_operator(
4482 domain_id,
4483 operator_account,
4484 operator_free_balance,
4485 operator_stake,
4486 10 * AI3,
4487 pair.public(),
4488 Default::default(),
4489 BTreeMap::from_iter(nominators),
4490 );
4491
4492 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4493 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4494 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4495
4496 assert_err!(
4498 do_nominate_operator::<Test>(operator_id, nominator_account, 0),
4499 StakingError::ZeroDeposit
4500 );
4501
4502 assert_err!(
4504 do_withdraw_stake::<Test>(operator_id, nominator_account, 0),
4505 StakingError::ZeroWithdraw
4506 );
4507
4508 do_withdraw_stake::<Test>(
4510 operator_id,
4511 nominator_account,
4512 STORAGE_FEE_RESERVE.left_from_one() * operator_stake - MinOperatorStake::get(),
4514 )
4515 .unwrap();
4516 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4517 });
4518 }
4519
4520 #[test]
4521 fn deposit_and_withdraw_should_be_rejected_due_to_missing_share_price() {
4522 let domain_id = DomainId::new(0);
4523 let operator_account = 1;
4524 let operator_free_balance = 250 * AI3;
4525 let operator_stake = 200 * AI3;
4526 let pair = OperatorPair::from_seed(&[0; 32]);
4527 let nominator_account = 2;
4528 let nominator_free_balance = 150 * AI3;
4529 let nominator_stake = 100 * AI3;
4530
4531 let nominators = vec![
4532 (operator_account, (operator_free_balance, operator_stake)),
4533 (nominator_account, (nominator_free_balance, nominator_stake)),
4534 ];
4535
4536 let total_deposit = 300 * AI3;
4537 let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() * total_deposit;
4538
4539 let mut ext = new_test_ext();
4540 ext.execute_with(|| {
4541 let (operator_id, _) = register_operator(
4542 domain_id,
4543 operator_account,
4544 operator_free_balance,
4545 operator_stake,
4546 10 * AI3,
4547 pair.public(),
4548 Default::default(),
4549 BTreeMap::from_iter(nominators),
4550 );
4551
4552 do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4553 let domain_stake_summary = DomainStakingSummary::<Test>::get(domain_id).unwrap();
4554 assert_eq!(domain_stake_summary.current_total_stake, init_total_stake);
4555
4556 do_nominate_operator::<Test>(operator_id, nominator_account, 5 * AI3).unwrap();
4557 do_withdraw_stake::<Test>(operator_id, nominator_account, 3 * AI3).unwrap();
4559
4560 let previous_epoch = do_finalize_domain_current_epoch::<Test>(domain_id).unwrap();
4562 OperatorEpochSharePrice::<Test>::remove(
4564 operator_id,
4565 DomainEpoch::from((domain_id, previous_epoch.completed_epoch_index)),
4566 );
4567
4568 assert_err!(
4570 do_nominate_operator::<Test>(operator_id, nominator_account, AI3),
4571 StakingError::MissingOperatorEpochSharePrice
4572 );
4573 assert_err!(
4574 do_withdraw_stake::<Test>(operator_id, nominator_account, 1),
4575 StakingError::MissingOperatorEpochSharePrice
4576 );
4577 });
4578 }
4579
4580 #[test]
4581 fn test_share_price_deposit() {
4582 let total_shares = 45 * AI3;
4583 let total_stake = 45 * AI3 + 37;
4584 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4585
4586 let to_deposit_stakes = [
4588 5,
4589 7,
4590 9,
4591 11,
4592 17,
4593 23,
4594 934,
4595 24931,
4596 349083467,
4597 2 * AI3 + 32,
4598 52 * AI3 - 4729034,
4599 2732 * AI3 - 1720,
4600 1117 * AI3 + 1839832,
4601 31232 * AI3 - 987654321,
4602 ];
4603
4604 let mut deposited_share = 0;
4605 let mut deposited_stake = 0;
4606 for to_deposit_stake in to_deposit_stakes {
4607 let to_deposit_share = sp.stake_to_shares::<Test>(to_deposit_stake);
4608
4609 deposited_stake += to_deposit_stake;
4611 deposited_share += to_deposit_share;
4614
4615 let total_deposited_share = sp.stake_to_shares::<Test>(deposited_stake);
4620
4621 assert!(total_deposited_share >= deposited_share);
4626
4627 assert!(total_stake + deposited_stake > total_shares + total_deposited_share);
4630 }
4631 }
4632
4633 #[test]
4634 fn test_share_price_withdraw() {
4635 let total_shares = 123 * AI3;
4636 let total_stake = 123 * AI3 + 13;
4637 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4638
4639 let to_withdraw_shares = [
4641 1,
4642 3,
4643 7,
4644 13,
4645 17,
4646 123,
4647 43553,
4648 546393039,
4649 15 * AI3 + 1342,
4650 2 * AI3 - 423,
4651 31 * AI3 - 1321,
4652 42 * AI3 + 4564234,
4653 7 * AI3 - 987654321,
4654 3 * AI3 + 987654321123879,
4655 ];
4656
4657 let mut withdrawn_share = 0;
4658 let mut withdrawn_stake = 0;
4659 for to_withdraw_share in to_withdraw_shares {
4660 let to_withdraw_stake = sp.shares_to_stake::<Test>(to_withdraw_share);
4661
4662 withdrawn_share += to_withdraw_share;
4664 withdrawn_stake += to_withdraw_stake;
4667
4668 let total_withdrawn_stake = sp.shares_to_stake::<Test>(withdrawn_share);
4673
4674 assert!(total_withdrawn_stake >= withdrawn_stake);
4678
4679 assert!(total_stake - withdrawn_stake >= total_shares - withdrawn_share);
4682 }
4683 }
4684
4685 #[test]
4686 fn test_share_price_unlock() {
4687 let mut total_shares = 20 * AI3;
4688 let mut total_stake = 20 * AI3 + 12;
4689
4690 for to_unlock_share in [
4696 AI3 + 123,
4697 2 * AI3 - 456,
4698 3 * AI3 - 789,
4699 4 * AI3 - 123 + 456,
4700 7 * AI3 + 789 - 987654321,
4701 3 * AI3 + 987654321,
4702 ] {
4703 let sp = SharePrice::new::<Test>(total_shares, total_stake).unwrap();
4704
4705 let to_unlock_stake = sp.shares_to_stake::<Test>(to_unlock_share);
4706
4707 total_shares = total_shares.checked_sub(to_unlock_share).unwrap();
4708 total_stake = total_stake.checked_sub(to_unlock_stake).unwrap();
4709 }
4710 assert_eq!(total_shares, 0);
4711 }
4712}