code.vegaprotocol.io/vega@v0.79.0/datanode/sqlsubscribers/funding_payments_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package sqlsubscribers_test
    17  
    18  import (
    19  	"context"
    20  	"testing"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/events"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/datanode/entities"
    26  	"code.vegaprotocol.io/vega/datanode/sqlsubscribers"
    27  	"code.vegaprotocol.io/vega/datanode/sqlsubscribers/mocks"
    28  	"code.vegaprotocol.io/vega/libs/num"
    29  	"code.vegaprotocol.io/vega/libs/ptr"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/stretchr/testify/require"
    33  )
    34  
    35  type fpSub struct {
    36  	*sqlsubscribers.FundingPaymentSubscriber
    37  	ctrl  *gomock.Controller
    38  	store *mocks.MockFundingPaymentsStore
    39  }
    40  
    41  func TestBasicInterface(t *testing.T) {
    42  	sub := getFundingPaymentsSub(t)
    43  	ctx := context.Background()
    44  	defer sub.ctrl.Finish()
    45  	types := sub.Types()
    46  	require.Equal(t, 2, len(types))
    47  	require.EqualValues(t, []events.Type{
    48  		events.FundingPaymentsEvent,
    49  		events.LossSocializationEvent,
    50  	}, types)
    51  	require.NoError(t, sub.Flush(ctx))
    52  }
    53  
    54  func TestUnmatchedLossSocCases(t *testing.T) {
    55  	t.Run("simple case getting the previous record and adding a new one with amount lost", testUnmatchedLossSoc)
    56  	t.Run("excess from loss socialisation sets amount lost to zero", testUnmatchedZeroOutLoss)
    57  	t.Run("excess from loss socialisation increases amount", testUnmatchedIncreasesAmount)
    58  	t.Run("excess from loss socialisation decreases loss amount", testUnmatchedDecreasesLoss)
    59  }
    60  
    61  func TestCachedFundingPaymentsAndLossSocialisation(t *testing.T) {
    62  	t.Run("expected flow: funding payment followed by loss socialisation events", processFundingPaymentThenLossSocialisation)
    63  }
    64  
    65  func processFundingPaymentThenLossSocialisation(t *testing.T) {
    66  	sub := getFundingPaymentsSub(t)
    67  	defer sub.ctrl.Finish()
    68  	ctx := context.Background()
    69  	now := time.Now()
    70  	party, market := "partyID", "marketID"
    71  	won := uint64(1000)
    72  	seq := uint64(123)
    73  	loss := num.NewUint(100)
    74  	// first, send the funding payment events: party wins 1000
    75  	fEvt := events.NewFundingPaymentsEvent(ctx, market, seq, []events.Transfer{
    76  		getTransferEvent(party, market, num.NewUint(won), false),
    77  	})
    78  	var got entities.FundingPayment
    79  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).Do(func(_ context.Context, data []*entities.FundingPayment) {
    80  		profit := num.DecimalFromFloat(float64(won))
    81  		require.Equal(t, 1, len(data))
    82  		require.True(t, profit.Equal(data[0].Amount))
    83  		require.True(t, data[0].LossSocialisationAmount.IsZero())
    84  		require.Equal(t, seq, data[0].FundingPeriodSeq)
    85  		got = *data[0]
    86  	}).Return(nil)
    87  
    88  	// step 1: send the funding payment events, see if we get the data we expect.
    89  	sub.Push(ctx, fEvt)
    90  
    91  	// now create the loss socialisation event.
    92  	evt := events.NewLossSocializationEvent(ctx, party, market, loss, true, now.Unix(), types.LossTypeFunding)
    93  	// make sure we process this event
    94  	require.True(t, evt.IsFunding())
    95  
    96  	// should be in cache, so no need to expect a call here
    97  	// sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(0)
    98  	// use the DoAndReturn to make sure the entitiy is updated correctly
    99  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error {
   100  		require.Equal(t, 1, len(data))
   101  		require.Equal(t, got.PartyID, data[0].PartyID)
   102  		require.Equal(t, got.MarketID, data[0].MarketID)
   103  		require.Equal(t, got.FundingPeriodSeq, data[0].FundingPeriodSeq)
   104  		require.True(t, data[0].LossSocialisationAmount.IsNegative())
   105  		require.True(t, data[0].LossSocialisationAmount.IsInteger())
   106  		require.Equal(t, loss.String(), data[0].LossSocialisationAmount.Abs().String())
   107  		// amounts are untouched
   108  		require.True(t, got.Amount.Equal(data[0].Amount))
   109  		return nil
   110  	})
   111  
   112  	sub.Push(ctx, evt)
   113  	// make sure to reset the cache
   114  	sub.Flush(ctx)
   115  }
   116  
   117  func testUnmatchedLossSoc(t *testing.T) {
   118  	sub := getFundingPaymentsSub(t)
   119  	defer sub.ctrl.Finish()
   120  	ctx := context.Background()
   121  	last := time.Now().Add(-1 * time.Second)
   122  	party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix()
   123  	loss := num.NewUint(100)
   124  	get := entities.FundingPayment{
   125  		PartyID:                 entities.PartyID(party),
   126  		MarketID:                entities.MarketID(market),
   127  		FundingPeriodSeq:        123,
   128  		VegaTime:                last,
   129  		Amount:                  num.DecimalZero(),
   130  		LossSocialisationAmount: num.DecimalZero(),
   131  	}
   132  	evt := events.NewLossSocializationEvent(ctx, party, market, loss, true, nowTS, types.LossTypeFunding)
   133  	// make sure we process this event
   134  	require.True(t, evt.IsFunding())
   135  
   136  	sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil)
   137  	// use the DoAndReturn to make sure the entitiy is updated correctly
   138  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error {
   139  		require.Equal(t, 1, len(data))
   140  		require.Equal(t, get.PartyID, data[0].PartyID)
   141  		require.Equal(t, get.MarketID, data[0].MarketID)
   142  		require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq)
   143  		require.True(t, data[0].LossSocialisationAmount.IsNegative())
   144  		require.True(t, data[0].LossSocialisationAmount.IsInteger())
   145  		require.Equal(t, loss.String(), data[0].LossSocialisationAmount.Abs().String())
   146  		return nil
   147  	})
   148  
   149  	sub.Push(ctx, evt)
   150  	// make sure to reset the cache
   151  	sub.Flush(ctx)
   152  }
   153  
   154  func testUnmatchedZeroOutLoss(t *testing.T) {
   155  	sub := getFundingPaymentsSub(t)
   156  	defer sub.ctrl.Finish()
   157  	ctx := context.Background()
   158  	last := time.Now().Add(-1 * time.Second)
   159  	party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix()
   160  	loss := num.NewUint(100)
   161  	get := entities.FundingPayment{
   162  		PartyID:                 entities.PartyID(party),
   163  		MarketID:                entities.MarketID(market),
   164  		FundingPeriodSeq:        123,
   165  		VegaTime:                last,
   166  		Amount:                  num.DecimalZero(),
   167  		LossSocialisationAmount: num.DecimalFromUint(loss).Neg(),
   168  	}
   169  	evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding)
   170  	// make sure we process this event
   171  	require.True(t, evt.IsFunding())
   172  
   173  	sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil)
   174  	// use the DoAndReturn to make sure the entitiy is updated correctly
   175  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error {
   176  		require.Equal(t, 1, len(data))
   177  		require.Equal(t, get.PartyID, data[0].PartyID)
   178  		require.Equal(t, get.MarketID, data[0].MarketID)
   179  		require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq)
   180  		require.True(t, data[0].LossSocialisationAmount.IsZero())
   181  		return nil
   182  	})
   183  
   184  	sub.Push(ctx, evt)
   185  	// make sure to reset the cache
   186  	sub.Flush(ctx)
   187  }
   188  
   189  func testUnmatchedIncreasesAmount(t *testing.T) {
   190  	sub := getFundingPaymentsSub(t)
   191  	defer sub.ctrl.Finish()
   192  	ctx := context.Background()
   193  	last := time.Now().Add(-1 * time.Second)
   194  	party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix()
   195  	loss := num.NewUint(11)
   196  	get := entities.FundingPayment{
   197  		PartyID:                 entities.PartyID(party),
   198  		MarketID:                entities.MarketID(market),
   199  		FundingPeriodSeq:        123,
   200  		VegaTime:                last,
   201  		Amount:                  num.DecimalFromFloat(1000),
   202  		LossSocialisationAmount: num.DecimalFromFloat(1).Neg(),
   203  	}
   204  	evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding)
   205  	// make sure we process this event
   206  	require.True(t, evt.IsFunding())
   207  
   208  	sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil)
   209  	// use the DoAndReturn to make sure the entitiy is updated correctly
   210  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error {
   211  		excess := num.DecimalFromUint(loss).Add(get.LossSocialisationAmount)
   212  		require.Equal(t, 1, len(data))
   213  		require.Equal(t, get.PartyID, data[0].PartyID)
   214  		require.Equal(t, get.MarketID, data[0].MarketID)
   215  		require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq)
   216  		require.True(t, data[0].LossSocialisationAmount.IsZero()) // loss is zeroed out
   217  		// the new amount equals the excess in additional payout (so amount + extra pay - loss amount)
   218  		require.True(t, get.Amount.Add(excess).Equal(data[0].Amount))
   219  		return nil
   220  	})
   221  
   222  	sub.Push(ctx, evt)
   223  	// make sure to reset the cache
   224  	sub.Flush(ctx)
   225  }
   226  
   227  func testUnmatchedDecreasesLoss(t *testing.T) {
   228  	sub := getFundingPaymentsSub(t)
   229  	defer sub.ctrl.Finish()
   230  	ctx := context.Background()
   231  	last := time.Now().Add(-1 * time.Second)
   232  	party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix()
   233  	loss := num.NewUint(10)
   234  	get := entities.FundingPayment{
   235  		PartyID:                 entities.PartyID(party),
   236  		MarketID:                entities.MarketID(market),
   237  		FundingPeriodSeq:        123,
   238  		VegaTime:                last,
   239  		Amount:                  num.DecimalFromFloat(1000),
   240  		LossSocialisationAmount: num.DecimalFromFloat(100).Neg(),
   241  	}
   242  	evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding)
   243  	// make sure we process this event
   244  	require.True(t, evt.IsFunding())
   245  
   246  	sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil)
   247  	// use the DoAndReturn to make sure the entitiy is updated correctly
   248  	sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error {
   249  		require.Equal(t, 1, len(data))
   250  		require.Equal(t, get.PartyID, data[0].PartyID)
   251  		require.Equal(t, get.MarketID, data[0].MarketID)
   252  		require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq)
   253  		// loss soc has been reduced by the amount from the event
   254  		require.True(t, data[0].LossSocialisationAmount.Equal(get.LossSocialisationAmount.Add(num.DecimalFromUint(loss)))) // loss is zeroed out
   255  		// amount paid hasn't changed
   256  		require.True(t, get.Amount.Equal(data[0].Amount))
   257  		return nil
   258  	})
   259  
   260  	sub.Push(ctx, evt)
   261  	// make sure to reset the cache
   262  	sub.Flush(ctx)
   263  }
   264  
   265  func getFundingPaymentsSub(t *testing.T) *fpSub {
   266  	t.Helper()
   267  	ctrl := gomock.NewController(t)
   268  	store := mocks.NewMockFundingPaymentsStore(ctrl)
   269  	sub := sqlsubscribers.NewFundingPaymentsSubscriber(store)
   270  	return &fpSub{
   271  		FundingPaymentSubscriber: sub,
   272  		ctrl:                     ctrl,
   273  		store:                    store,
   274  	}
   275  }
   276  
   277  type mpStub struct {
   278  	party  string
   279  	market string
   280  	amount *num.Uint
   281  	loss   bool
   282  }
   283  
   284  func (m *mpStub) Party() string {
   285  	return m.party
   286  }
   287  
   288  func (m *mpStub) Size() int64 {
   289  	return 0
   290  }
   291  
   292  func (m *mpStub) Buy() int64 {
   293  	return 0
   294  }
   295  
   296  func (m *mpStub) Sell() int64 {
   297  	return 0
   298  }
   299  
   300  func (m *mpStub) Price() *num.Uint {
   301  	return num.UintZero()
   302  }
   303  
   304  func (m *mpStub) BuySumProduct() *num.Uint {
   305  	return num.UintZero()
   306  }
   307  
   308  func (m *mpStub) SellSumProduct() *num.Uint {
   309  	return num.UintZero()
   310  }
   311  
   312  func (m *mpStub) VWBuy() *num.Uint {
   313  	return num.UintZero()
   314  }
   315  
   316  func (m *mpStub) VWSell() *num.Uint {
   317  	return num.UintZero()
   318  }
   319  
   320  func (m *mpStub) AverageEntryPrice() *num.Uint {
   321  	return num.UintZero()
   322  }
   323  
   324  func (m *mpStub) Transfer() *types.Transfer {
   325  	ret := &types.Transfer{
   326  		Owner: m.party,
   327  		Amount: &types.FinancialAmount{
   328  			Asset:  "testasset",
   329  			Amount: m.amount.Clone(),
   330  		},
   331  		Type:       types.TransferTypePerpFundingWin,
   332  		MinAmount:  num.UintZero(),
   333  		Market:     "market",
   334  		TransferID: ptr.From("test"),
   335  	}
   336  	if m.loss {
   337  		ret.Type = types.TransferTypePerpFundingLoss
   338  	}
   339  	return ret
   340  }
   341  
   342  func getTransferEvent(party, market string, amount *num.Uint, loss bool) events.Transfer {
   343  	if amount == nil {
   344  		amount = num.UintZero()
   345  	}
   346  	mp := mpStub{
   347  		party:  party,
   348  		market: market,
   349  		amount: amount.Clone(),
   350  		loss:   loss,
   351  	}
   352  	return &mp
   353  }