Skip to content

Commit 40d527a

Browse files
committed
Full fix
1 parent 37c5e98 commit 40d527a

File tree

3 files changed

+86
-89
lines changed

3 files changed

+86
-89
lines changed

pallets/dapp-staking-v3/src/test/testing_utils.rs

+29-38
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ pub(crate) fn assert_unstake(
627627
.expect("Entry must exist since 'unstake' is being called.");
628628
let pre_era_info = pre_snapshot.current_era_info;
629629

630+
let unstake_era = pre_snapshot.active_protocol_state.era;
630631
let unstake_period = pre_snapshot.active_protocol_state.period_number();
631632
let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod();
632633

@@ -679,9 +680,8 @@ pub(crate) fn assert_unstake(
679680
// 2. verify staker info
680681
// =====================
681682
// =====================
682-
let staker_delta_stake =
683-
pre_staker_info.staked.total() - pre_staker_info.previous_staked.total();
684683

684+
// Verify that expected unstake amounts are applied.
685685
if is_full_unstake {
686686
assert!(
687687
!StakerInfo::<Test>::contains_key(&account, smart_contract),
@@ -722,19 +722,19 @@ pub(crate) fn assert_unstake(
722722
is_loyal,
723723
"If 'Voting' stake amount is reduced in B&E period, loyalty flag must be set to false."
724724
);
725+
}
725726

726-
if staker_delta_stake.is_zero() {
727-
assert!(post_staker_info.previous_staked.is_empty(),);
728-
} else {
729-
assert_eq!(
730-
post_staker_info.previous_staked.total(),
731-
pre_staker_info.staked.total()
732-
);
733-
assert_eq!(
734-
post_staker_info.previous_staked.era,
735-
pre_staker_info.staked.era - 1
736-
);
737-
}
727+
let unstaked_amount_era_pairs =
728+
pre_staker_info
729+
.clone()
730+
.unstake(amount, unstake_period, unstake_subperiod);
731+
assert!(unstaked_amount_era_pairs.len() <= 2 && unstaked_amount_era_pairs.len() > 0);
732+
{
733+
let (last_unstake_era, last_unstake_amount) = unstaked_amount_era_pairs
734+
.last()
735+
.expect("Has to exist due to success of previous check");
736+
assert_eq!(*last_unstake_era, unstake_era.max(pre_staker_info.era()));
737+
assert_eq!(*last_unstake_amount, amount);
738738
}
739739

740740
// 3. verify contract stake
@@ -753,30 +753,21 @@ pub(crate) fn assert_unstake(
753753
"Staked amount must decreased by the 'amount'"
754754
);
755755

756-
// Ensure that former era stake is updated correctly.
757-
// If this is true, it means that the staker must have had a stake in some previous era.
758-
// It's not possible to unstake more than user has staked, so this has to always hold true.
759-
if staker_delta_stake < amount {
760-
let expected_adjustment = amount - staker_delta_stake;
761-
762-
let latest_contract_stake_era = post_contract_stake
763-
.latest_stake_era()
764-
.expect("Has to be defined");
765-
766-
let previous_former_era_stake_amount = pre_contract_stake
767-
.get(latest_contract_stake_era - 1, unstake_period)
768-
.expect("Has to be defined")
769-
.total();
770-
let current_former_era_stake_amount = post_contract_stake
771-
.get(latest_contract_stake_era - 1, unstake_period)
772-
.expect("Has to be defined")
773-
.total();
774-
775-
assert_eq!(
776-
current_former_era_stake_amount,
777-
previous_former_era_stake_amount - expected_adjustment,
778-
"Former era stake can only be adjusted by the amount that was actually unstaked from it."
779-
);
756+
// Ensure staked amounts are updated as expected, unless it's full unstake.
757+
if !is_full_unstake {
758+
for (unstake_era_iter, unstake_amount_iter) in unstaked_amount_era_pairs {
759+
assert_eq!(
760+
post_contract_stake
761+
.get(unstake_era_iter, unstake_period)
762+
.expect("Must exist.")
763+
.total(),
764+
pre_contract_stake
765+
.get(unstake_era_iter, unstake_period)
766+
.expect("Must exist")
767+
.total()
768+
- unstake_amount_iter
769+
);
770+
}
780771
}
781772

782773
// 4. verify era info

pallets/dapp-staking-v3/src/test/tests_types.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,7 @@ fn singular_staking_info_basics_are_ok() {
20492049
era_1 + 1,
20502050
"Stake era should remain valid."
20512051
);
2052+
assert!(staking_info.previous_staked.is_empty());
20522053

20532054
// Add some staked amount during `BuildAndEarn` period
20542055
let era_2 = 9;
@@ -2067,6 +2068,12 @@ fn singular_staking_info_basics_are_ok() {
20672068
bep_stake_amount_1
20682069
);
20692070
assert_eq!(staking_info.era(), era_2 + 1);
2071+
2072+
assert_eq!(staking_info.previous_staked.total(), vote_stake_amount_1);
2073+
assert_eq!(
2074+
staking_info.previous_staked.era, era_2,
2075+
"Must be equal to the previous staked era."
2076+
);
20702077
}
20712078

20722079
#[test]
@@ -2102,7 +2109,7 @@ fn singular_staking_info_unstake_during_voting_is_ok() {
21022109
let remaining_stake = staking_info.total_staked_amount();
21032110
assert_eq!(
21042111
staking_info.unstake(remaining_stake + 1, era_2, Subperiod::Voting),
2105-
vec![(era_2 - 1, remaining_stake), (era_2, remaining_stake)]
2112+
vec![(era_2, remaining_stake)]
21062113
);
21072114
assert!(staking_info.total_staked_amount().is_zero());
21082115
assert!(
@@ -2174,8 +2181,7 @@ fn singular_staking_info_unstake_during_bep_is_ok() {
21742181

21752182
assert_eq!(
21762183
staking_info.unstake(unstake_2, era_2, Subperiod::BuildAndEarn),
2177-
// Both amounts are the same since previous staked era is less than last staked era - 1
2178-
vec![(era_2 - 1, unstake_2), (era_2, unstake_2)]
2184+
vec![(era_2, unstake_2)]
21792185
);
21802186
assert_eq!(
21812187
staking_info.total_staked_amount(),

pallets/dapp-staking-v3/src/types.rs

+48-48
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,9 @@ impl SingularStakingInfo {
10021002
// Keep the previous stake amount for future reference
10031003
self.previous_staked = self.staked;
10041004
self.previous_staked.era = current_era;
1005+
if self.previous_staked.total().is_zero() {
1006+
self.previous_staked = Default::default();
1007+
}
10051008

10061009
// Stake is only valid from the next era so we keep it consistent here
10071010
self.staked.add(amount, subperiod);
@@ -1021,22 +1024,6 @@ impl SingularStakingInfo {
10211024
/// This means that the returned value will at most cover two eras - the last staked era, and the one before it.
10221025
///
10231026
/// Last staked era can be the current era, or the era after.
1024-
/// If after the unstake operation, delta between previous & current era is larger than 1, previous staked entry is ignored.
1025-
///
1026-
/// #### Example (simplified)
1027-
///
1028-
/// 1. previous_staked: (era: 4, amount: 50), staked: (era: 5, amount: 100)
1029-
/// 2. unstake 30 in era 6 is called
1030-
/// 3. Return value is: (era: 5, amount: 30), (era: 6, amount: 30)
1031-
/// 4. previous_staked: (era: 5, amount: 70), (era: 6, amount: 70)
1032-
///
1033-
/// In case same example is repeated, but unstake is done in era 5:
1034-
///
1035-
/// 1. previous_staked: (era: 4, amount: 50), staked: (era: 5, amount: 100)
1036-
/// 2. unstake 30 in era 5 is called
1037-
/// 3. Return value is: (era: 5, amount: 30)
1038-
/// 4. previous_staked: (era: 4, amount: 50), (era: 5, amount: 70)
1039-
///
10401027
pub fn unstake(
10411028
&mut self,
10421029
amount: Balance,
@@ -1046,51 +1033,64 @@ impl SingularStakingInfo {
10461033
let mut result = Vec::new();
10471034
let staked_snapshot = self.staked;
10481035

1049-
// 1. Modify current staked amount
1036+
// 1. Modify current staked amount, and update the result.
10501037
self.staked.subtract(amount);
10511038
let unstaked_amount = staked_snapshot.total().saturating_sub(self.staked.total());
10521039
self.staked.era = self.staked.era.max(current_era);
10531040
result.push((self.staked.era, unstaked_amount));
10541041

1055-
// 2. Calculate previous stake delta.
1056-
// In case new stake era is exactly one era after the previous stake era, we can calculate this as a delta.
1057-
// Otherwise, if it's further far away in the past, the previous era stake is equal to the snapshot.
1058-
let stake_delta = if self.staked.era == self.previous_staked.era.saturating_add(1) {
1059-
staked_snapshot
1060-
.total()
1061-
.saturating_sub(self.previous_staked.total())
1062-
} else {
1063-
Balance::zero()
1064-
};
1065-
1066-
// 2. Update loyal staker flag accordingly
1042+
// 2. Update loyal staker flag accordingly.
10671043
self.loyal_staker = self.loyal_staker
10681044
&& match subperiod {
10691045
Subperiod::Voting => !self.staked.voting.is_zero(),
10701046
Subperiod::BuildAndEarn => self.staked.voting == staked_snapshot.voting,
10711047
};
10721048

1073-
// 3. Move over the snapshot to the previous snapshot field and make sure
1074-
// to update era in case something was staked before.
1075-
if stake_delta.is_zero() {
1076-
self.previous_staked = Default::default();
1049+
// 3. Determine what was the previous staked amount.
1050+
// This is done by simply comparing where does the _previous era_ fit in the current context.
1051+
let previous_era = self.staked.era.saturating_sub(1);
1052+
1053+
self.previous_staked = if staked_snapshot.era <= previous_era {
1054+
let mut previous_staked = staked_snapshot;
1055+
previous_staked.era = previous_era;
1056+
previous_staked
1057+
} else if !self.previous_staked.is_empty() && self.previous_staked.era <= previous_era {
1058+
let mut previous_staked = self.previous_staked;
1059+
previous_staked.era = previous_era;
1060+
previous_staked
10771061
} else {
1078-
self.previous_staked = staked_snapshot;
1079-
self.previous_staked.era = self.staked.era.saturating_sub(1);
1080-
}
1062+
Default::default()
1063+
};
10811064

1082-
// 4. If something was unstaked from the previous era, add it to the result
1083-
if unstaked_amount > stake_delta {
1084-
// Doesn't need to be at 0-th index, but we still add it for clarity
1085-
result.insert(
1086-
0,
1087-
(
1088-
self.staked.era.saturating_sub(1),
1089-
// The diff between unstake amount & stake delta tells us how much was
1090-
// actually unstaked from the previous era.
1091-
unstaked_amount.saturating_sub(stake_delta),
1092-
),
1093-
)
1065+
// 4. Calculate how much is being unstaked from the previous staked era entry, in case its era equals the current era.
1066+
//
1067+
// Simples way to explain this is via an example.
1068+
// Let's assume a simplification where stake amount entries are in `(era, amount)` format.
1069+
//
1070+
// 1. Values: previous_staked: **(2, 10)**, staked: **(3, 15)**
1071+
// 2. User calls unstake during **era 2**, and unstakes amount **6**.
1072+
// Clearly some amount was staked during era 2, which resulted in era 3 stake being increased by 5.
1073+
// Calling unstake immediately in the same era should not necessarily reduce current era stake amount.
1074+
// This should be allowed to happen only if the unstaked amount is larger than the difference between the staked amount of two eras.
1075+
// 3. Values: previous_staked: **(2, 9)**, staked: **(3, 9)**
1076+
//
1077+
// An alternative scenario, where user calls unstake during **era 2**, and unstakes amount **4**.
1078+
// 3. Values: previous_staked: **(2, 10)**, staked: **(3, 11)**
1079+
//
1080+
// Note that the unstake operation didn't chip away from the current era, only the next one.
1081+
if self.previous_staked.era == current_era {
1082+
let maybe_stake_delta = staked_snapshot
1083+
.total()
1084+
.checked_sub(self.previous_staked.total());
1085+
match maybe_stake_delta {
1086+
Some(stake_delta) if unstaked_amount > stake_delta => {
1087+
let overflow_amount = unstaked_amount - stake_delta;
1088+
self.previous_staked.subtract(overflow_amount);
1089+
1090+
result.insert(0, (self.previous_staked.era, overflow_amount));
1091+
}
1092+
_ => {}
1093+
}
10941094
}
10951095

10961096
result

0 commit comments

Comments
 (0)