From 63f94d6083c09486ad33be87cf2cc820db7914d5 Mon Sep 17 00:00:00 2001 From: Valentin Trinque Date: Fri, 3 May 2024 18:58:14 +0200 Subject: [PATCH 1/2] chore: Improve vesting engine tests to ensure proper numbers --- core/collateral/engine.go | 22 +- core/collateral/engine_test.go | 6 +- core/rewards/engine.go | 2 +- core/rewards/mocks/mocks.go | 4 +- core/vesting/{vesting.go => engine.go} | 57 +- core/vesting/engine_test.go | 988 ++++++++++++++++++ core/vesting/helper_for_test.go | 216 ++++ core/vesting/mocks/mocks.go | 121 +-- .../{vesting_snapshot.go => snapshot.go} | 0 core/vesting/snapshot_test.go | 164 +++ core/vesting/vesting_snapshot_test.go | 145 --- core/vesting/vesting_test.go | 468 --------- 12 files changed, 1404 insertions(+), 789 deletions(-) rename core/vesting/{vesting.go => engine.go} (89%) create mode 100644 core/vesting/engine_test.go create mode 100644 core/vesting/helper_for_test.go rename core/vesting/{vesting_snapshot.go => snapshot.go} (100%) create mode 100644 core/vesting/snapshot_test.go delete mode 100644 core/vesting/vesting_snapshot_test.go delete mode 100644 core/vesting/vesting_test.go diff --git a/core/collateral/engine.go b/core/collateral/engine.go index e69042d4d8f..f7b788dd322 100644 --- a/core/collateral/engine.go +++ b/core/collateral/engine.go @@ -219,8 +219,8 @@ func (e *Engine) GetPartyBalance(party string) *num.Uint { return num.UintZero() } -func (e *Engine) GetAllVestingQuantumBalance(party string) *num.Uint { - balance := num.UintZero() +func (e *Engine) GetAllVestingQuantumBalance(party string) num.Decimal { + balance := num.DecimalZero() for asset, details := range e.enabledAssets { // vesting balance @@ -229,14 +229,14 @@ func (e *Engine) GetAllVestingQuantumBalance(party string) *num.Uint { quantum = details.Details.Quantum } if acc, ok := e.accs[e.accountID(noMarket, party, asset, types.AccountTypeVestingRewards)]; ok { - quantumBalance, _ := num.UintFromDecimal(acc.Balance.ToDecimal().Div(quantum)) - balance.AddSum(quantumBalance) + quantumBalance := acc.Balance.ToDecimal().Div(quantum) + balance = balance.Add(quantumBalance) } // vested balance if acc, ok := e.accs[e.accountID(noMarket, party, asset, types.AccountTypeVestedRewards)]; ok { - quantumBalance, _ := num.UintFromDecimal(acc.Balance.ToDecimal().Div(quantum)) - balance.AddSum(quantumBalance) + quantumBalance := acc.Balance.ToDecimal().Div(quantum) + balance = balance.Add(quantumBalance) } } @@ -863,9 +863,7 @@ func (e *Engine) TransferRewards(ctx context.Context, rewardAccountID string, tr return responses, nil } -func (e *Engine) TransferVestedRewards( - ctx context.Context, transfers []*types.Transfer, -) ([]*types.LedgerMovement, error) { +func (e *Engine) TransferVestedRewards(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovement, error) { if len(transfers) == 0 { return nil, nil } @@ -3640,10 +3638,10 @@ func (e *Engine) CreatePartyGeneralAccount(ctx context.Context, partyID, asset s return generalID, nil } -// GetOrCreatePartyVestingAccount create the general account for a party. +// GetOrCreatePartyVestingRewardAccount create the general account for a party. func (e *Engine) GetOrCreatePartyVestingRewardAccount(ctx context.Context, partyID, asset string) *types.Account { if !e.AssetExists(asset) { - e.log.Panic("trying to use a nonexisting asset for reward accounts, something went very wrong somewhere", + e.log.Panic("trying to use a non-existent asset for reward accounts, something went very wrong somewhere", logging.String("asset-id", asset)) } @@ -3673,7 +3671,7 @@ func (e *Engine) GetPartyVestedRewardAccount(partyID, asset string) (*types.Acco return e.GetAccountByID(vested) } -// GetOrCreatePartyVestedAccount create the general account for a party. +// GetOrCreatePartyVestedRewardAccount create the general account for a party. func (e *Engine) GetOrCreatePartyVestedRewardAccount(ctx context.Context, partyID, asset string) *types.Account { if !e.AssetExists(asset) { e.log.Panic("trying to use a nonexisting asset for reward accounts, something went very wrong somewhere", diff --git a/core/collateral/engine_test.go b/core/collateral/engine_test.go index 6878a6f3518..aaea6f9e658 100644 --- a/core/collateral/engine_test.go +++ b/core/collateral/engine_test.go @@ -149,7 +149,7 @@ func TestGetAllVestingQuantumBalance(t *testing.T) { party := "party1" balance := eng.GetAllVestingQuantumBalance(party) - assert.Equal(t, int(balance.Uint64()), 0) + assert.Equal(t, balance.String(), "0") assetT := types.Asset{ ID: "USDC", @@ -179,7 +179,7 @@ func TestGetAllVestingQuantumBalance(t *testing.T) { ) balance = eng.GetAllVestingQuantumBalance(party) - assert.Equal(t, int(balance.Uint64()), 100) + assert.Equal(t, balance.String(), "100") // add some more of the other account // now add some balance to an asset @@ -190,7 +190,7 @@ func TestGetAllVestingQuantumBalance(t *testing.T) { ) balance = eng.GetAllVestingQuantumBalance(party) - assert.Equal(t, int(balance.Uint64()), 103) + assert.Equal(t, balance.String(), "103.17") } func testClearFeeAccounts(t *testing.T) { diff --git a/core/rewards/engine.go b/core/rewards/engine.go index 2d06ea1c28d..7950d4b9d79 100644 --- a/core/rewards/engine.go +++ b/core/rewards/engine.go @@ -89,7 +89,7 @@ type Teams interface { type Vesting interface { AddReward(party, asset string, amount *num.Uint, lockedForEpochs uint64) - GetRewardBonusMultiplier(party string) (*num.Uint, num.Decimal) + GetRewardBonusMultiplier(party string) (num.Decimal, num.Decimal) } type ActivityStreak interface { diff --git a/core/rewards/mocks/mocks.go b/core/rewards/mocks/mocks.go index 171e9bd3e42..0ffd8bf8f41 100644 --- a/core/rewards/mocks/mocks.go +++ b/core/rewards/mocks/mocks.go @@ -374,10 +374,10 @@ func (mr *MockVestingMockRecorder) AddReward(arg0, arg1, arg2, arg3 interface{}) } // GetRewardBonusMultiplier mocks base method. -func (m *MockVesting) GetRewardBonusMultiplier(arg0 string) (*num.Uint, decimal.Decimal) { +func (m *MockVesting) GetRewardBonusMultiplier(arg0 string) (decimal.Decimal, decimal.Decimal) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRewardBonusMultiplier", arg0) - ret0, _ := ret[0].(*num.Uint) + ret0, _ := ret[0].(decimal.Decimal) ret1, _ := ret[1].(decimal.Decimal) return ret0, ret1 } diff --git a/core/vesting/vesting.go b/core/vesting/engine.go similarity index 89% rename from core/vesting/vesting.go rename to core/vesting/engine.go index 333c92e9512..de05bfdd3bf 100644 --- a/core/vesting/vesting.go +++ b/core/vesting/engine.go @@ -32,15 +32,12 @@ import ( "golang.org/x/exp/slices" ) -//go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/vesting Collateral,ActivityStreakVestingMultiplier,Broker,Assets +//go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/vesting ActivityStreakVestingMultiplier,Assets type Collateral interface { - TransferVestedRewards( - ctx context.Context, transfers []*types.Transfer, - ) ([]*types.LedgerMovement, error) + TransferVestedRewards(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovement, error) GetVestingRecovery() map[string]map[string]*num.Uint - GetAllVestingQuantumBalance(party string) *num.Uint - GetVestingAccounts() []*types.Account + GetAllVestingQuantumBalance(party string) num.Decimal } type ActivityStreakVestingMultiplier interface { @@ -111,9 +108,7 @@ func (e *Engine) OnCheckpointLoaded() { } } -func (e *Engine) OnBenefitTiersUpdate( - _ context.Context, v interface{}, -) error { +func (e *Engine) OnBenefitTiersUpdate(_ context.Context, v interface{}) error { tiers, err := types.VestingBenefitTiersFromUntypedProto(v) if err != nil { return err @@ -126,16 +121,12 @@ func (e *Engine) OnBenefitTiersUpdate( return nil } -func (e *Engine) OnRewardVestingBaseRateUpdate( - _ context.Context, baseRate num.Decimal, -) error { +func (e *Engine) OnRewardVestingBaseRateUpdate(_ context.Context, baseRate num.Decimal) error { e.baseRate = baseRate return nil } -func (e *Engine) OnRewardVestingMinimumTransferUpdate( - _ context.Context, minimumTransfer num.Decimal, -) error { +func (e *Engine) OnRewardVestingMinimumTransferUpdate(_ context.Context, minimumTransfer num.Decimal) error { e.minTransfer = minimumTransfer return nil } @@ -145,12 +136,12 @@ func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) { e.broadcastRewardBonusMultipliers(ctx, epoch.Seq) e.moveLocked() e.distributeVested(ctx) - e.clearup() + e.clearState() e.broadcastSummary(ctx, epoch.Seq) } } -func (e *Engine) OnEpochRestore(ctx context.Context, epoch types.Epoch) { +func (e *Engine) OnEpochRestore(_ context.Context, epoch types.Epoch) { e.epochSeq = epoch.Seq } @@ -161,24 +152,20 @@ func (e *Engine) AddReward( ) { // no locktime, just increase the amount in vesting if lockedForEpochs == 0 { - e.increaseVestingBalance( - party, asset, amount, - ) + e.increaseVestingBalance(party, asset, amount) return } - e.increaseLockedForAsset( - party, asset, amount, lockedForEpochs, - ) + e.increaseLockedForAsset(party, asset, amount, lockedForEpochs) } -func (e *Engine) GetRewardBonusMultiplier(party string) (*num.Uint, num.Decimal) { +func (e *Engine) GetRewardBonusMultiplier(party string) (num.Decimal, num.Decimal) { quantumBalance := e.c.GetAllVestingQuantumBalance(party) multiplier := num.DecimalOne() for _, b := range e.benefitTiers { - if quantumBalance.LT(b.MinimumQuantumBalance) { + if quantumBalance.LessThan(num.DecimalFromUint(b.MinimumQuantumBalance)) { break } @@ -234,7 +221,7 @@ func (e *Engine) increaseVestingBalance( partyRewards.Vesting[asset] = vesting } -// checkLocked will move around locked funds. +// moveLocked will move around locked funds. // if the lock for epoch reach 0, the full amount // is added to the vesting amount for the asset. func (e *Engine) moveLocked() { @@ -272,9 +259,7 @@ func (e *Engine) distributeVested(ctx context.Context) { sort.Strings(assets) for _, asset := range assets { balance := rewards.Vesting[asset] - transfer := e.makeTransfer( - party, asset, balance.Clone(), - ) + transfer := e.makeTransfer(party, asset, balance.Clone()) // we are clearing the account, // we can delete it. @@ -289,7 +274,7 @@ func (e *Engine) distributeVested(ctx context.Context) { } // nothing to be done - if len(transfers) <= 0 { + if len(transfers) == 0 { return } @@ -344,9 +329,9 @@ func (e *Engine) makeTransfer( } // just remove party entries once they are not needed anymore. -func (e *Engine) clearup() { +func (e *Engine) clearState() { for party, v := range e.state { - if len(v.Locked) <= 0 && len(v.Vesting) <= 0 { + if len(v.Locked) == 0 && len(v.Vesting) == 0 { delete(e.state, party) } } @@ -358,12 +343,6 @@ func (e *Engine) broadcastSummary(ctx context.Context, seq uint64) { PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, } - parties := make([]string, 0, len(e.state)) - for k := range e.state { - parties = append(parties, k) - } - sort.Strings(parties) - for p, pRewards := range e.state { pSummary := &eventspb.PartyVestingSummary{ Party: p, @@ -427,6 +406,8 @@ func (e *Engine) broadcastRewardBonusMultipliers(ctx context.Context, seq uint64 for _, party := range parties { quantumBalance, multiplier := e.GetRewardBonusMultiplier(party) + // To avoid excessively large decimals. + quantumBalance.Round(2) evt.Stats = append(evt.Stats, &eventspb.PartyVestingStats{ PartyId: party, RewardBonusMultiplier: multiplier.String(), diff --git a/core/vesting/engine_test.go b/core/vesting/engine_test.go new file mode 100644 index 00000000000..58ba3ed61e4 --- /dev/null +++ b/core/vesting/engine_test.go @@ -0,0 +1,988 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vesting_test + +import ( + "context" + "testing" + + "code.vegaprotocol.io/vega/core/assets" + "code.vegaprotocol.io/vega/core/events" + "code.vegaprotocol.io/vega/core/types" + "code.vegaprotocol.io/vega/libs/num" + vegapb "code.vegaprotocol.io/vega/protos/vega" + eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDistributeAfterDelay(t *testing.T) { + v := getTestEngine(t) + + ctx := context.Background() + + // distribute 90% as the base rate, + // so first we distribute some, then we get under the minimum value, and all the rest + // is distributed + require.NoError(t, v.OnRewardVestingBaseRateUpdate(ctx, num.MustDecimalFromString("0.9"))) + // this is multiplied by the quantum, so it will make it 100% of the quantum + require.NoError(t, v.OnRewardVestingMinimumTransferUpdate(ctx, num.MustDecimalFromString("1"))) + + require.NoError(t, v.OnBenefitTiersUpdate(ctx, &vegapb.VestingBenefitTiers{ + Tiers: []*vegapb.VestingBenefitTier{ + { + MinimumQuantumBalance: "200", + RewardMultiplier: "1", + }, + { + MinimumQuantumBalance: "350", + RewardMultiplier: "2", + }, + { + MinimumQuantumBalance: "500", + RewardMultiplier: "3", + }, + }, + })) + + v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) + v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) + + party := "party1" + vegaAsset := "VEGA" + + v.col.InitVestedBalance(party, vegaAsset, num.NewUint(300)) + + epochSeq := uint64(1) + + t.Run("No vesting stats and summary when no reward is being vested", func(t *testing.T) { + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + }) + + t.Run("Add a reward locked for 3 epochs", func(t *testing.T) { + v.AddReward(party, vegaAsset, num.NewUint(100), 3) + }) + + t.Run("Wait for 3 epochs", func(t *testing.T) { + for i := 0; i < 3; i++ { + epochSeq += 1 + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{ + { + Asset: vegaAsset, + UntilEpoch: 5, + Balance: "100", + }, + }, + PartyVestingBalances: []*eventspb.PartyVestingBalance{}, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + } + }) + + t.Run("First reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{}, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "10", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Second reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "390", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("No vesting stats and summary when no reward is being vested anymore", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) +} + +func TestDistributeWithNoDelay(t *testing.T) { + v := getTestEngine(t) + + ctx := context.Background() + + // distribute 90% as the base rate, + // so first we distribute some, then we get under the minimum value, and all the rest + // is distributed + require.NoError(t, v.OnRewardVestingBaseRateUpdate(ctx, num.MustDecimalFromString("0.9"))) + // this is multiplied by the quantum, so it will make it 100% of the quantum + require.NoError(t, v.OnRewardVestingMinimumTransferUpdate(ctx, num.MustDecimalFromString("1"))) + + require.NoError(t, v.OnBenefitTiersUpdate(ctx, &vegapb.VestingBenefitTiers{ + Tiers: []*vegapb.VestingBenefitTier{ + { + MinimumQuantumBalance: "200", + RewardMultiplier: "1", + }, + { + MinimumQuantumBalance: "350", + RewardMultiplier: "2", + }, + { + MinimumQuantumBalance: "500", + RewardMultiplier: "3", + }, + }, + })) + + // set the asvm to return always 1 + v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) + + // set asset to return proper quantum + v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) + + party := "party1" + vegaAsset := "VEGA" + + v.col.InitVestedBalance(party, vegaAsset, num.NewUint(300)) + + epochSeq := uint64(1) + + t.Run("No vesting stats and summary when no reward is being vested", func(t *testing.T) { + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + }) + + t.Run("Add a reward without epoch lock", func(t *testing.T) { + v.AddReward(party, vegaAsset, num.NewUint(100), 0) + }) + + t.Run("First reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{}, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "10", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Second reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "390", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("No vesting stats and summary when no reward is being vested anymore", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) +} + +func TestDistributeWithStreakRate(t *testing.T) { + v := getTestEngine(t) + + ctx := context.Background() + + // distribute 90% as the base rate, + // so first we distribute some, then we get under the minimum value, and all the rest + // is distributed + require.NoError(t, v.OnRewardVestingBaseRateUpdate(ctx, num.MustDecimalFromString("0.9"))) + // this is multiplied by the quantum, so it will make it 100% of the quantum + require.NoError(t, v.OnRewardVestingMinimumTransferUpdate(ctx, num.MustDecimalFromString("1"))) + + require.NoError(t, v.OnBenefitTiersUpdate(ctx, &vegapb.VestingBenefitTiers{ + Tiers: []*vegapb.VestingBenefitTier{ + { + MinimumQuantumBalance: "200", + RewardMultiplier: "1", + }, + { + MinimumQuantumBalance: "350", + RewardMultiplier: "2", + }, + { + MinimumQuantumBalance: "500", + RewardMultiplier: "3", + }, + }, + })) + + v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1.1")) + v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) + + party := "party1" + vegaAsset := "VEGA" + + v.col.InitVestedBalance(party, vegaAsset, num.NewUint(300)) + + epochSeq := uint64(1) + + t.Run("No vesting stats and summary when no reward is being vested", func(t *testing.T) { + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + }) + + t.Run("Add a reward without epoch lock", func(t *testing.T) { + v.AddReward(party, vegaAsset, num.NewUint(100), 0) + }) + + t.Run("First reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{}, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "1", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Second reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "399", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("No vesting stats and summary when no reward is being vested anymore", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) +} + +func TestDistributeMultipleAfterDelay(t *testing.T) { + v := getTestEngine(t) + + ctx := context.Background() + + // distribute 90% as the base rate, + // so first we distribute some, then we get under the minimum value, and all the rest + // is distributed + require.NoError(t, v.OnRewardVestingBaseRateUpdate(ctx, num.MustDecimalFromString("0.9"))) + // this is multiplied by the quantum, so it will make it 100% of the quantum + require.NoError(t, v.OnRewardVestingMinimumTransferUpdate(ctx, num.MustDecimalFromString("1"))) + + require.NoError(t, v.OnBenefitTiersUpdate(ctx, &vegapb.VestingBenefitTiers{ + Tiers: []*vegapb.VestingBenefitTier{ + { + MinimumQuantumBalance: "200", + RewardMultiplier: "1", + }, + { + MinimumQuantumBalance: "350", + RewardMultiplier: "2", + }, + { + MinimumQuantumBalance: "500", + RewardMultiplier: "3", + }, + }, + })) + + v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) + v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) + + party := "party1" + vegaAsset := "VEGA" + + v.col.InitVestedBalance(party, vegaAsset, num.NewUint(300)) + + epochSeq := uint64(1) + + t.Run("No vesting stats and summary when no reward is being vested", func(t *testing.T) { + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + }) + + t.Run("Add a reward locked for 2 epochs", func(t *testing.T) { + v.AddReward(party, vegaAsset, num.NewUint(100), 2) + }) + + t.Run("Add another reward locked for 1 epoch", func(t *testing.T) { + v.AddReward(party, vegaAsset, num.NewUint(100), 1) + }) + + t.Run("Wait for 1 epoch", func(t *testing.T) { + epochSeq += 1 + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{ + { + Asset: vegaAsset, + UntilEpoch: 3, + Balance: "100", + }, + { + Asset: vegaAsset, + UntilEpoch: 4, + Balance: "100", + }, + }, + PartyVestingBalances: []*eventspb.PartyVestingBalance{}, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Action: vegapb.EpochAction_EPOCH_ACTION_END, + Seq: epochSeq, + }) + }) + + t.Run("First reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "1", + QuantumBalance: "300", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{ + { + Asset: vegaAsset, + UntilEpoch: 4, + Balance: "100", + }, + }, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "10", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Second reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "390", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{}, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "11", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Third reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "489", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{ + { + Party: party, + PartyLockedBalances: []*eventspb.PartyLockedBalance{}, + PartyVestingBalances: []*eventspb.PartyVestingBalance{ + { + Asset: vegaAsset, + Balance: "1", + }, + }, + }, + }, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("Fourth reward payment", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{ + { + PartyId: party, + RewardBonusMultiplier: "2", + QuantumBalance: "499", + }, + }, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data + // consistency. + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) + + t.Run("No vesting stats and summary when no reward is being vested anymore", func(t *testing.T) { + epochSeq += 1 + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingStatsUpdated) + require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) + assert.Equal(t, eventspb.VestingStatsUpdated{ + AtEpoch: epochSeq, + Stats: []*eventspb.PartyVestingStats{}, + }, e.Proto()) + }).Times(1) + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.VestingBalancesSummary) + require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) + assert.Equal(t, eventspb.VestingBalancesSummary{ + EpochSeq: epochSeq, + PartiesVestingSummary: []*eventspb.PartyVestingSummary{}, + }, e.Proto()) + }).Times(1) + + v.OnEpochEvent(ctx, types.Epoch{ + Seq: epochSeq, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + }) + }) +} diff --git a/core/vesting/helper_for_test.go b/core/vesting/helper_for_test.go new file mode 100644 index 00000000000..485ed784545 --- /dev/null +++ b/core/vesting/helper_for_test.go @@ -0,0 +1,216 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vesting_test + +import ( + "context" + "testing" + "time" + + "code.vegaprotocol.io/vega/core/assets/common" + bmocks "code.vegaprotocol.io/vega/core/broker/mocks" + "code.vegaprotocol.io/vega/core/integration/stubs" + "code.vegaprotocol.io/vega/core/snapshot" + "code.vegaprotocol.io/vega/core/stats" + "code.vegaprotocol.io/vega/core/types" + "code.vegaprotocol.io/vega/core/vesting" + "code.vegaprotocol.io/vega/core/vesting/mocks" + "code.vegaprotocol.io/vega/libs/num" + "code.vegaprotocol.io/vega/logging" + "code.vegaprotocol.io/vega/paths" + vegapb "code.vegaprotocol.io/vega/protos/vega" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +type testEngine struct { + *vesting.Engine + + ctrl *gomock.Controller + col *collateralMock + asvm *mocks.MockActivityStreakVestingMultiplier + broker *bmocks.MockBroker + assets *mocks.MockAssets +} + +func getTestEngine(t *testing.T) *testEngine { + t.Helper() + ctrl := gomock.NewController(t) + logger := logging.NewTestLogger() + col := newCollateralMock(t) + broker := bmocks.NewMockBroker(ctrl) + asvm := mocks.NewMockActivityStreakVestingMultiplier(ctrl) + assets := mocks.NewMockAssets(ctrl) + + return &testEngine{ + Engine: vesting.New( + logger, col, asvm, broker, assets, + ), + ctrl: ctrl, + broker: broker, + col: col, + asvm: asvm, + assets: assets, + } +} + +type testSnapshotEngine struct { + engine *vesting.SnapshotEngine + + ctrl *gomock.Controller + col *collateralMock + asvm *mocks.MockActivityStreakVestingMultiplier + broker *bmocks.MockBroker + assets *mocks.MockAssets + + currentEpoch uint64 +} + +func newEngine(t *testing.T) *testSnapshotEngine { + t.Helper() + ctrl := gomock.NewController(t) + col := newCollateralMock(t) + asvm := mocks.NewMockActivityStreakVestingMultiplier(ctrl) + broker := bmocks.NewMockBroker(ctrl) + assets := mocks.NewMockAssets(ctrl) + + return &testSnapshotEngine{ + engine: vesting.NewSnapshotEngine( + logging.NewTestLogger(), col, asvm, broker, assets, + ), + ctrl: ctrl, + col: col, + asvm: asvm, + broker: broker, + assets: assets, + currentEpoch: 10, + } +} + +type collateralMock struct { + vestedAccountAmount map[string]map[string]*num.Uint +} + +func (c *collateralMock) InitVestedBalance(party, asset string, balance *num.Uint) { + c.vestedAccountAmount[party] = map[string]*num.Uint{ + asset: balance, + } +} + +func (c *collateralMock) TransferVestedRewards(_ context.Context, transfers []*types.Transfer) ([]*types.LedgerMovement, error) { + for _, transfer := range transfers { + vestedAccount, ok := c.vestedAccountAmount[transfer.Owner] + if !ok { + vestedAccount = map[string]*num.Uint{} + c.vestedAccountAmount[transfer.Owner] = map[string]*num.Uint{} + } + + amount, ok := vestedAccount[transfer.Amount.Asset] + if !ok { + amount = num.UintZero() + vestedAccount[transfer.Amount.Asset] = amount + } + + amount.AddSum(transfer.Amount.Amount) + } + return []*types.LedgerMovement{}, nil +} + +func (c *collateralMock) GetVestingRecovery() map[string]map[string]*num.Uint { + // Only used for checkpoint. + return nil +} + +// GetAllVestingQuantumBalance is a custom implementation used to ensure +// the vesting engine account for benefit tiers during computation. +// Using this implementation saves us from mocking this at every call to +// `OnEpochEvent()` with consistent results. +func (c *collateralMock) GetAllVestingQuantumBalance(party string) num.Decimal { + vestedAccount, ok := c.vestedAccountAmount[party] + if !ok { + return num.DecimalZero() + } + + balance := num.DecimalZero() + for _, n := range vestedAccount { + balance = balance.Add(num.DecimalFromUint(n)) + } + + return balance +} + +func newCollateralMock(t *testing.T) *collateralMock { + t.Helper() + + return &collateralMock{ + vestedAccountAmount: make(map[string]map[string]*num.Uint), + } +} + +type dummyAsset struct { + quantum uint64 +} + +func (d dummyAsset) Type() *types.Asset { + return &types.Asset{ + Details: &types.AssetDetails{ + Quantum: num.DecimalFromInt64(int64(d.quantum)), + }, + } +} + +func (dummyAsset) GetAssetClass() common.AssetClass { return common.ERC20 } +func (dummyAsset) IsValid() bool { return true } +func (dummyAsset) SetPendingListing() {} +func (dummyAsset) SetRejected() {} +func (dummyAsset) SetEnabled() {} +func (dummyAsset) SetValid() {} +func (dummyAsset) String() string { return "" } + +func newSnapshotEngine(t *testing.T, vegaPath paths.Paths, now time.Time, engine *vesting.SnapshotEngine) *snapshot.Engine { + t.Helper() + + log := logging.NewTestLogger() + timeService := stubs.NewTimeStub() + timeService.SetTime(now) + statsData := stats.New(log, stats.NewDefaultConfig()) + config := snapshot.DefaultConfig() + + snapshotEngine, err := snapshot.NewEngine(vegaPath, config, log, timeService, statsData.Blockchain) + require.NoError(t, err) + + snapshotEngine.AddProviders(engine) + + return snapshotEngine +} + +func nextEpoch(ctx context.Context, t *testing.T, te *testSnapshotEngine, startEpochTime time.Time) { + t.Helper() + + te.engine.OnEpochEvent(ctx, types.Epoch{ + Seq: te.currentEpoch, + Action: vegapb.EpochAction_EPOCH_ACTION_END, + EndTime: startEpochTime.Add(-1 * time.Second), + }) + + te.currentEpoch += 1 + te.engine.OnEpochEvent(ctx, types.Epoch{ + Seq: te.currentEpoch, + Action: vegapb.EpochAction_EPOCH_ACTION_START, + StartTime: startEpochTime, + }) +} diff --git a/core/vesting/mocks/mocks.go b/core/vesting/mocks/mocks.go index e455b24170f..ef3e5e11c01 100644 --- a/core/vesting/mocks/mocks.go +++ b/core/vesting/mocks/mocks.go @@ -1,101 +1,17 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: code.vegaprotocol.io/vega/core/vesting (interfaces: Collateral,ActivityStreakVestingMultiplier,Broker,Assets) +// Source: code.vegaprotocol.io/vega/core/vesting (interfaces: ActivityStreakVestingMultiplier,Assets) // Package mocks is a generated GoMock package. package mocks import ( - context "context" reflect "reflect" assets "code.vegaprotocol.io/vega/core/assets" - events "code.vegaprotocol.io/vega/core/events" - types "code.vegaprotocol.io/vega/core/types" - num "code.vegaprotocol.io/vega/libs/num" gomock "github.com/golang/mock/gomock" decimal "github.com/shopspring/decimal" ) -// MockCollateral is a mock of Collateral interface. -type MockCollateral struct { - ctrl *gomock.Controller - recorder *MockCollateralMockRecorder -} - -// MockCollateralMockRecorder is the mock recorder for MockCollateral. -type MockCollateralMockRecorder struct { - mock *MockCollateral -} - -// NewMockCollateral creates a new mock instance. -func NewMockCollateral(ctrl *gomock.Controller) *MockCollateral { - mock := &MockCollateral{ctrl: ctrl} - mock.recorder = &MockCollateralMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCollateral) EXPECT() *MockCollateralMockRecorder { - return m.recorder -} - -// GetAllVestingQuantumBalance mocks base method. -func (m *MockCollateral) GetAllVestingQuantumBalance(arg0 string) *num.Uint { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllVestingQuantumBalance", arg0) - ret0, _ := ret[0].(*num.Uint) - return ret0 -} - -// GetAllVestingQuantumBalance indicates an expected call of GetAllVestingQuantumBalance. -func (mr *MockCollateralMockRecorder) GetAllVestingQuantumBalance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllVestingQuantumBalance", reflect.TypeOf((*MockCollateral)(nil).GetAllVestingQuantumBalance), arg0) -} - -// GetVestingAccounts mocks base method. -func (m *MockCollateral) GetVestingAccounts() []*types.Account { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVestingAccounts") - ret0, _ := ret[0].([]*types.Account) - return ret0 -} - -// GetVestingAccounts indicates an expected call of GetVestingAccounts. -func (mr *MockCollateralMockRecorder) GetVestingAccounts() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVestingAccounts", reflect.TypeOf((*MockCollateral)(nil).GetVestingAccounts)) -} - -// GetVestingRecovery mocks base method. -func (m *MockCollateral) GetVestingRecovery() map[string]map[string]*num.Uint { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVestingRecovery") - ret0, _ := ret[0].(map[string]map[string]*num.Uint) - return ret0 -} - -// GetVestingRecovery indicates an expected call of GetVestingRecovery. -func (mr *MockCollateralMockRecorder) GetVestingRecovery() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVestingRecovery", reflect.TypeOf((*MockCollateral)(nil).GetVestingRecovery)) -} - -// TransferVestedRewards mocks base method. -func (m *MockCollateral) TransferVestedRewards(arg0 context.Context, arg1 []*types.Transfer) ([]*types.LedgerMovement, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TransferVestedRewards", arg0, arg1) - ret0, _ := ret[0].([]*types.LedgerMovement) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TransferVestedRewards indicates an expected call of TransferVestedRewards. -func (mr *MockCollateralMockRecorder) TransferVestedRewards(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferVestedRewards", reflect.TypeOf((*MockCollateral)(nil).TransferVestedRewards), arg0, arg1) -} - // MockActivityStreakVestingMultiplier is a mock of ActivityStreakVestingMultiplier interface. type MockActivityStreakVestingMultiplier struct { ctrl *gomock.Controller @@ -133,41 +49,6 @@ func (mr *MockActivityStreakVestingMultiplierMockRecorder) GetRewardsVestingMult return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewardsVestingMultiplier", reflect.TypeOf((*MockActivityStreakVestingMultiplier)(nil).GetRewardsVestingMultiplier), arg0) } -// MockBroker is a mock of Broker interface. -type MockBroker struct { - ctrl *gomock.Controller - recorder *MockBrokerMockRecorder -} - -// MockBrokerMockRecorder is the mock recorder for MockBroker. -type MockBrokerMockRecorder struct { - mock *MockBroker -} - -// NewMockBroker creates a new mock instance. -func NewMockBroker(ctrl *gomock.Controller) *MockBroker { - mock := &MockBroker{ctrl: ctrl} - mock.recorder = &MockBrokerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockBroker) EXPECT() *MockBrokerMockRecorder { - return m.recorder -} - -// Send mocks base method. -func (m *MockBroker) Send(arg0 events.Event) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Send", arg0) -} - -// Send indicates an expected call of Send. -func (mr *MockBrokerMockRecorder) Send(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockBroker)(nil).Send), arg0) -} - // MockAssets is a mock of Assets interface. type MockAssets struct { ctrl *gomock.Controller diff --git a/core/vesting/vesting_snapshot.go b/core/vesting/snapshot.go similarity index 100% rename from core/vesting/vesting_snapshot.go rename to core/vesting/snapshot.go diff --git a/core/vesting/snapshot_test.go b/core/vesting/snapshot_test.go new file mode 100644 index 00000000000..6ee3573d96a --- /dev/null +++ b/core/vesting/snapshot_test.go @@ -0,0 +1,164 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vesting_test + +import ( + "context" + "testing" + "time" + + "code.vegaprotocol.io/vega/core/assets" + "code.vegaprotocol.io/vega/core/types" + "code.vegaprotocol.io/vega/libs/num" + vgtest "code.vegaprotocol.io/vega/libs/test" + "code.vegaprotocol.io/vega/paths" + vegapb "code.vegaprotocol.io/vega/protos/vega" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSnapshotEngine(t *testing.T) { + ctx := vgtest.VegaContext("chainid", 100) + + vegaPath := paths.New(t.TempDir()) + now := time.Now() + + te1 := newEngine(t) + snapshotEngine1 := newSnapshotEngine(t, vegaPath, now, te1.engine) + closeSnapshotEngine1 := vgtest.OnlyOnce(snapshotEngine1.Close) + defer closeSnapshotEngine1() + + require.NoError(t, snapshotEngine1.Start(ctx)) + + setupMocks(t, te1) + setupNetParams(ctx, t, te1) + + te1.engine.AddReward("party1", "eth", num.NewUint(100), 4) + te1.engine.AddReward("party1", "btc", num.NewUint(150), 1) + te1.engine.AddReward("party1", "eth", num.NewUint(200), 0) + + nextEpoch(ctx, t, te1, time.Now()) + + te1.engine.AddReward("party2", "btc", num.NewUint(100), 2) + te1.engine.AddReward("party3", "btc", num.NewUint(100), 0) + + nextEpoch(ctx, t, te1, time.Now()) + + te1.engine.AddReward("party4", "eth", num.NewUint(100), 1) + te1.engine.AddReward("party5", "doge", num.NewUint(100), 0) + + // Take a snapshot. + hash1, err := snapshotEngine1.SnapshotNow(ctx) + require.NoError(t, err) + snapshottedEpoch := te1.currentEpoch + + // This is what must be replayed after snapshot restoration. + replayFn := func(te *testSnapshotEngine) { + te.engine.AddReward("party6", "doge", num.NewUint(100), 3) + + nextEpoch(ctx, t, te, time.Now()) + + te.engine.AddReward("party7", "eth", num.NewUint(100), 2) + te.engine.AddReward("party8", "vega", num.NewUint(100), 10) + + nextEpoch(ctx, t, te, time.Now()) + } + + replayFn(te1) + + state1 := map[string][]byte{} + for _, key := range te1.engine.Keys() { + state, additionalProvider, err := te1.engine.GetState(key) + require.NoError(t, err) + assert.Empty(t, additionalProvider) + state1[key] = state + } + + closeSnapshotEngine1() + + // Reload the engine using the previous snapshot. + + te2 := newEngine(t) + snapshotEngine2 := newSnapshotEngine(t, vegaPath, now, te2.engine) + defer snapshotEngine2.Close() + + setupMocks(t, te2) + setupNetParams(ctx, t, te2) + + // Ensure the engine's epoch (and test helpers) starts at the same epoch the + // first engine has been snapshotted. + te2.currentEpoch = snapshottedEpoch + te2.engine.OnEpochRestore(ctx, types.Epoch{ + Seq: snapshottedEpoch, + Action: vegapb.EpochAction_EPOCH_ACTION_START, + }) + + // This triggers the state restoration from the local snapshot. + require.NoError(t, snapshotEngine2.Start(ctx)) + + // Comparing the hash after restoration, to ensure it produces the same result. + hash2, _, _ := snapshotEngine2.Info() + require.Equal(t, hash1, hash2) + + // Replaying the same commands after snapshot has been taken with first engine. + replayFn(te2) + + state2 := map[string][]byte{} + for _, key := range te2.engine.Keys() { + state, additionalProvider, err := te2.engine.GetState(key) + require.NoError(t, err) + assert.Empty(t, additionalProvider) + state2[key] = state + } + + for key := range state1 { + assert.Equalf(t, state1[key], state2[key], "Key %q does not have the same data", key) + } +} + +func setupNetParams(ctx context.Context, t *testing.T, te *testSnapshotEngine) { + t.Helper() + + require.NoError(t, te.engine.OnBenefitTiersUpdate(ctx, &vegapb.VestingBenefitTiers{ + Tiers: []*vegapb.VestingBenefitTier{ + { + MinimumQuantumBalance: "10000", + RewardMultiplier: "1.5", + }, + { + MinimumQuantumBalance: "100000", + RewardMultiplier: "2", + }, + { + MinimumQuantumBalance: "500000", + RewardMultiplier: "2.5", + }, + }, + })) + + require.NoError(t, te.engine.OnRewardVestingBaseRateUpdate(ctx, num.MustDecimalFromString("0.9"))) + require.NoError(t, te.engine.OnRewardVestingMinimumTransferUpdate(ctx, num.MustDecimalFromString("1"))) +} + +func setupMocks(t *testing.T, te *testSnapshotEngine) { + t.Helper() + + te.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) + te.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) + te.broker.EXPECT().Send(gomock.Any()).AnyTimes() +} diff --git a/core/vesting/vesting_snapshot_test.go b/core/vesting/vesting_snapshot_test.go deleted file mode 100644 index ae91f49a1a1..00000000000 --- a/core/vesting/vesting_snapshot_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (C) 2023 Gobalsky Labs Limited -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package vesting_test - -import ( - "context" - "testing" - - "code.vegaprotocol.io/vega/core/assets" - "code.vegaprotocol.io/vega/core/types" - "code.vegaprotocol.io/vega/core/vesting" - "code.vegaprotocol.io/vega/core/vesting/mocks" - "code.vegaprotocol.io/vega/libs/num" - "code.vegaprotocol.io/vega/libs/proto" - "code.vegaprotocol.io/vega/logging" - vegapb "code.vegaprotocol.io/vega/protos/vega" - snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -type testSnapshotEngine struct { - *vesting.SnapshotEngine - - ctrl *gomock.Controller - col *mocks.MockCollateral - asvm *mocks.MockActivityStreakVestingMultiplier - broker *mocks.MockBroker - assets *mocks.MockAssets -} - -func getTestSnapshotEngine(t *testing.T) *testSnapshotEngine { - t.Helper() - ctrl := gomock.NewController(t) - col := mocks.NewMockCollateral(ctrl) - asvm := mocks.NewMockActivityStreakVestingMultiplier(ctrl) - broker := mocks.NewMockBroker(ctrl) - assets := mocks.NewMockAssets(ctrl) - - return &testSnapshotEngine{ - SnapshotEngine: vesting.NewSnapshotEngine( - logging.NewTestLogger(), col, asvm, broker, assets, - ), - ctrl: ctrl, - col: col, - asvm: asvm, - broker: broker, - assets: assets, - } -} - -func TestSnapshot(t *testing.T) { - v1 := getTestSnapshotEngine(t) - setDefaults(t, v1) - - // set couple of rewards - v1.AddReward("party1", "eth", num.NewUint(100), 4) - v1.AddReward("party1", "btc", num.NewUint(150), 1) - v1.AddReward("party1", "eth", num.NewUint(200), 0) - v1.AddReward("party2", "btc", num.NewUint(100), 2) - v1.AddReward("party3", "btc", num.NewUint(100), 0) - v1.AddReward("party4", "eth", num.NewUint(100), 1) - v1.AddReward("party5", "doge", num.NewUint(100), 0) - v1.AddReward("party5", "btc", num.NewUint(1420), 1) - v1.AddReward("party6", "doge", num.NewUint(100), 3) - v1.AddReward("party7", "eth", num.NewUint(100), 2) - v1.AddReward("party8", "vega", num.NewUint(100), 10) - - state1, _, err := v1.GetState(vesting.VestingKey) - assert.NoError(t, err) - assert.NotNil(t, state1) - - ppayload := &snapshotpb.Payload{} - err = proto.Unmarshal(state1, ppayload) - assert.NoError(t, err) - - v2 := getTestSnapshotEngine(t) - setDefaults(t, v2) - _, err = v2.LoadState(context.Background(), types.PayloadFromProto(ppayload)) - assert.NoError(t, err) - - // now assert the v2 produce the same state - state2, _, err := v2.GetState(vesting.VestingKey) - assert.NoError(t, err) - assert.NotNil(t, state2) - - assert.Equal(t, state1, state2) - - // now move a couple of epoch for good measure - epochsForward(t, v1) - epochsForward(t, v2) - - // now assert the v2 produce the same state - state1, _, err = v1.GetState(vesting.VestingKey) - assert.NoError(t, err) - assert.NotNil(t, state1) - state2, _, err = v2.GetState(vesting.VestingKey) - assert.NoError(t, err) - assert.NotNil(t, state2) - - assert.Equal(t, state1, state2) -} - -func epochsForward(t *testing.T, v *testSnapshotEngine) { - t.Helper() - - // expect at least 3 transfers and events call, 1 per epoch move - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(3).Return(nil, nil) - v.col.EXPECT().GetAllVestingQuantumBalance(gomock.Any()).AnyTimes().Return(num.UintZero()) - v.broker.EXPECT().Send(gomock.Any()).Times(9) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) -} - -func setDefaults(t *testing.T, v *testSnapshotEngine) { - t.Helper() - v.OnRewardVestingBaseRateUpdate(context.Background(), num.MustDecimalFromString("0.9")) - v.OnRewardVestingMinimumTransferUpdate(context.Background(), num.MustDecimalFromString("1")) - v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) - v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return( - assets.NewAsset(dummyAsset{quantum: 10}), nil, - ) -} diff --git a/core/vesting/vesting_test.go b/core/vesting/vesting_test.go deleted file mode 100644 index 8396ae1992d..00000000000 --- a/core/vesting/vesting_test.go +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright (C) 2023 Gobalsky Labs Limited -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package vesting_test - -import ( - "context" - "testing" - - "code.vegaprotocol.io/vega/core/assets" - "code.vegaprotocol.io/vega/core/assets/common" - "code.vegaprotocol.io/vega/core/types" - "code.vegaprotocol.io/vega/core/vesting" - "code.vegaprotocol.io/vega/core/vesting/mocks" - "code.vegaprotocol.io/vega/libs/num" - "code.vegaprotocol.io/vega/logging" - vegapb "code.vegaprotocol.io/vega/protos/vega" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -type testEngine struct { - *vesting.Engine - - ctrl *gomock.Controller - col *mocks.MockCollateral - asvm *mocks.MockActivityStreakVestingMultiplier - broker *mocks.MockBroker - assets *mocks.MockAssets -} - -func getTestEngine(t *testing.T) *testEngine { - t.Helper() - ctrl := gomock.NewController(t) - col := mocks.NewMockCollateral(ctrl) - asvm := mocks.NewMockActivityStreakVestingMultiplier(ctrl) - broker := mocks.NewMockBroker(ctrl) - assets := mocks.NewMockAssets(ctrl) - - return &testEngine{ - Engine: vesting.New( - logging.NewTestLogger(), col, asvm, broker, assets, - ), - ctrl: ctrl, - col: col, - asvm: asvm, - broker: broker, - assets: assets, - } -} - -func TestRewardMultiplier(t *testing.T) { - v := getTestEngine(t) - - // set benefits tiers - err := v.OnBenefitTiersUpdate(context.Background(), &vegapb.VestingBenefitTiers{ - Tiers: []*vegapb.VestingBenefitTier{ - { - MinimumQuantumBalance: "10000", - RewardMultiplier: "1.5", - }, - { - MinimumQuantumBalance: "100000", - RewardMultiplier: "2", - }, - { - MinimumQuantumBalance: "500000", - RewardMultiplier: "2.5", - }, - }, - }) - - assert.NoError(t, err) - - v.col.EXPECT().GetAllVestingQuantumBalance("party1").Times(1).Return(num.UintZero()) - quantumBalance, bonus := v.GetRewardBonusMultiplier("party1") - assert.Equal(t, num.DecimalOne(), bonus) - assert.Equal(t, num.UintZero(), quantumBalance) - - v.col.EXPECT().GetAllVestingQuantumBalance("party1").Times(1).Return(num.NewUint(10001)) - quantumBalance, bonus = v.GetRewardBonusMultiplier("party1") - assert.Equal(t, num.MustDecimalFromString("1.5"), bonus) - assert.Equal(t, num.MustUintFromString("10001", 10), quantumBalance) - - v.col.EXPECT().GetAllVestingQuantumBalance("party1").Times(1).Return(num.NewUint(100001)) - quantumBalance, bonus = v.GetRewardBonusMultiplier("party1") - assert.Equal(t, num.MustDecimalFromString("2"), bonus) - assert.Equal(t, num.MustUintFromString("100001", 10), quantumBalance) - - v.col.EXPECT().GetAllVestingQuantumBalance("party1").Times(1).Return(num.NewUint(500001)) - quantumBalance, bonus = v.GetRewardBonusMultiplier("party1") - assert.Equal(t, num.MustDecimalFromString("2.5"), bonus) - assert.Equal(t, num.MustUintFromString("500001", 10), quantumBalance) -} - -func TestDistributeAfterDelay(t *testing.T) { - v := getTestEngine(t) - - // distribute 90% as the base rate, - // so first we distribute some, then we get under the minimum value, and all the rest - // is distributed - v.OnRewardVestingBaseRateUpdate(context.Background(), num.MustDecimalFromString("0.9")) - // this is multiplied by the quantume, so it will make it 100% of the quantum - v.OnRewardVestingMinimumTransferUpdate(context.Background(), num.MustDecimalFromString("1")) - - v.col.EXPECT().GetAllVestingQuantumBalance(gomock.Any()).AnyTimes().Return(num.UintZero()) - - // set the asvm to return always 1 - v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) - - // set asset to return proper quantum - v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // Add a reward to be locked for 3 epochs then - // we add a 100 of the reward. - // it will be paid in 2 times, first 90, - // then the remain 10, - // and it'll be all - v.AddReward("party1", "eth", num.NewUint(100), 3) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect 1 call to the collateral for the transfer of 90, for the transfer of the 90 - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 90) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 90) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect 1 call to the collateral for the transfer of 10, for the transfer of the 90, which is the whole remaining thing - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 10) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 10) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // try it again and nothing happen - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) -} - -func TestDistributeWithNoDelay(t *testing.T) { - v := getTestEngine(t) - - // distribute 90% as the base rate, - // so first we distribute some, then we get under the minimum value, and all the rest - // is distributed - v.OnRewardVestingBaseRateUpdate(context.Background(), num.MustDecimalFromString("0.9")) - // this is multiplied by the quantume, so it will make it 100% of the quantum - v.OnRewardVestingMinimumTransferUpdate(context.Background(), num.MustDecimalFromString("1")) - - v.col.EXPECT().GetAllVestingQuantumBalance(gomock.Any()).AnyTimes().Return(num.UintZero()) - - // set the asvm to return always 1 - v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) - - // set asset to return proper quantum - v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // we add a 100 of the reward. - // it will be paid in 2 times, first 90, - // then the remain 10, - // and it'll be all - v.AddReward("party1", "eth", num.NewUint(100), 0) - - // now we expect 1 call to the collateral for the transfer of 90, for the transfer of the 90 - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 90) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 90) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect 1 call to the collateral for the transfer of 10, for the transfer of the 90, which is the whole remaining thing - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 10) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 10) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // try it again and nothing happen - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) -} - -func TestDistributeWithStreakRate(t *testing.T) { - v := getTestEngine(t) - - // distribute 90% as the base rate, - // so first we distribute some, then we get under the minimum value, and all the rest - // is distributed - v.OnRewardVestingBaseRateUpdate(context.Background(), num.MustDecimalFromString("0.9")) - // this is multiplied by the quantume, so it will make it 100% of the quantum - v.OnRewardVestingMinimumTransferUpdate(context.Background(), num.MustDecimalFromString("1")) - - v.col.EXPECT().GetAllVestingQuantumBalance(gomock.Any()).AnyTimes().Return(num.UintZero()) - - // set the asvm to return always 1 - v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1.1")) - - // set asset to return proper quantum - v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // Add a reward to be locked for 3 epochs then - // we add a 100 of the reward. - // it will be paid in 2 times, first 90, - // then the remain 10, - // and it'll be all - v.AddReward("party1", "eth", num.NewUint(100), 0) - - // now we expect 1 call to the collateral for the transfer of 99, for the transfer of the 99 - // this is 100 * 0.9 + 1.1 - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 99) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 99) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect 1 call to the collateral for the transfer of 10, for the transfer of the 90, which is the whole remaining thing - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 1) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 1) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // try it again and nothing happen - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) -} - -func TestDistributeMultipleAfterDelay(t *testing.T) { - v := getTestEngine(t) - - // distribute 90% as the base rate, - // so first we distribute some, then we get under the minimum value, and all the rest - // is distributed - v.OnRewardVestingBaseRateUpdate(context.Background(), num.MustDecimalFromString("0.9")) - // this is multiplied by the quantume, so it will make it 100% of the quantum - v.OnRewardVestingMinimumTransferUpdate(context.Background(), num.MustDecimalFromString("1")) - - v.col.EXPECT().GetAllVestingQuantumBalance(gomock.Any()).AnyTimes().Return(num.UintZero()) - - // set the asvm to return always 1 - v.asvm.EXPECT().GetRewardsVestingMultiplier(gomock.Any()).AnyTimes().Return(num.MustDecimalFromString("1")) - - // set asset to return proper quantum - v.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(assets.NewAsset(dummyAsset{quantum: 10}), nil) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // Add a reward to be locked for 2 epochs then - // we add a 100 of the reward. - v.AddReward("party1", "eth", num.NewUint(100), 2) - // then another for 1 epoch - v.AddReward("party1", "eth", num.NewUint(100), 1) - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect 1 call to the collateral for the transfer of 90, for the transfer of the 90 - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 90) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 90) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - // this will deliver 100 more as well ready to be paid - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect another transfer of 99 which is 110*0.9 - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 99) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 99) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect another transfer of 9 which is 110*0.9 floored - // but it's actually defaulting to 10 which is the minimum acceptable transfer - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 10) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 10) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // now we expect another transfer of 1 which is all that is left - v.col.EXPECT().TransferVestedRewards(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( - func(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovements, error) { - assert.Len(t, transfers, 1) - assert.Equal(t, int(transfers[0].Amount.Amount.Uint64()), 1) - assert.Equal(t, transfers[0].Owner, "party1") - assert.Equal(t, int(transfers[0].MinAmount.Uint64()), 1) - assert.Equal(t, transfers[0].Amount.Asset, "eth") - return nil, nil - }, - ) - // one call to the broker - v.broker.EXPECT().Send(gomock.Any()).Times(3) - - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) - - // try it again and nothing happen - v.broker.EXPECT().Send(gomock.Any()).Times(2) - v.OnEpochEvent(context.Background(), types.Epoch{ - Action: vegapb.EpochAction_EPOCH_ACTION_END, - }) -} - -type dummyAsset struct { - quantum uint64 -} - -func (d dummyAsset) Type() *types.Asset { - return &types.Asset{ - Details: &types.AssetDetails{ - Quantum: num.DecimalFromInt64(int64(d.quantum)), - }, - } -} - -func (dummyAsset) GetAssetClass() common.AssetClass { return common.ERC20 } -func (dummyAsset) IsValid() bool { return true } -func (dummyAsset) SetPendingListing() {} -func (dummyAsset) SetRejected() {} -func (dummyAsset) SetEnabled() {} -func (dummyAsset) SetValid() {} -func (dummyAsset) String() string { return "" } From 6fde06119dc813ef433a10e5507a838332545c15 Mon Sep 17 00:00:00 2001 From: Valentin Trinque Date: Fri, 10 May 2024 15:23:02 +0200 Subject: [PATCH 2/2] fix: Ensure the vesting stats events are generated after the computation --- CHANGELOG.md | 2 +- core/collateral/engine_test.go | 2 +- core/vesting/engine.go | 11 ++- core/vesting/engine_test.go | 152 +++++++++++++-------------------- core/vesting/snapshot.go | 3 +- 5 files changed, 67 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a98b02a00a3..f9ca1de97c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ ### 🐛 Fixes -- [](https://github.com/vegaprotocol/vega/issues/xxx) +- [11066](https://github.com/vegaprotocol/vega/issues/11066) - Ensure vesting statistics match vesting accounts numbers. ## 0.76.1 diff --git a/core/collateral/engine_test.go b/core/collateral/engine_test.go index aaea6f9e658..d418e8a015f 100644 --- a/core/collateral/engine_test.go +++ b/core/collateral/engine_test.go @@ -190,7 +190,7 @@ func TestGetAllVestingQuantumBalance(t *testing.T) { ) balance = eng.GetAllVestingQuantumBalance(party) - assert.Equal(t, balance.String(), "103.17") + assert.Equal(t, balance.String(), "103.1666666666666667") } func testClearFeeAccounts(t *testing.T) { diff --git a/core/vesting/engine.go b/core/vesting/engine.go index de05bfdd3bf..81e7852976e 100644 --- a/core/vesting/engine.go +++ b/core/vesting/engine.go @@ -133,11 +133,11 @@ func (e *Engine) OnRewardVestingMinimumTransferUpdate(_ context.Context, minimum func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) { if epoch.Action == proto.EpochAction_EPOCH_ACTION_END { - e.broadcastRewardBonusMultipliers(ctx, epoch.Seq) e.moveLocked() e.distributeVested(ctx) - e.clearState() + e.broadcastVestingStatsUpdate(ctx, epoch.Seq) e.broadcastSummary(ctx, epoch.Seq) + e.clearState() } } @@ -328,7 +328,6 @@ func (e *Engine) makeTransfer( return transfer } -// just remove party entries once they are not needed anymore. func (e *Engine) clearState() { for party, v := range e.state { if len(v.Locked) == 0 && len(v.Vesting) == 0 { @@ -344,6 +343,10 @@ func (e *Engine) broadcastSummary(ctx context.Context, seq uint64) { } for p, pRewards := range e.state { + if len(pRewards.Vesting) == 0 && len(pRewards.Locked) == 0 { + continue + } + pSummary := &eventspb.PartyVestingSummary{ Party: p, PartyLockedBalances: []*eventspb.PartyLockedBalance{}, @@ -395,7 +398,7 @@ func (e *Engine) broadcastSummary(ctx context.Context, seq uint64) { e.broker.Send(events.NewVestingBalancesSummaryEvent(ctx, evt)) } -func (e *Engine) broadcastRewardBonusMultipliers(ctx context.Context, seq uint64) { +func (e *Engine) broadcastVestingStatsUpdate(ctx context.Context, seq uint64) { evt := &eventspb.VestingStatsUpdated{ AtEpoch: seq, Stats: make([]*eventspb.PartyVestingStats, 0, len(e.state)), diff --git a/core/vesting/engine_test.go b/core/vesting/engine_test.go index 58ba3ed61e4..fcaeb17e106 100644 --- a/core/vesting/engine_test.go +++ b/core/vesting/engine_test.go @@ -148,6 +148,9 @@ func TestDistributeAfterDelay(t *testing.T) { t.Run("First reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -156,21 +159,13 @@ func TestDistributeAfterDelay(t *testing.T) { Stats: []*eventspb.PartyVestingStats{ { PartyId: party, - RewardBonusMultiplier: "1", - QuantumBalance: "300", + RewardBonusMultiplier: "2", + QuantumBalance: "390", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -199,6 +194,9 @@ func TestDistributeAfterDelay(t *testing.T) { t.Run("Second reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -208,20 +206,12 @@ func TestDistributeAfterDelay(t *testing.T) { { PartyId: party, RewardBonusMultiplier: "2", - QuantumBalance: "390", + QuantumBalance: "400", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -337,6 +327,9 @@ func TestDistributeWithNoDelay(t *testing.T) { t.Run("First reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -345,21 +338,13 @@ func TestDistributeWithNoDelay(t *testing.T) { Stats: []*eventspb.PartyVestingStats{ { PartyId: party, - RewardBonusMultiplier: "1", - QuantumBalance: "300", + RewardBonusMultiplier: "2", + QuantumBalance: "390", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -388,6 +373,9 @@ func TestDistributeWithNoDelay(t *testing.T) { t.Run("Second reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -397,20 +385,12 @@ func TestDistributeWithNoDelay(t *testing.T) { { PartyId: party, RewardBonusMultiplier: "2", - QuantumBalance: "390", + QuantumBalance: "400", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -523,6 +503,9 @@ func TestDistributeWithStreakRate(t *testing.T) { t.Run("First reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -531,21 +514,13 @@ func TestDistributeWithStreakRate(t *testing.T) { Stats: []*eventspb.PartyVestingStats{ { PartyId: party, - RewardBonusMultiplier: "1", - QuantumBalance: "300", + RewardBonusMultiplier: "2", + QuantumBalance: "399", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -574,6 +549,9 @@ func TestDistributeWithStreakRate(t *testing.T) { t.Run("Second reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -583,20 +561,12 @@ func TestDistributeWithStreakRate(t *testing.T) { { PartyId: party, RewardBonusMultiplier: "2", - QuantumBalance: "399", + QuantumBalance: "400", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -763,6 +733,9 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { t.Run("First reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -771,21 +744,13 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { Stats: []*eventspb.PartyVestingStats{ { PartyId: party, - RewardBonusMultiplier: "1", - QuantumBalance: "300", + RewardBonusMultiplier: "2", + QuantumBalance: "390", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -820,6 +785,9 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { t.Run("Second reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -829,20 +797,12 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { { PartyId: party, RewardBonusMultiplier: "2", - QuantumBalance: "390", + QuantumBalance: "489", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -871,6 +831,9 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { t.Run("Third reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -880,20 +843,12 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { { PartyId: party, RewardBonusMultiplier: "2", - QuantumBalance: "489", + QuantumBalance: "499", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -922,6 +877,9 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { t.Run("Fourth reward payment", func(t *testing.T) { epochSeq += 1 + + expectLedgerMovements(t, v) + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingStatsUpdated) require.True(t, ok, "Event should be a VestingStatsUpdated, but is %T", evt) @@ -930,21 +888,13 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { Stats: []*eventspb.PartyVestingStats{ { PartyId: party, - RewardBonusMultiplier: "2", - QuantumBalance: "499", + RewardBonusMultiplier: "3", + QuantumBalance: "500", }, }, }, e.Proto()) }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { - e, ok := evt.(*events.LedgerMovements) - require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) - // LedgerMovements is the result of a mock, so it doesn't really make sense to verify data - // consistency. - assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) - }).Times(1) - v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { e, ok := evt.(*events.VestingBalancesSummary) require.True(t, ok, "Event should be a VestingBalancesSummary, but is %T", evt) @@ -986,3 +936,15 @@ func TestDistributeMultipleAfterDelay(t *testing.T) { }) }) } + +// LedgerMovements is the result of a mock, so it doesn't really make sense to +// verify data consistency. +func expectLedgerMovements(t *testing.T, v *testEngine) { + t.Helper() + + v.broker.EXPECT().Send(gomock.Any()).Do(func(evt events.Event) { + e, ok := evt.(*events.LedgerMovements) + require.True(t, ok, "Event should be a LedgerMovements, but is %T", evt) + assert.Equal(t, eventspb.LedgerMovements{LedgerMovements: []*vegapb.LedgerMovement{}}, e.Proto()) + }).Times(1) +} diff --git a/core/vesting/snapshot.go b/core/vesting/snapshot.go index 0265014c90c..ea7b9c3261f 100644 --- a/core/vesting/snapshot.go +++ b/core/vesting/snapshot.go @@ -96,8 +96,7 @@ func (e *SnapshotEngine) loadStateFromSnapshot(_ context.Context, state *snapsho e.log.Panic("uint256 in snapshot underflow", logging.String("value", epochBalance.Balance)) } - e.increaseLockedForAsset( - entry.Party, locked.Asset, balance, epochBalance.Epoch) + e.increaseLockedForAsset(entry.Party, locked.Asset, balance, epochBalance.Epoch) } } }