Skip to content

Commit fe0b140

Browse files
authored
fix(staker): various increase/decrease/renew test cases (#1171)
1 parent 5151cfc commit fe0b140

7 files changed

Lines changed: 193 additions & 42 deletions

File tree

builtin/staker/delegations.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ func (d *delegations) Add(
6060
if aggregated.IsEmpty() {
6161
aggregated = newAggregation()
6262
}
63-
nextPeriodStake := validation.NextPeriodStakes(aggregated)
64-
nextPeriodStake = nextPeriodStake.Add(nextPeriodStake, stake)
65-
if nextPeriodStake.Cmp(MaxStake) > 0 {
63+
nextPeriodTVL := big.NewInt(0).Add(validation.NextPeriodTVL(), aggregated.NextPeriodTVL())
64+
nextPeriodTVL = nextPeriodTVL.Add(nextPeriodTVL, stake)
65+
if nextPeriodTVL.Cmp(MaxStake) > 0 {
6666
return thor.Bytes32{}, errors.New("validation's next period stake exceeds max stake")
6767
}
6868

@@ -84,30 +84,23 @@ func (d *delegations) Add(
8484
ValidationID: validationID,
8585
FirstIteration: validation.CurrentIteration() + 1,
8686
}
87-
8887
weight := delegation.Weight()
89-
90-
if !autoRenew {
88+
if delegation.AutoRenew {
89+
aggregated.PendingRecurringVET = big.NewInt(0).Add(aggregated.PendingRecurringVET, delegation.Stake)
90+
aggregated.PendingRecurringWeight = big.NewInt(0).Add(aggregated.PendingRecurringWeight, weight)
91+
} else {
92+
aggregated.PendingOneTimeVET = big.NewInt(0).Add(aggregated.PendingOneTimeVET, delegation.Stake)
93+
aggregated.PendingOneTimeWeight = big.NewInt(0).Add(aggregated.PendingOneTimeWeight, weight)
9194
last := validation.CurrentIteration() + 1
9295
delegation.LastIteration = &last
9396
}
9497

9598
if err := d.queuedVET.Add(stake); err != nil {
9699
return thor.Bytes32{}, err
97100
}
98-
99101
if err := d.queuedWeight.Add(weight); err != nil {
100102
return thor.Bytes32{}, err
101103
}
102-
103-
if delegation.AutoRenew {
104-
aggregated.PendingRecurringVET = big.NewInt(0).Add(aggregated.PendingRecurringVET, delegation.Stake)
105-
aggregated.PendingRecurringWeight = big.NewInt(0).Add(aggregated.PendingRecurringWeight, weight)
106-
} else {
107-
aggregated.PendingOneTimeVET = big.NewInt(0).Add(aggregated.PendingOneTimeVET, delegation.Stake)
108-
aggregated.PendingOneTimeWeight = big.NewInt(0).Add(aggregated.PendingOneTimeWeight, weight)
109-
}
110-
111104
if err := d.storage.SetAggregation(validationID, aggregated, false); err != nil {
112105
return thor.Bytes32{}, err
113106
}
@@ -166,9 +159,18 @@ func (d *delegations) EnableAutoRenew(delegationID thor.Bytes32) error {
166159
if err != nil {
167160
return err
168161
}
169-
162+
if delegation.AutoRenew {
163+
return errors.New("delegation is already autoRenew")
164+
}
170165
weight := delegation.Weight()
171166

167+
// validate that the enablement does not exceed the max stake considering next staking period changes
168+
nextPeriodTVL := big.NewInt(0).Add(validation.NextPeriodTVL(), aggregation.NextPeriodTVL())
169+
nextPeriodTVL.Add(nextPeriodTVL, delegation.Stake)
170+
if nextPeriodTVL.Cmp(MaxStake) > 0 {
171+
return errors.New("validation's next period stake exceeds max stake")
172+
}
173+
172174
if delegation.IsLocked(validation) {
173175
// move the delegation's portion of non-recurring to locked.
174176
// this means the funds will not be available until the validator is inactive, or the delegation signals an exit

builtin/staker/delegations_test.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,26 @@ func Test_AddDelegator_StakeRange(t *testing.T) {
168168
assert.ErrorContains(t, err, "validation's next period stake exceeds max stake")
169169

170170
// should be able stake 1 VET
171-
_, err = staker.AddDelegation(validator.ID, big.NewInt(1), true, 255)
171+
id1, err := staker.AddDelegation(validator.ID, big.NewInt(1), true, 255)
172172
assert.NoError(t, err)
173+
delegation, _, err := staker.GetDelegation(id1)
174+
assert.NoError(t, err)
175+
assert.Equal(t, big.NewInt(1), delegation.Stake)
176+
aggregation, err := staker.storage.GetAggregation(validator.ID)
177+
assert.NoError(t, err)
178+
assert.Equal(t, big.NewInt(1), aggregation.PendingRecurringVET)
173179

174180
// should be able stake for all remaining space
175181
validator = validators[1]
176182
validation, err := staker.Get(validator.ID)
177183
assert.NoError(t, err)
178-
remaining := big.NewInt(0).Sub(MaxStake, validation.NextPeriodStakes(newAggregation()))
184+
remaining := big.NewInt(0).Sub(MaxStake, validation.NextPeriodTVL())
179185
_, err = staker.AddDelegation(validator.ID, remaining, true, 255)
180186
assert.NoError(t, err)
187+
188+
// should not be able to stake more than max stake
189+
_, err = staker.AddDelegation(validator.ID, big.NewInt(1000000000000000000), true, 255)
190+
assert.ErrorContains(t, err, "validation's next period stake exceeds max stake")
181191
}
182192

183193
func Test_AddDelegator_ValidatorNotFound(t *testing.T) {
@@ -309,6 +319,9 @@ func Test_Delegator_DisableAutoRenew_InAStakingPeriod(t *testing.T) {
309319
// And a delegation is added with auto renew enabled
310320
validator := validators[0]
311321
stake := RandomStake()
322+
validation, err := staker.Get(validator.ID)
323+
assert.NoError(t, err)
324+
validationStake := big.NewInt(0).Set(validation.LockedVET)
312325

313326
id, err := staker.AddDelegation(validator.ID, stake, true, 255)
314327
assert.NoError(t, err)
@@ -350,6 +363,9 @@ func Test_Delegator_DisableAutoRenew_InAStakingPeriod(t *testing.T) {
350363
aggregation, err = staker.storage.GetAggregation(validator.ID)
351364
assert.NoError(t, err)
352365
assert.Equal(t, stake, aggregation.WithdrawableVET)
366+
validation, err = staker.Get(validator.ID)
367+
assert.NoError(t, err)
368+
assert.Equal(t, validationStake, validation.LockedVET)
353369
}
354370

355371
func Test_Delegator_EnableAutoRenew_PendingLocked(t *testing.T) {
@@ -626,3 +642,42 @@ func Test_Delegator_Queued_Weight_MultipleDelegations_Withdraw(t *testing.T) {
626642
assert.Equal(t, initialQueuedVET, afterWithdraw2QueuedVET)
627643
assert.Equal(t, initialQueuedWeight, afterWithdraw2QueuedWeight)
628644
}
645+
646+
func Test_Delegations_EnableAutoRenew_MatchStakeReached(t *testing.T) {
647+
staker, validators := newDelegationStaker(t)
648+
649+
validator := validators[0]
650+
maxStake := big.NewInt(0).Sub(MaxStake, validator.LockedVET)
651+
652+
// Add a delegation with auto renew enabled
653+
delegationID, err := staker.AddDelegation(validator.ID, maxStake, false, 255)
654+
assert.NoError(t, err)
655+
656+
// Should be pending
657+
delegation1, _, err := staker.GetDelegation(delegationID)
658+
assert.NoError(t, err)
659+
validation, err := staker.Get(validator.ID)
660+
assert.NoError(t, err)
661+
assert.False(t, delegation1.IsLocked(validation))
662+
663+
// Delegation should become active
664+
_, _, err = staker.Housekeep(validator.Period)
665+
assert.NoError(t, err)
666+
delegation1, _, err = staker.GetDelegation(delegationID)
667+
assert.NoError(t, err)
668+
validation, err = staker.Get(validator.ID)
669+
assert.NoError(t, err)
670+
assert.True(t, delegation1.IsLocked(validation))
671+
672+
// Enable auto renew for delegation. Should be possible since the max stake won't be reached.
673+
assert.NoError(t, staker.UpdateDelegationAutoRenew(delegationID, true))
674+
// Immediately turn it off again for the test
675+
assert.NoError(t, staker.UpdateDelegationAutoRenew(delegationID, false))
676+
677+
// Add a new delegation. It should be possible to add the delegation since the previous one is due to withdraw.
678+
_, err = staker.AddDelegation(validator.ID, maxStake, true, 255)
679+
assert.NoError(t, err)
680+
681+
// Enable auto renew for the first delegation - should fail since the presence of other delegator's exceeds max stake
682+
assert.ErrorContains(t, staker.UpdateDelegationAutoRenew(delegationID, true), "validation's next period stake exceeds max stake")
683+
}

builtin/staker/housekeeping.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (s *Staker) performRenewalUpdates(id thor.Bytes32, validator *Validation) e
112112
queuedWeight := big.NewInt(0).Add(validatorRenewal.QueuedDecreaseWeight, delegationsRenewal.QueuedDecreaseWeight)
113113

114114
// set the new totals
115-
validator.LockedVET = big.NewInt(0).Add(validator.LockedVET, changeTVL)
115+
validator.LockedVET = big.NewInt(0).Add(validator.LockedVET, validatorRenewal.ChangeTVL)
116116
validator.Weight = big.NewInt(0).Add(validator.Weight, changeWeight)
117117
if err := s.lockedVET.Add(changeTVL); err != nil {
118118
return err

builtin/staker/staker.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,9 @@ func (s *Staker) GetValidatorsTotals(validationID thor.Bytes32) (*ValidationTota
352352
if err != nil {
353353
return nil, err
354354
}
355+
delegationLockedStake := big.NewInt(0).Add(aggregation.CurrentRecurringVET, aggregation.CurrentOneTimeVET)
355356
return &ValidationTotals{
356-
TotalLockedStake: validator.LockedVET,
357+
TotalLockedStake: big.NewInt(0).Add(validator.LockedVET, delegationLockedStake),
357358
TotalLockedWeight: validator.Weight,
358359
DelegationsLockedStake: big.NewInt(0).Add(aggregation.CurrentRecurringVET, aggregation.CurrentOneTimeVET),
359360
DelegationsLockedWeight: big.NewInt(0).Add(aggregation.CurrentRecurringWeight, aggregation.CurrentOneTimeWeight),

builtin/staker/types.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type Validation struct {
3131
StartBlock uint32 // the block number when the validation started the first staking period
3232
ExitBlock *uint32 `rlp:"nil"` // the block number when the validation moved to cooldown
3333

34-
LockedVET *big.Int // the amount of VET locked for the current staking period
34+
LockedVET *big.Int // the amount of VET locked for the current staking period, for the validator only
3535
NextPeriodDecrease *big.Int // the amount of VET that will be unlocked in the next staking period. DOES NOT contribute to the TVL
3636
PendingLocked *big.Int // the amount of VET that will be locked in the next staking period
3737
CooldownVET *big.Int // the amount of VET that is locked into the validation's cooldown
@@ -61,10 +61,11 @@ func (v *Validation) IsPeriodEnd(current uint32) bool {
6161
return diff%v.Period == 0
6262
}
6363

64-
// NextPeriodStakes returns the validation stake and all the delegator stakes for the next staking period.
65-
func (v *Validation) NextPeriodStakes(delegation *Aggregation) *big.Int {
64+
// NextPeriodTVL returns the amount of VET that will be locked in the next staking period for the validator only.
65+
func (v *Validation) NextPeriodTVL() *big.Int {
6666
validationTotal := big.NewInt(0).Add(v.LockedVET, v.PendingLocked)
67-
return validationTotal.Add(validationTotal, delegation.NextPeriodLocked())
67+
validationTotal = big.NewInt(0).Sub(validationTotal, v.NextPeriodDecrease)
68+
return validationTotal
6869
}
6970

7071
func (v *Validation) CurrentIteration() uint32 {
@@ -187,16 +188,15 @@ func (a *Aggregation) IsEmpty() bool {
187188
return a.CurrentRecurringVET == nil && a.CurrentOneTimeVET == nil && a.PendingRecurringVET == nil && a.PendingOneTimeVET == nil && a.WithdrawableVET == nil
188189
}
189190

190-
// PeriodLocked returns the VET locked for a given validation's delegations for the current staking period.
191-
func (a *Aggregation) PeriodLocked() *big.Int {
192-
return big.NewInt(0).Add(a.CurrentRecurringVET, a.CurrentOneTimeVET)
193-
}
194-
195-
// NextPeriodLocked returns the PeriodLocked for the next staking period
196-
func (a *Aggregation) NextPeriodLocked() *big.Int {
197-
total := big.NewInt(0).Add(a.CurrentRecurringVET, a.PendingRecurringVET)
198-
total = total.Add(total, a.PendingOneTimeVET)
199-
return total
191+
// NextPeriodTVL is the total value locked (TVL) for the next period.
192+
// It is the sum of the currently recurring VET, plus any pending recurring and one-time VET.
193+
// Does not include CurrentOneTimeVET since that stake is due to withdraw.
194+
func (a *Aggregation) NextPeriodTVL() *big.Int {
195+
nextTVL := big.NewInt(0)
196+
nextTVL.Add(nextTVL, a.CurrentRecurringVET)
197+
nextTVL.Add(nextTVL, a.PendingRecurringVET)
198+
nextTVL.Add(nextTVL, a.PendingOneTimeVET)
199+
return nextTVL
200200
}
201201

202202
// Renew moves the stakes and weights around as follows:

builtin/staker/validations.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,10 @@ func (v *validations) IncreaseStake(id thor.Bytes32, endorsor thor.Address, amou
319319
if err != nil {
320320
return err
321321
}
322-
nextPeriodTVL := entry.NextPeriodStakes(aggregation)
322+
validatorTVL := entry.NextPeriodTVL()
323+
// we do not consider aggregation.CurrentRecurringVET since the delegator could enable auto-renew
324+
delegationTVL := big.NewInt(0).Add(aggregation.CurrentRecurringVET, aggregation.PendingRecurringVET)
325+
nextPeriodTVL := big.NewInt(0).Add(validatorTVL, delegationTVL)
323326
newTVL := big.NewInt(0).Add(nextPeriodTVL, amount)
324327

325328
if newTVL.Cmp(MaxStake) > 0 {
@@ -354,21 +357,30 @@ func (v *validations) DecreaseStake(id thor.Bytes32, endorsor thor.Address, amou
354357
if entry.Status == StatusActive && !entry.AutoRenew {
355358
return errors.New("validator is not set to renew in the next period, all funds will be withdrawable")
356359
}
357-
newStake := big.NewInt(0).Add(entry.LockedVET, entry.PendingLocked)
358-
newStake = newStake.Sub(newStake, amount)
359-
if newStake.Cmp(MinStake) < 0 {
360-
return errors.New("stake is too low for validator")
361-
}
360+
362361
aggregation, err := v.storage.GetAggregation(id)
363362
if err != nil {
364363
return err
365364
}
366365

367366
if entry.Status == StatusActive {
367+
// We don't consider any increases, i.e., entry.PendingLocked. We only consider locked and current decreases.
368+
// The reason is that validator can instantly withdraw PendingLocked at any time.
369+
// We need to make sure the locked VET minus the sum of the current decreases is still above the minimum stake.
370+
nextPeriodTVL := big.NewInt(0).Sub(entry.LockedVET, entry.NextPeriodDecrease)
371+
nextPeriodTVL = nextPeriodTVL.Sub(nextPeriodTVL, amount)
372+
if nextPeriodTVL.Cmp(MinStake) < 0 {
373+
return errors.New("next period stake is too low for validator")
374+
}
368375
entry.NextPeriodDecrease = big.NewInt(0).Add(entry.NextPeriodDecrease, amount)
369376
}
370377

371378
if entry.Status == StatusQueued {
379+
// All the validator's stake exists within PendingLocked, so we need to make sure it maintains a minimum of MinStake.
380+
nextPeriodTVL := big.NewInt(0).Sub(entry.PendingLocked, amount)
381+
if nextPeriodTVL.Cmp(MinStake) < 0 {
382+
return errors.New("next period stake is too low for validator")
383+
}
372384
entry.PendingLocked = big.NewInt(0).Sub(entry.PendingLocked, amount)
373385
entry.WithdrawableVET = big.NewInt(0).Add(entry.WithdrawableVET, amount)
374386
if err := v.queuedVET.Sub(amount); err != nil {
Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2269,13 +2269,19 @@ func Test_GetValidatorTotals(t *testing.T) {
22692269
_, _, err = staker.Housekeep(validator.Period)
22702270
assert.NoError(t, err)
22712271

2272+
aggregation, err = staker.storage.GetAggregation(validator.ID)
2273+
assert.NoError(t, err)
2274+
22722275
totals, err := staker.GetValidatorsTotals(validator.ID)
22732276
assert.NoError(t, err)
22742277

22752278
fetchedValidator, err := staker.Get(validator.ID)
22762279
assert.NoError(t, err)
22772280

2278-
assert.Equal(t, fetchedValidator.LockedVET, totals.TotalLockedStake)
2281+
expectedStake := big.NewInt(0).Add(aggregation.CurrentRecurringVET, aggregation.CurrentOneTimeVET)
2282+
expectedStake.Add(expectedStake, validator.LockedVET)
2283+
2284+
assert.Equal(t, expectedStake, totals.TotalLockedStake)
22792285
assert.Equal(t, fetchedValidator.Weight, totals.TotalLockedWeight)
22802286
assert.Equal(t, delegation.Stake, totals.DelegationsLockedStake)
22812287
assert.Equal(t, delegation.Weight(), totals.DelegationsLockedWeight)
@@ -2310,3 +2316,78 @@ func Test_Validator_Decrease_Exit_Withdraw(t *testing.T) {
23102316
assert.Equal(t, StatusExit, validator.Status)
23112317
assert.Equal(t, originalStake, validator.CooldownVET)
23122318
}
2319+
2320+
func Test_Validator_Decrease_SeveralTimes(t *testing.T) {
2321+
staker, _ := newStaker(t, 0, 1, false)
2322+
2323+
acc := datagen.RandAddress()
2324+
2325+
originalStake := big.NewInt(0).Mul(big.NewInt(3), MinStake)
2326+
id, err := staker.AddValidator(acc, acc, LowStakingPeriod, originalStake, true, 0)
2327+
assert.NoError(t, err)
2328+
_, err = staker.validations.ActivateNext(0, staker.params)
2329+
assert.NoError(t, err)
2330+
2331+
// Decrease stake - ok 75m - 25m = 50m
2332+
err = staker.DecreaseStake(acc, id, MinStake)
2333+
assert.NoError(t, err)
2334+
2335+
// Decrease stake - ok 50m - 25m = 25m
2336+
err = staker.DecreaseStake(acc, id, MinStake)
2337+
assert.NoError(t, err)
2338+
2339+
// Decrease stake - should fail, min stake is 25m
2340+
err = staker.DecreaseStake(acc, id, MinStake)
2341+
assert.ErrorContains(t, err, "next period stake is too low for validator")
2342+
}
2343+
2344+
func Test_Validator_IncreaseDecrease_Combinations(t *testing.T) {
2345+
staker, _ := newStaker(t, 0, 1, false)
2346+
acc := datagen.RandAddress()
2347+
2348+
// Add & activate validator
2349+
id, err := staker.AddValidator(acc, acc, LowStakingPeriod, MinStake, true, 0)
2350+
assert.NoError(t, err)
2351+
2352+
// Increase and decrease - both should be okay since we're only dealing with PendingLocked
2353+
assert.NoError(t, staker.IncreaseStake(acc, id, MinStake)) // 25m + 25m = 50m
2354+
assert.NoError(t, staker.DecreaseStake(acc, id, MinStake)) // 25m - 50m = 25m
2355+
2356+
// Activate the validator.
2357+
_, err = staker.validations.ActivateNext(0, staker.params)
2358+
assert.NoError(t, err)
2359+
2360+
// Withdraw the previous decrease amount
2361+
withdrawal, err := staker.WithdrawStake(acc, id, 0)
2362+
assert.NoError(t, err)
2363+
assert.Equal(t, MinStake, withdrawal, "withdraw should be 0 since we are withdrawing from pending locked")
2364+
2365+
// Assert previous increase/decrease had no effect since they requested the same amount
2366+
validation, err := staker.Get(id)
2367+
assert.NoError(t, err)
2368+
assert.Equal(t, StatusActive, validation.Status)
2369+
assert.Equal(t, MinStake, validation.LockedVET)
2370+
2371+
// Increase stake (ok): 25m + 25m = 50m
2372+
assert.NoError(t, staker.IncreaseStake(acc, id, MinStake))
2373+
// Decrease stake (NOT ok): 25m - 25m = 0. The Previous increase is not applied since it is still currently withdrawable.
2374+
assert.ErrorContains(t, staker.DecreaseStake(acc, id, MinStake), "next period stake is too low for validator")
2375+
// Instantly withdraw - This is bad, it pulls from the PendingLocked, which means total stake later will be 0.
2376+
// The decrease previously marked as okay since the current TVL + pending TVL was greater than the min stake.
2377+
withdraw1, err := staker.WithdrawStake(acc, id, 0)
2378+
assert.NoError(t, err)
2379+
assert.Equal(t, MinStake, withdraw1, "withdraw should be 0 since we are withdrawing from pending locked")
2380+
2381+
// Housekeep, should move pending locked to locked, and pending withdraw to withdrawable
2382+
_, _, err = staker.Housekeep(LowStakingPeriod)
2383+
assert.NoError(t, err)
2384+
2385+
// Withdraw again
2386+
withdraw2, err := staker.WithdrawStake(acc, id, LowStakingPeriod+cooldownPeriod)
2387+
assert.NoError(t, err)
2388+
assert.Equal(t, big.NewInt(0), withdraw2)
2389+
2390+
validator, err := staker.Get(id)
2391+
assert.NoError(t, err)
2392+
assert.Equal(t, 0, validator.LockedVET.Cmp(MinStake), "locked vet should be greater than or equal to min stake")
2393+
}

0 commit comments

Comments
 (0)