Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix staked amount too high after unstake issue #1270

Merged
merged 2 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions pallets/dapp-staking-v3/src/test/testing_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,15 +727,16 @@ pub(crate) fn assert_unstake(
let unstaked_amount_era_pairs =
pre_staker_info
.clone()
.unstake(expected_amount, unstake_period, unstake_subperiod);
.unstake(expected_amount, unstake_era, unstake_subperiod);
assert!(unstaked_amount_era_pairs.len() <= 2 && unstaked_amount_era_pairs.len() > 0);
{
let (last_unstake_era, last_unstake_amount) = unstaked_amount_era_pairs
.last()
.expect("Has to exist due to success of previous check");
assert_eq!(*last_unstake_era, unstake_era.max(pre_staker_info.era()));
assert_eq!(*last_unstake_amount, expected_amount);
}

// If unstake from next era exists, it must exactly match the expected unstake amount.
unstaked_amount_era_pairs
.iter()
.filter(|(era, _)| *era > unstake_era)
.for_each(|(_, amount)| {
assert_eq!(*amount, expected_amount);
});

// 3. verify contract stake
// =========================
Expand All @@ -753,21 +754,31 @@ pub(crate) fn assert_unstake(
"Staked amount must decreased by the 'amount'"
);

// Ensure staked amounts are updated as expected, unless it's full unstake.
if !is_full_unstake {
for (unstake_era_iter, unstake_amount_iter) in unstaked_amount_era_pairs {
assert_eq!(
post_contract_stake
.get(unstake_era_iter, unstake_period)
.expect("Must exist.")
.total(),
pre_contract_stake
.get(unstake_era_iter, unstake_period)
.expect("Must exist")
.total()
- unstake_amount_iter
);
}
// A generic check, comparing what was received in the (era, amount) pairs and the impact it had on the contract stake.
for (unstake_era_iter, unstake_amount_iter) in unstaked_amount_era_pairs {
assert_eq!(
post_contract_stake
.get(unstake_era_iter, unstake_period)
.unwrap_or_default() // it's possible that full unstake cleared the entry
.total(),
pre_contract_stake
.get(unstake_era_iter, unstake_period)
.expect("Must exist")
.total()
- unstake_amount_iter
);
}

// More precise check, independent of the generic check above.
// If next era entry exists, it must be reduced by the unstake amount, nothing less.
if let Some(entry) = pre_contract_stake.get(unstake_era + 1, unstake_period) {
assert_eq!(
post_contract_stake
.get(unstake_era + 1, unstake_period)
.unwrap_or_default()
.total(),
entry.total() - expected_amount
);
}

// 4. verify era info
Expand Down
44 changes: 44 additions & 0 deletions pallets/dapp-staking-v3/src/test/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3242,3 +3242,47 @@ fn claim_dapp_reward_with_rank() {
}));
})
}

#[test]
fn unstake_correctly_reduces_future_contract_stake() {
ExtBuilder::build().execute_with(|| {
// 0. Register smart contract, lock&stake some amount with staker 1 during the voting subperiod
let smart_contract = MockSmartContract::wasm(1 as AccountId);
assert_register(1, &smart_contract);

let (staker_1, amount_1) = (1, 29);
assert_lock(staker_1, amount_1);
assert_stake(staker_1, &smart_contract, amount_1);

// 1. Advance to the build&earn subperiod, stake some amount with staker 2
advance_to_next_era();
let (staker_2, amount_2) = (2, 11);
assert_lock(staker_2, amount_2);
assert_stake(staker_2, &smart_contract, amount_2);

// 2. Advance a few eras, creating a gap but remaining within the same period.
// Claim all rewards for staker 1.
// Lock & stake some amount with staker 3.
advance_to_era(ActiveProtocolState::<Test>::get().era + 3);
assert_eq!(
ActiveProtocolState::<Test>::get().period_number(),
1,
"Sanity check."
);
for _ in 0..required_number_of_reward_claims(staker_1) {
assert_claim_staker_rewards(staker_1);
}

// This ensures contract stake entry is aligned to the current era, and future entry refers to the era after this one.
//
// This is important to reproduce an issue where the (era, amount) pairs returned by the `unstake` function don't correctly
// cover the next era.
let (staker_3, amount_3) = (3, 13);
assert_lock(staker_3, amount_3);
assert_stake(staker_3, &smart_contract, amount_3);

// 3. Unstake from staker 1, and ensure the future stake is reduced.
// Unstake amount should be slightly higher than the 2nd stake amount to ensure whole b&e stake amount is removed.
assert_unstake(staker_1, &smart_contract, amount_2 + 3);
})
}
74 changes: 69 additions & 5 deletions pallets/dapp-staking-v3/src/test/tests_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2115,7 +2115,8 @@ fn singular_staking_info_unstake_during_voting_is_ok() {
let remaining_stake = staking_info.total_staked_amount();
assert_eq!(
staking_info.unstake(remaining_stake + 1, era_2, Subperiod::Voting),
vec![(era_2, remaining_stake)]
vec![(era_2, remaining_stake), (era_2 + 1, remaining_stake)],
"Also chipping away from the next era since the unstake is relevant to the ongoing era."
);
assert!(staking_info.total_staked_amount().is_zero());
assert!(
Expand Down Expand Up @@ -2217,7 +2218,8 @@ fn singular_staking_info_unstake_during_bep_is_ok() {

assert_eq!(
staking_info.unstake(unstake_2, era_2, Subperiod::BuildAndEarn),
vec![(era_2, unstake_2)]
vec![(era_2, unstake_2), (era_2 + 1, unstake_2)],
"Also chipping away from the next era since the unstake is relevant to the ongoing era."
);
assert_eq!(
staking_info.total_staked_amount(),
Expand All @@ -2240,6 +2242,59 @@ fn singular_staking_info_unstake_during_bep_is_ok() {
assert_eq!(staking_info.previous_staked.era, era_2 - 1);
}

#[test]
fn singular_staking_info_unstake_era_amount_pairs_are_ok() {
let period_number = 1;
let subperiod = Subperiod::BuildAndEarn;

// 1. Unstake only reduces the amount from a the future era
{
let era = 3;
let stake_amount = 13;
let unstake_amount = 3;
let mut staking_info = SingularStakingInfo::new(period_number, subperiod);
staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn);

assert_eq!(
staking_info.unstake(unstake_amount, era, Subperiod::BuildAndEarn),
vec![(era + 1, unstake_amount)]
);
}

// 2. Unstake reduces the amount from the current & next era.
{
let era = 3;
let stake_amount = 17;
let unstake_amount = 5;
let mut staking_info = SingularStakingInfo::new(period_number, subperiod);
staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn);

assert_eq!(
staking_info
.clone()
.unstake(unstake_amount, era + 1, Subperiod::BuildAndEarn),
vec![(era + 1, unstake_amount), (era + 2, unstake_amount)]
);
}

// 3. Unstake reduces the amount from the current & next era.
// Unlike the previous example, entries are not aligned with the current era
{
let era = 3;
let stake_amount = 17;
let unstake_amount = 5;
let mut staking_info = SingularStakingInfo::new(period_number, subperiod);
staking_info.stake(stake_amount, era, Subperiod::BuildAndEarn);

assert_eq!(
staking_info
.clone()
.unstake(unstake_amount, era + 2, Subperiod::BuildAndEarn),
vec![(era + 2, unstake_amount), (era + 3, unstake_amount)]
);
}
}

#[test]
fn contract_stake_amount_basic_get_checks_work() {
// Sanity checks for empty struct
Expand Down Expand Up @@ -2413,9 +2468,18 @@ fn contract_stake_amount_stake_is_ok() {
let stake_era_2 = era_2 + 1;
let amount_2 = 37;
contract_stake.stake(amount_2, period_info_1, era_2);
let entry_2_1 = contract_stake.get(stake_era_1, period_1).unwrap();
let entry_2_1 = contract_stake
.get(era_2, period_1)
.expect("Since stake will change next era, entries should be aligned.");
let entry_2_2 = contract_stake.get(stake_era_2, period_1).unwrap();
assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged.");
assert_eq!(
entry_2_1.for_type(Subperiod::Voting),
entry_1_2.for_type(Subperiod::Voting)
);
assert_eq!(
entry_2_1.for_type(Subperiod::BuildAndEarn),
entry_1_2.for_type(Subperiod::BuildAndEarn)
);
assert_eq!(entry_2_2.era, stake_era_2);
assert_eq!(entry_2_2.period, period_1);
assert_eq!(
Expand All @@ -2442,7 +2506,7 @@ fn contract_stake_amount_stake_is_ok() {

contract_stake.stake(amount_3, period_info_2, era_3);
assert!(
contract_stake.get(stake_era_1, period_1).is_none(),
contract_stake.get(era_2, period_1).is_none(),
"Old period must be removed."
);
assert!(
Expand Down
8 changes: 6 additions & 2 deletions pallets/dapp-staking-v3/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ impl SingularStakingInfo {
let mut result = Vec::new();
let staked_snapshot = self.staked;

// 1. Modify current staked amount, and update the result.
// 1. Modify 'current' staked amount, and update the result.
self.staked.subtract(amount);
let unstaked_amount = staked_snapshot.total().saturating_sub(self.staked.total());
self.staked.era = self.staked.era.max(current_era);
Expand Down Expand Up @@ -1105,6 +1105,9 @@ impl SingularStakingInfo {
}
_ => {}
}
} else if self.staked.era == current_era {
// In case the `staked` era was already the current era, it also means we're chipping away from the future era.
result.push((self.staked.era.saturating_add(1), unstaked_amount));
}

// 5. Convenience cleanup
Expand Down Expand Up @@ -1251,6 +1254,8 @@ impl ContractStakeAmount {
// Future entry has an older era, but periods match so overwrite the 'current' entry with it
Some(stake_amount) if stake_amount.period == period_info.number => {
self.staked = *stake_amount;
// Align the eras to keep it simple
self.staked.era = current_era;
}
// Otherwise do nothing
_ => (),
Expand Down Expand Up @@ -1303,7 +1308,6 @@ impl ContractStakeAmount {
}

// 2. Value updates - only after alignment

for (era, amount) in era_and_amount_pairs {
if self.staked.era == era {
self.staked.subtract(amount);
Expand Down
Loading