From 661c919e0e242e6415a511c4a4b2d2196441ea54 Mon Sep 17 00:00:00 2001 From: Jeremy Letang Date: Tue, 5 Dec 2023 16:13:23 +0000 Subject: [PATCH] Merge pull request #10212 from vegaprotocol/reward-fix fix: ensure infra fees reward are not counted for vesting --- CHANGELOG.md | 6 ++ core/collateral/engine.go | 13 +++++ core/protocol/all_services.go | 1 + core/rewards/engine.go | 9 ++- core/vesting/mocks/mocks.go | 14 +++++ core/vesting/vesting.go | 25 ++++++++- core/vesting/vesting_snapshot.go | 20 ++++++- core/vesting/vesting_snapshot_test.go | 79 +++++++++++++++++++++++++++ 8 files changed, 159 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a37965df9..b9023b2899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### 🐛 Fixes +- [10211](https://github.com/vegaprotocol/vega/issues/10211) - Ensure infra fees don't get counted for vesting. + +## 0.73.7 + +### 🐛 Fixes + - [10166](https://github.com/vegaprotocol/vega/issues/10166) - Closed markets should not be subscribed to data sources when restored from a snapshot. - [10177](https://github.com/vegaprotocol/vega/issues/10177) - Add validation that order sizes are not strangely large. diff --git a/core/collateral/engine.go b/core/collateral/engine.go index c32ec36b73..44440723d9 100644 --- a/core/collateral/engine.go +++ b/core/collateral/engine.go @@ -4360,3 +4360,16 @@ func (e *Engine) TransferSpot(ctx context.Context, partyID, toPartyID, asset str } return res, nil } + +func (e *Engine) GetVestingAccounts() []*types.Account { + accs := []*types.Account{} + for _, a := range e.accs { + if a.Type == types.AccountTypeVestingRewards { + accs = append(accs, a.Clone()) + } + } + sort.Slice(accs, func(i, j int) bool { + return accs[i].ID < accs[j].ID + }) + return accs +} diff --git a/core/protocol/all_services.go b/core/protocol/all_services.go index 80565f5012..6ae4b6df14 100644 --- a/core/protocol/all_services.go +++ b/core/protocol/all_services.go @@ -330,6 +330,7 @@ func newServices( ) svcs.vesting = vesting.NewSnapshotEngine(svcs.log, svcs.collateral, svcs.activityStreak, svcs.broker, svcs.assets) + svcs.timeService.NotifyOnTick(svcs.vesting.OnTick) svcs.rewards = rewards.New(svcs.log, svcs.conf.Rewards, svcs.broker, svcs.delegation, svcs.epochService, svcs.collateral, svcs.timeService, svcs.marketActivityTracker, svcs.topology, svcs.vesting, svcs.banking, svcs.activityStreak) // register this after the rewards engine is created to make sure the on epoch is called in the right order. diff --git a/core/rewards/engine.go b/core/rewards/engine.go index c1a11dd5bb..afa4dd19e3 100644 --- a/core/rewards/engine.go +++ b/core/rewards/engine.go @@ -447,9 +447,12 @@ func (e *Engine) distributePayout(ctx context.Context, po *payout) { return } - for _, party := range partyIDs { - amt := po.partyToAmount[party] - e.vesting.AddReward(party, po.asset, amt, po.lockedForEpochs) + // if the reward type is not infra fee, report it to the vesting engine + if po.rewardType != types.AccountTypeFeesInfrastructure { + for _, party := range partyIDs { + amt := po.partyToAmount[party] + e.vesting.AddReward(party, po.asset, amt, po.lockedForEpochs) + } } e.broker.Send(events.NewLedgerMovements(ctx, responses)) } diff --git a/core/vesting/mocks/mocks.go b/core/vesting/mocks/mocks.go index 4a31a98213..e455b24170 100644 --- a/core/vesting/mocks/mocks.go +++ b/core/vesting/mocks/mocks.go @@ -53,6 +53,20 @@ func (mr *MockCollateralMockRecorder) GetAllVestingQuantumBalance(arg0 interface 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() diff --git a/core/vesting/vesting.go b/core/vesting/vesting.go index 2b1cafd28d..7b61cc5040 100644 --- a/core/vesting/vesting.go +++ b/core/vesting/vesting.go @@ -18,6 +18,7 @@ package vesting import ( "context" "sort" + "time" "code.vegaprotocol.io/vega/core/assets" "code.vegaprotocol.io/vega/core/events" @@ -39,6 +40,7 @@ type Collateral interface { ) ([]*types.LedgerMovement, error) GetVestingRecovery() map[string]map[string]*num.Uint GetAllVestingQuantumBalance(party string) *num.Uint + GetVestingAccounts() []*types.Account } type ActivityStreakVestingMultiplier interface { @@ -76,7 +78,9 @@ type Engine struct { baseRate num.Decimal benefitTiers []*types.VestingBenefitTier - state map[string]*PartyRewards + state map[string]*PartyRewards + epochSeq uint64 + upgradeHackActivated bool } func New( @@ -146,7 +150,9 @@ func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) { } } -func (e *Engine) OnEpochRestore(ctx context.Context, epoch types.Epoch) {} +func (e *Engine) OnEpochRestore(ctx context.Context, epoch types.Epoch) { + e.epochSeq = epoch.Seq +} func (e *Engine) AddReward( party, asset string, @@ -295,6 +301,15 @@ func (e *Engine) distributeVested(ctx context.Context) { e.broker.Send(events.NewLedgerMovements(ctx, responses)) } +// OnTick is called on the beginning of the block. In here +// this is a post upgrade. +func (e *Engine) OnTick(ctx context.Context, _ time.Time) { + if e.upgradeHackActivated { + e.broadcastSummary(ctx, e.epochSeq) + e.upgradeHackActivated = false + } +} + func (e *Engine) makeTransfer( party, assetID string, balance *num.Uint, @@ -343,6 +358,12 @@ 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, diff --git a/core/vesting/vesting_snapshot.go b/core/vesting/vesting_snapshot.go index bf6cf77b14..6f2af09cb9 100644 --- a/core/vesting/vesting_snapshot.go +++ b/core/vesting/vesting_snapshot.go @@ -21,6 +21,7 @@ import ( "sort" "code.vegaprotocol.io/vega/core/types" + vgcontext "code.vegaprotocol.io/vega/libs/context" "code.vegaprotocol.io/vega/libs/num" "code.vegaprotocol.io/vega/libs/proto" "code.vegaprotocol.io/vega/logging" @@ -64,21 +65,34 @@ func (e *SnapshotEngine) GetState(k string) ([]byte, []types.StateProvider, erro return state, nil, err } -func (e *SnapshotEngine) LoadState(_ context.Context, p *types.Payload) ([]types.StateProvider, error) { +func (e *SnapshotEngine) LoadState(ctx context.Context, p *types.Payload) ([]types.StateProvider, error) { if e.Namespace() != p.Data.Namespace() { return nil, types.ErrInvalidSnapshotNamespace } switch data := p.Data.(type) { case *types.PayloadVesting: - e.loadStateFromSnapshot(data.Vesting) + e.loadStateFromSnapshot(ctx, data.Vesting) return nil, nil default: return nil, types.ErrUnknownSnapshotType } } -func (e *SnapshotEngine) loadStateFromSnapshot(state *snapshotpb.Vesting) { +func (e *SnapshotEngine) recoverVesting736() { + e.upgradeHackActivated = true + accs := e.c.GetVestingAccounts() + for _, a := range accs { + e.increaseVestingBalance(a.Owner, a.Asset, a.Balance) + } +} + +func (e *SnapshotEngine) loadStateFromSnapshot(ctx context.Context, state *snapshotpb.Vesting) { + if vgcontext.InProgressUpgradeFrom(ctx, "v0.73.6") { + e.recoverVesting736() + return + } + for _, entry := range state.PartiesReward { for _, v := range entry.InVesting { balance, underflow := num.UintFromString(v.Balance, 10) diff --git a/core/vesting/vesting_snapshot_test.go b/core/vesting/vesting_snapshot_test.go index ae91f49a1a..631b5586f8 100644 --- a/core/vesting/vesting_snapshot_test.go +++ b/core/vesting/vesting_snapshot_test.go @@ -18,11 +18,14 @@ package vesting_test import ( "context" "testing" + "time" "code.vegaprotocol.io/vega/core/assets" + "code.vegaprotocol.io/vega/core/events" "code.vegaprotocol.io/vega/core/types" "code.vegaprotocol.io/vega/core/vesting" "code.vegaprotocol.io/vega/core/vesting/mocks" + vegacontext "code.vegaprotocol.io/vega/libs/context" "code.vegaprotocol.io/vega/libs/num" "code.vegaprotocol.io/vega/libs/proto" "code.vegaprotocol.io/vega/logging" @@ -31,6 +34,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type testSnapshotEngine struct { @@ -115,6 +119,81 @@ func TestSnapshot(t *testing.T) { assert.Equal(t, state1, state2) } +func TestSnapshotUpgrade73_6(t *testing.T) { + v1 := getTestSnapshotEngine(t) + setDefaults(t, v1) + 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) + ctx := vegacontext.WithSnapshotInfo(context.Background(), "v0.73.6", true) + v2.col.EXPECT().GetVestingAccounts().Return([]*types.Account{ + {Owner: "party1", Asset: "eth", Balance: num.NewUint(100)}, + {Owner: "party1", Asset: "btc", Balance: num.NewUint(200)}, + {Owner: "party2", Asset: "btc", Balance: num.NewUint(300)}, + {Owner: "party3", Asset: "doge", Balance: num.NewUint(400)}, + }).Times(1) + + // note a few things here: + // 1. we ignore whatever comes from the snapshot and set the vesting balance to what the collateral account has + // 2. All locked will be set implicitly to 0 + // 3. it is assumed and I think it's safe to do so that it is not possible to have a vesting balance here without having a collateral account, + // so it is guaranteed that we're including in the summary an updateed balance for anyone with a vesting account. + v2.broker.EXPECT().Send(gomock.Any()).Times(1).Do(func(evt events.Event) { + summary := evt.StreamMessage().GetVestingBalancesSummary() + require.Equal(t, uint64(500), summary.EpochSeq) + require.Equal(t, 3, len(summary.PartiesVestingSummary)) + require.Equal(t, "party1", summary.PartiesVestingSummary[0].Party) + require.Equal(t, 0, len(summary.PartiesVestingSummary[0].PartyLockedBalances)) + require.Equal(t, 2, len(summary.PartiesVestingSummary[0].PartyVestingBalances)) + require.Equal(t, "btc", summary.PartiesVestingSummary[0].PartyVestingBalances[0].Asset) + require.Equal(t, "200", summary.PartiesVestingSummary[0].PartyVestingBalances[0].Balance) + require.Equal(t, "eth", summary.PartiesVestingSummary[0].PartyVestingBalances[1].Asset) + require.Equal(t, "100", summary.PartiesVestingSummary[0].PartyVestingBalances[1].Balance) + require.Equal(t, "party2", summary.PartiesVestingSummary[1].Party) + require.Equal(t, 0, len(summary.PartiesVestingSummary[1].PartyLockedBalances)) + require.Equal(t, 1, len(summary.PartiesVestingSummary[1].PartyVestingBalances)) + require.Equal(t, "btc", summary.PartiesVestingSummary[1].PartyVestingBalances[0].Asset) + require.Equal(t, "300", summary.PartiesVestingSummary[1].PartyVestingBalances[0].Balance) + require.Equal(t, "party3", summary.PartiesVestingSummary[2].Party) + require.Equal(t, 0, len(summary.PartiesVestingSummary[2].PartyLockedBalances)) + require.Equal(t, 1, len(summary.PartiesVestingSummary[2].PartyVestingBalances)) + require.Equal(t, "doge", summary.PartiesVestingSummary[2].PartyVestingBalances[0].Asset) + require.Equal(t, "400", summary.PartiesVestingSummary[2].PartyVestingBalances[0].Balance) + }) + v2.OnEpochRestore(ctx, types.Epoch{Seq: 500}) + _, err = v2.LoadState(ctx, types.PayloadFromProto(ppayload)) + assert.NoError(t, err) + + v2.OnTick(ctx, time.Now()) + + // now assert the v2 produce the same state + state2, _, err := v2.GetState(vesting.VestingKey) + assert.NoError(t, err) + assert.NotNil(t, state2) + + // the state won't match as we reset the locked to 0 and we got the full amount into vesting + assert.NotEqual(t, state1, state2) +} + func epochsForward(t *testing.T, v *testSnapshotEngine) { t.Helper()