Skip to content

Commit fe1d4cd

Browse files
authored
Merge pull request #2419 from subspace/constant_withdrawal
Staking: ensure withdrawals are constant compute while unlocking or slashing operator
2 parents d12871b + 050cc61 commit fe1d4cd

File tree

3 files changed

+145
-150
lines changed

3 files changed

+145
-150
lines changed

crates/pallet-domains/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ mod pallet {
169169
use sp_std::boxed::Box;
170170
use sp_std::collections::btree_map::BTreeMap;
171171
use sp_std::collections::btree_set::BTreeSet;
172-
use sp_std::collections::vec_deque::VecDeque;
173172
use sp_std::fmt::Debug;
174173
use sp_std::vec;
175174
use sp_std::vec::Vec;
@@ -410,7 +409,7 @@ mod pallet {
410409
OperatorId,
411410
Identity,
412411
NominatorId<T>,
413-
VecDeque<Withdrawal<T::Share, DomainBlockNumberFor<T>>>,
412+
Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
414413
OptionQuery,
415414
>;
416415

crates/pallet-domains/src/staking.rs

Lines changed: 119 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use sp_runtime::traits::{CheckedAdd, CheckedSub, One, Zero};
2121
use sp_runtime::{Perbill, Percent, Saturating};
2222
use sp_std::collections::btree_map::BTreeMap;
2323
use sp_std::collections::btree_set::BTreeSet;
24+
use sp_std::collections::vec_deque::VecDeque;
2425
use sp_std::iter::Iterator;
2526
use sp_std::vec::IntoIter;
2627

@@ -48,17 +49,20 @@ impl SharePrice {
4849

4950
/// Converts stake to shares based on the share price
5051
pub(crate) fn stake_to_shares<T: Config>(&self, stake: BalanceOf<T>) -> T::Share {
51-
self.0.mul_floor(stake).into()
52+
if self.0.is_one() {
53+
stake.into()
54+
} else {
55+
self.0.mul_floor(stake).into()
56+
}
5257
}
5358

5459
/// Converts shares to stake based on the share price
5560
pub(crate) fn shares_to_stake<T: Config>(&self, shares: T::Share) -> BalanceOf<T> {
56-
self.0.saturating_reciprocal_mul_floor(shares.into())
57-
}
58-
59-
/// Returns true if the share price is one
60-
pub(crate) fn is_one(&self) -> bool {
61-
self.0.is_one()
61+
if self.0.is_one() {
62+
shares.into()
63+
} else {
64+
self.0.saturating_reciprocal_mul_floor(shares.into())
65+
}
6266
}
6367
}
6468

@@ -92,11 +96,15 @@ pub(crate) struct PendingDeposit<Balance: Copy> {
9296
}
9397

9498
/// A nominator's withdrawal from a given operator pool.
95-
#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
96-
pub(crate) struct Withdrawal<Share, DomainBlockNumber> {
97-
pub(crate) allowed_since_domain_epoch: DomainEpoch,
98-
pub(crate) unlock_at_confirmed_domain_block_number: DomainBlockNumber,
99-
pub(crate) shares: Share,
99+
#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
100+
pub(crate) struct Withdrawal<Balance, Share, DomainBlockNumber> {
101+
/// Total withdrawal amount requested by the nominator that are in unlocking state excluding withdrawal in shares.
102+
pub(crate) total_withdrawal_amount: Balance,
103+
/// Individual withdrawal amounts with their unlocking block for a given domain
104+
pub(crate) withdrawals: VecDeque<(DomainId, DomainBlockNumber, Balance)>,
105+
/// Withdrawal that was initiated by nominator and not converted to balance due to
106+
/// unfinished domain epoch.
107+
pub(crate) withdrawal_in_shares: Option<(DomainEpoch, DomainBlockNumber, Share)>,
100108
}
101109

102110
#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
@@ -328,7 +336,7 @@ pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T:
328336
) -> Result<DepositInfo<BalanceOf<T>>, Error> {
329337
Deposits::<T>::try_mutate(operator_id, nominator_id, |maybe_deposit| {
330338
let mut deposit = maybe_deposit.take().unwrap_or_default();
331-
do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, current_domain_epoch)?;
339+
do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit)?;
332340

333341
// add or create new pending deposit
334342
let (pending_deposit, deposit_info) = match deposit.pending {
@@ -374,19 +382,14 @@ pub(crate) fn do_calculate_previous_epoch_deposit_shares_and_add_new_deposit<T:
374382
pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
375383
operator_id: OperatorId,
376384
deposit: &mut Deposit<T::Share, BalanceOf<T>>,
377-
current_domain_epoch: DomainEpoch,
378385
) -> Result<(), Error> {
379386
let maybe_pending_deposit = deposit.pending.take();
380387
// if it is one of the previous domain epoch, then calculate shares for the epoch and update known deposit
381388
deposit.pending = if let Some(PendingDeposit {
382389
effective_domain_epoch,
383390
amount,
384-
}) = maybe_pending_deposit && effective_domain_epoch != current_domain_epoch
391+
}) = maybe_pending_deposit && let Some(epoch_share_price) = OperatorEpochSharePrice::<T>::get(operator_id, effective_domain_epoch)
385392
{
386-
let epoch_share_price =
387-
OperatorEpochSharePrice::<T>::get(operator_id, effective_domain_epoch)
388-
.ok_or(Error::MissingOperatorEpochSharePrice)?;
389-
390393
let new_shares = epoch_share_price.stake_to_shares::<T>(amount);
391394
deposit.known.shares = deposit
392395
.known
@@ -401,6 +404,32 @@ pub(crate) fn do_convert_previous_epoch_deposits<T: Config>(
401404
Ok(())
402405
}
403406

407+
/// Converts any epoch withdrawals into balance using the operator epoch share price.
408+
/// If there is no share price available, this will be no-op
409+
pub(crate) fn do_convert_previous_epoch_withdrawal<T: Config>(
410+
operator_id: OperatorId,
411+
withdrawal: &mut Withdrawal<BalanceOf<T>, T::Share, DomainBlockNumberFor<T>>,
412+
) -> Result<(), Error> {
413+
let withdrawal_in_shares = withdrawal.withdrawal_in_shares.take();
414+
withdrawal.withdrawal_in_shares =
415+
if let Some((domain_epoch, domain_block_number, shares)) = withdrawal_in_shares && let Some(epoch_share_price) = OperatorEpochSharePrice::<T>::get(operator_id, domain_epoch){
416+
let withdrawal_amount = epoch_share_price.shares_to_stake::<T>(shares);
417+
withdrawal.total_withdrawal_amount = withdrawal
418+
.total_withdrawal_amount
419+
.checked_add(&withdrawal_amount)
420+
.ok_or(Error::BalanceOverflow)?;
421+
let (domain_id, _) = domain_epoch.deconstruct();
422+
withdrawal
423+
.withdrawals
424+
.push_back((domain_id, domain_block_number, withdrawal_amount));
425+
None
426+
} else {
427+
withdrawal_in_shares
428+
};
429+
430+
Ok(())
431+
}
432+
404433
pub(crate) fn do_nominate_operator<T: Config>(
405434
operator_id: OperatorId,
406435
nominator_id: T::AccountId,
@@ -621,7 +650,15 @@ pub(crate) fn do_withdraw_stake<T: Config>(
621650

622651
Deposits::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_deposit| {
623652
let deposit = maybe_deposit.as_mut().ok_or(Error::InsufficientShares)?;
624-
do_convert_previous_epoch_deposits::<T>(operator_id, deposit, domain_current_epoch)?;
653+
do_convert_previous_epoch_deposits::<T>(operator_id, deposit)?;
654+
Ok(())
655+
})?;
656+
657+
Withdrawals::<T>::try_mutate(operator_id, nominator_id.clone(), |maybe_withdrawal| {
658+
if let Some(withdrawal) = maybe_withdrawal {
659+
do_convert_previous_epoch_withdrawal::<T>(operator_id, withdrawal)?;
660+
}
661+
625662
Ok(())
626663
})?;
627664

@@ -710,21 +747,31 @@ pub(crate) fn do_withdraw_stake<T: Config>(
710747
.checked_add(&T::StakeWithdrawalLockingPeriod::get())
711748
.ok_or(Error::BlockNumberOverflow)?;
712749

713-
Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawals| {
714-
let mut withdrawals = maybe_withdrawals.take().unwrap_or_default();
715-
// if there is an existing withdrawal within the same epoch, then update it instead
716-
if let Some(withdrawal) = withdrawals.back_mut() && withdrawal.allowed_since_domain_epoch == domain_current_epoch {
717-
withdrawal.shares = withdrawal.shares.checked_add(&shares_withdrew).ok_or(Error::ShareOverflow)?;
718-
withdrawal.unlock_at_confirmed_domain_block_number = unlock_at_confirmed_domain_block_number;
719-
} else {
720-
withdrawals.push_back(Withdrawal {
721-
allowed_since_domain_epoch: domain_current_epoch,
722-
unlock_at_confirmed_domain_block_number,
723-
shares: shares_withdrew,
724-
});
725-
}
750+
Withdrawals::<T>::try_mutate(operator_id, nominator_id, |maybe_withdrawal| {
751+
let mut withdrawal = maybe_withdrawal.take().unwrap_or_default();
752+
// if this is some, then the withdrawal was initiated in this current epoch due to conversion
753+
// of previous epoch withdrawals from shares to balances above. So just update it instead
754+
let withdrawals_in_shares = withdrawal.withdrawal_in_shares.take();
755+
withdrawal.withdrawal_in_shares = Some(
756+
if let Some((domain_epoch, _, shares)) = withdrawals_in_shares {
757+
let updated_shares = shares
758+
.checked_add(&shares_withdrew)
759+
.ok_or(Error::ShareOverflow)?;
760+
(
761+
domain_epoch,
762+
unlock_at_confirmed_domain_block_number,
763+
updated_shares,
764+
)
765+
} else {
766+
(
767+
domain_current_epoch,
768+
unlock_at_confirmed_domain_block_number,
769+
shares_withdrew,
770+
)
771+
},
772+
);
726773

727-
*maybe_withdrawals = Some(withdrawals);
774+
*maybe_withdrawal = Some(withdrawal);
728775
Ok(())
729776
})
730777
})
@@ -742,35 +789,29 @@ pub(crate) fn do_unlock_funds<T: Config>(
742789
Error::OperatorNotRegistered
743790
);
744791

745-
Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawals| {
746-
let withdrawals = maybe_withdrawals.as_mut().ok_or(Error::MissingWithdrawal)?;
747-
let Withdrawal {
748-
allowed_since_domain_epoch,
749-
unlock_at_confirmed_domain_block_number,
750-
shares,
751-
} = withdrawals.pop_front().ok_or(Error::MissingWithdrawal)?;
792+
Withdrawals::<T>::try_mutate_exists(operator_id, nominator_id.clone(), |maybe_withdrawal| {
793+
let withdrawal = maybe_withdrawal.as_mut().ok_or(Error::MissingWithdrawal)?;
794+
do_convert_previous_epoch_withdrawal::<T>(operator_id, withdrawal)?;
795+
let (domain_id, unlock_at_confirmed_domain_block_number, amount_to_unlock) = withdrawal
796+
.withdrawals
797+
.pop_front()
798+
.ok_or(Error::MissingWithdrawal)?;
752799

753-
let (domain_id, _) = allowed_since_domain_epoch.deconstruct();
754800
let latest_confirmed_block_number = LatestConfirmedDomainBlockNumber::<T>::get(domain_id);
755801
ensure!(
756802
unlock_at_confirmed_domain_block_number <= latest_confirmed_block_number,
757803
Error::UnlockPeriodNotComplete
758804
);
759805

806+
// deduct the amount unlocked from total
807+
withdrawal.total_withdrawal_amount = withdrawal
808+
.total_withdrawal_amount
809+
.checked_sub(&amount_to_unlock)
810+
.ok_or(Error::BalanceUnderflow)?;
811+
760812
let staked_hold_id = T::HoldIdentifier::staking_staked(operator_id);
761813
let locked_amount = T::Currency::balance_on_hold(&staked_hold_id, &nominator_id);
762-
let amount_to_unlock: BalanceOf<T> = {
763-
let epoch_share_price =
764-
OperatorEpochSharePrice::<T>::get(operator_id, allowed_since_domain_epoch)
765-
.ok_or(Error::MissingOperatorEpochSharePrice)?;
766-
767-
// if the share price is one, just convert shares to ssc
768-
let amount_to_unlock = if epoch_share_price.is_one() {
769-
shares.into()
770-
} else {
771-
epoch_share_price.shares_to_stake::<T>(shares)
772-
};
773-
814+
let amount_to_release: BalanceOf<T> = {
774815
// if the amount to release is more than currently locked,
775816
// mint the diff and release the rest
776817
if let Some(amount_to_mint) = amount_to_unlock.checked_sub(&locked_amount) {
@@ -785,14 +826,14 @@ pub(crate) fn do_unlock_funds<T: Config>(
785826
T::Currency::release(
786827
&staked_hold_id,
787828
&nominator_id,
788-
amount_to_unlock,
829+
amount_to_release,
789830
Precision::Exact,
790831
)
791832
.map_err(|_| Error::RemoveLock)?;
792833

793834
// if there are no withdrawals, then delete the storage as well
794-
if withdrawals.is_empty() {
795-
*maybe_withdrawals = None;
835+
if withdrawal.withdrawals.is_empty() && withdrawal.withdrawal_in_shares.is_none() {
836+
*maybe_withdrawal = None;
796837
// if there is no deposit or pending deposits, then clean up the deposit state as well
797838
Deposits::<T>::mutate_exists(operator_id, nominator_id, |maybe_deposit| {
798839
if let Some(deposit) = maybe_deposit && deposit.known.shares.is_zero() && deposit.pending.is_none() {
@@ -805,54 +846,6 @@ pub(crate) fn do_unlock_funds<T: Config>(
805846
})
806847
}
807848

808-
/// Converts shares to ssc based on the OPerator epoch share price
809-
/// if the share price is not available, then return the shares as is
810-
fn convert_shares_to_ssc<T: Config>(
811-
operator_id: OperatorId,
812-
domain_epoch: DomainEpoch,
813-
shares: T::Share,
814-
) -> (BalanceOf<T>, T::Share) {
815-
match OperatorEpochSharePrice::<T>::get(operator_id, domain_epoch) {
816-
None => (Zero::zero(), shares),
817-
Some(epoch_share_price) => {
818-
// if the share price is one, just convert shares to ssc
819-
(
820-
if epoch_share_price.is_one() {
821-
shares.into()
822-
} else {
823-
epoch_share_price.shares_to_stake::<T>(shares)
824-
},
825-
Zero::zero(),
826-
)
827-
}
828-
}
829-
}
830-
// If there are any withdrawals for previous epoch, then calculate the SSC for those shares
831-
// using the share price at which the withdraw was initiated.
832-
// If there are any withdrawals during the current epoch, it share price is not calculated yet
833-
// so just return the shares as is
834-
pub(crate) fn calculate_withdraw_share_ssc<T: Config>(
835-
operator_id: OperatorId,
836-
nominator_id: NominatorId<T>,
837-
) -> (BalanceOf<T>, T::Share) {
838-
let withdrawals = Withdrawals::<T>::take(operator_id, nominator_id.clone()).unwrap_or_default();
839-
withdrawals.into_iter().fold(
840-
(BalanceOf::<T>::zero(), T::Share::zero()),
841-
|(total_amount, total_shares), withdraw| {
842-
let (amount, shares) = convert_shares_to_ssc::<T>(
843-
operator_id,
844-
withdraw.allowed_since_domain_epoch,
845-
withdraw.shares,
846-
);
847-
848-
(
849-
amount.saturating_add(total_amount),
850-
shares.saturating_add(total_shares),
851-
)
852-
},
853-
)
854-
}
855-
856849
/// Unlocks an already de-registered operator given unlock wait period is complete.
857850
pub(crate) fn do_unlock_operator<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
858851
Operators::<T>::try_mutate_exists(operator_id, |maybe_operator| {
@@ -884,7 +877,7 @@ pub(crate) fn do_unlock_operator<T: Config>(operator_id: OperatorId) -> Result<(
884877
let staked_hold_id = T::HoldIdentifier::staking_staked(operator_id);
885878
Deposits::<T>::drain_prefix(operator_id).try_for_each(|(nominator_id, mut deposit)| {
886879
// convert any deposits from the previous epoch to shares
887-
do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit, domain_epoch)?;
880+
do_convert_previous_epoch_deposits::<T>(operator_id, &mut deposit)?;
888881

889882
let current_locked_amount =
890883
T::Currency::balance_on_hold(&staked_hold_id, &nominator_id);
@@ -893,34 +886,37 @@ pub(crate) fn do_unlock_operator<T: Config>(operator_id: OperatorId) -> Result<(
893886
// if the withdrawals has share price noted, then convert them to SSC
894887
// if no share price, then it must be intitated in the epoch when operator was slashed,
895888
// so get the shares as is and include them in the total staked shares.
896-
let (amount_ready_withdraw, shares_withdrew_in_current_epoch) =
897-
calculate_withdraw_share_ssc::<T>(operator_id, nominator_id.clone());
889+
let (amount_ready_to_withdraw, shares_withdrew_in_current_epoch) =
890+
Withdrawals::<T>::take(operator_id, nominator_id.clone())
891+
.map(|mut withdrawal| {
892+
do_convert_previous_epoch_withdrawal::<T>(operator_id, &mut withdrawal)?;
893+
Ok((
894+
withdrawal.total_withdrawal_amount,
895+
withdrawal
896+
.withdrawal_in_shares
897+
.map(|(_, _, shares)| shares)
898+
.unwrap_or_default(),
899+
))
900+
})
901+
.unwrap_or(Ok((Zero::zero(), Zero::zero())))?;
898902

899903
// include all the known shares and shares that were withdrawn in the current epoch
900-
let nominator_shares = if shares_withdrew_in_current_epoch.is_zero() {
901-
deposit.known.shares
902-
} else {
903-
deposit
904-
.known
905-
.shares
906-
.checked_add(&shares_withdrew_in_current_epoch)
907-
.ok_or(Error::ShareOverflow)?
908-
};
904+
let nominator_shares = deposit
905+
.known
906+
.shares
907+
.checked_add(&shares_withdrew_in_current_epoch)
908+
.ok_or(Error::ShareOverflow)?;
909909

910910
// current staked amount
911-
let nominator_staked_amount = if share_price.is_one() {
912-
nominator_shares.into()
913-
} else {
914-
share_price.shares_to_stake::<T>(nominator_shares)
915-
};
911+
let nominator_staked_amount = share_price.shares_to_stake::<T>(nominator_shares);
916912

917913
let amount_deposited_in_previous_epoch = deposit
918914
.pending
919915
.map(|pending_deposit| pending_deposit.amount)
920916
.unwrap_or_default();
921917

922918
let total_amount_to_unlock = nominator_staked_amount
923-
.checked_add(&amount_ready_withdraw)
919+
.checked_add(&amount_ready_to_withdraw)
924920
.and_then(|amount| amount.checked_add(&amount_deposited_in_previous_epoch))
925921
.ok_or(Error::BalanceOverflow)?;
926922

0 commit comments

Comments
 (0)