code.vegaprotocol.io/vega@v0.79.0/core/banking/recurring_transfers.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 banking
    17  
    18  import (
    19  	"context"
    20  	"encoding/hex"
    21  	"errors"
    22  	"fmt"
    23  
    24  	"code.vegaprotocol.io/vega/core/events"
    25  	"code.vegaprotocol.io/vega/core/types"
    26  	"code.vegaprotocol.io/vega/libs/crypto"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/libs/proto"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	vegapb "code.vegaprotocol.io/vega/protos/vega"
    31  )
    32  
    33  var (
    34  	ErrStartEpochInThePast                                     = errors.New("start epoch in the past")
    35  	ErrCannotSubmitDuplicateRecurringTransferWithSameFromAndTo = errors.New("cannot submit duplicate recurring transfer with same from and to")
    36  )
    37  
    38  func (e *Engine) recurringTransfer(
    39  	ctx context.Context,
    40  	transfer *types.RecurringTransfer,
    41  ) (err error) {
    42  	defer func() {
    43  		if err != nil {
    44  			e.broker.Send(events.NewRecurringTransferFundsEventWithReason(ctx, transfer, err.Error(), e.getGameID(transfer)))
    45  		} else {
    46  			e.broker.Send(events.NewRecurringTransferFundsEvent(ctx, transfer, e.getGameID(transfer)))
    47  		}
    48  	}()
    49  
    50  	// ensure asset exists
    51  	a, err := e.assets.Get(transfer.Asset)
    52  	if err != nil {
    53  		transfer.Status = types.TransferStatusRejected
    54  		e.log.Debug("cannot transfer funds, invalid asset", logging.Error(err))
    55  		return fmt.Errorf("could not transfer funds: %w", err)
    56  	}
    57  
    58  	if transfer.DispatchStrategy != nil {
    59  		hasAsset := len(transfer.DispatchStrategy.AssetForMetric) > 0
    60  		// ensure the asset transfer is correct
    61  		if hasAsset {
    62  			_, err := e.assets.Get(transfer.DispatchStrategy.AssetForMetric)
    63  			if err != nil {
    64  				transfer.Status = types.TransferStatusRejected
    65  				e.log.Debug("cannot transfer funds, invalid asset for metric", logging.Error(err))
    66  				return fmt.Errorf("could not transfer funds, invalid asset for metric: %w", err)
    67  			}
    68  		}
    69  
    70  		if hasAsset && len(transfer.DispatchStrategy.Markets) > 0 {
    71  			asset := transfer.DispatchStrategy.AssetForMetric
    72  			for _, mid := range transfer.DispatchStrategy.Markets {
    73  				if !e.marketActivityTracker.MarketTrackedForAsset(mid, asset) {
    74  					transfer.Status = types.TransferStatusRejected
    75  					e.log.Debug("cannot transfer funds, invalid market for dispatch asset",
    76  						logging.String("mid", mid),
    77  						logging.String("asset", asset),
    78  					)
    79  					return errors.New("could not transfer funds, invalid market for dispatch asset")
    80  				}
    81  			}
    82  		}
    83  	}
    84  
    85  	if err := transfer.IsValid(); err != nil {
    86  		transfer.Status = types.TransferStatusRejected
    87  		return err
    88  	}
    89  
    90  	if err := e.ensureMinimalTransferAmount(a, transfer.Amount, transfer.FromAccountType, transfer.From, transfer.FromDerivedKey); err != nil {
    91  		transfer.Status = types.TransferStatusRejected
    92  		return err
    93  	}
    94  
    95  	if err := e.ensureNoRecurringTransferDuplicates(transfer); err != nil {
    96  		transfer.Status = types.TransferStatusRejected
    97  		return err
    98  	}
    99  
   100  	// can't create transfer with start epoch in the past
   101  	if transfer.StartEpoch < e.currentEpoch {
   102  		transfer.Status = types.TransferStatusRejected
   103  		return ErrStartEpochInThePast
   104  	}
   105  
   106  	// from here all sounds OK, we can add the transfer
   107  	// in the recurringTransfer map/slice
   108  	e.recurringTransfers = append(e.recurringTransfers, transfer)
   109  	e.recurringTransfersMap[transfer.ID] = transfer
   110  	e.registerDispatchStrategy(transfer.DispatchStrategy)
   111  
   112  	return nil
   113  }
   114  
   115  func (e *Engine) getGameID(transfer *types.RecurringTransfer) *string {
   116  	if transfer.DispatchStrategy == nil {
   117  		return nil
   118  	}
   119  	gameID := e.hashDispatchStrategy(transfer.DispatchStrategy)
   120  	return &gameID
   121  }
   122  
   123  func (e *Engine) hashDispatchStrategy(ds *vegapb.DispatchStrategy) string {
   124  	p, err := proto.Marshal(ds)
   125  	if err != nil {
   126  		e.log.Panic("failed to marshal dispatch strategy", logging.String("dispatch-strategy", ds.String()))
   127  	}
   128  	return hex.EncodeToString(crypto.Hash(p))
   129  }
   130  
   131  func (e *Engine) registerDispatchStrategy(ds *vegapb.DispatchStrategy) {
   132  	if ds == nil {
   133  		return
   134  	}
   135  	hash := e.hashDispatchStrategy(ds)
   136  	if _, ok := e.hashToStrategy[hash]; !ok {
   137  		e.hashToStrategy[hash] = &dispatchStrategyCacheEntry{ds: ds, refCount: 1}
   138  	} else {
   139  		e.hashToStrategy[hash].refCount++
   140  	}
   141  }
   142  
   143  func (e *Engine) unregisterDispatchStrategy(ds *vegapb.DispatchStrategy) {
   144  	if ds == nil {
   145  		return
   146  	}
   147  	hash := e.hashDispatchStrategy(ds)
   148  	e.hashToStrategy[hash].refCount--
   149  }
   150  
   151  func (e *Engine) cleanupStaleDispatchStrategies() {
   152  	for hash, dsc := range e.hashToStrategy {
   153  		if dsc.refCount == 0 {
   154  			delete(e.hashToStrategy, hash)
   155  			e.marketActivityTracker.GameFinished(hash)
   156  		}
   157  	}
   158  }
   159  
   160  func isSimilar(dispatchStrategy1, dispatchStrategy2 *vegapb.DispatchStrategy) bool {
   161  	p1, _ := proto.Marshal(dispatchStrategy1)
   162  	hash1 := hex.EncodeToString(crypto.Hash(p1))
   163  
   164  	p2, _ := proto.Marshal(dispatchStrategy2)
   165  	hash2 := hex.EncodeToString(crypto.Hash(p2))
   166  	return hash1 == hash2
   167  }
   168  
   169  func (e *Engine) ensureNoRecurringTransferDuplicates(
   170  	transfer *types.RecurringTransfer,
   171  ) error {
   172  	for _, v := range e.recurringTransfers {
   173  		// NB: 2 transfers are identical and not allowed if they have the same from, to, type AND the same dispatch strategy.
   174  		// This is needed so that we can for example setup transfer of USDT from one PK to the reward account with type maker fees received with dispatch based on the asset ETH -
   175  		// and then a similar transfer of USDT from the same PK to the same reward type but with different dispatch strategy - one tracking markets for the asset DAI.
   176  		if v.From == transfer.From && v.To == transfer.To && v.Asset == transfer.Asset && v.FromAccountType == transfer.FromAccountType && v.ToAccountType == transfer.ToAccountType && isSimilar(v.DispatchStrategy, transfer.DispatchStrategy) {
   177  			return ErrCannotSubmitDuplicateRecurringTransferWithSameFromAndTo
   178  		}
   179  	}
   180  
   181  	return nil
   182  }
   183  
   184  // dispatchRequired returns true if the metric for any qualifying entity in scope none zero.
   185  // NB1: the check for market value metric should be done separately
   186  // NB2: for validator ranking this will always return true as it is assumed that for the network to resume there must always be
   187  // a validator with non zero ranking.
   188  func (e *Engine) dispatchRequired(ctx context.Context, ds *vegapb.DispatchStrategy) bool {
   189  	required, ok := e.dispatchRequiredCache[e.hashDispatchStrategy(ds)]
   190  	if ok {
   191  		return required
   192  	}
   193  	defer func() { e.dispatchRequiredCache[e.hashDispatchStrategy(ds)] = required }()
   194  	switch ds.Metric {
   195  	case vegapb.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_PAID,
   196  		vegapb.DispatchMetric_DISPATCH_METRIC_MAKER_FEES_RECEIVED,
   197  		vegapb.DispatchMetric_DISPATCH_METRIC_LP_FEES_RECEIVED,
   198  		vegapb.DispatchMetric_DISPATCH_METRIC_AVERAGE_NOTIONAL,
   199  		vegapb.DispatchMetric_DISPATCH_METRIC_RELATIVE_RETURN,
   200  		vegapb.DispatchMetric_DISPATCH_METRIC_RETURN_VOLATILITY,
   201  		vegapb.DispatchMetric_DISPATCH_METRIC_REALISED_RETURN,
   202  		vegapb.DispatchMetric_DISPATCH_METRIC_ELIGIBLE_ENTITIES:
   203  		if ds.EntityScope == vegapb.EntityScope_ENTITY_SCOPE_INDIVIDUALS {
   204  			hasNonZeroMetric := false
   205  			partyMetrics := e.marketActivityTracker.CalculateMetricForIndividuals(ctx, ds)
   206  			gs := events.NewPartyGameScoresEvent(ctx, int64(e.currentEpoch), e.hashDispatchStrategy(ds), e.timeService.GetTimeNow(), partyMetrics)
   207  			e.broker.Send(gs)
   208  			hasEligibleParties := false
   209  			for _, pm := range partyMetrics {
   210  				if !pm.Score.IsZero() {
   211  					hasNonZeroMetric = true
   212  				}
   213  				if pm.IsEligible {
   214  					hasEligibleParties = true
   215  				}
   216  				if hasNonZeroMetric && hasEligibleParties {
   217  					break
   218  				}
   219  			}
   220  			required = hasNonZeroMetric || (hasEligibleParties && (ds.DistributionStrategy == vegapb.DistributionStrategy_DISTRIBUTION_STRATEGY_RANK || ds.DistributionStrategy == vegapb.DistributionStrategy_DISTRIBUTION_STRATEGY_RANK_LOTTERY))
   221  			return required
   222  		} else {
   223  			tcs, pcs := e.marketActivityTracker.CalculateMetricForTeams(ctx, ds)
   224  			gs := events.NewTeamGameScoresEvent(ctx, int64(e.currentEpoch), e.hashDispatchStrategy(ds), e.timeService.GetTimeNow(), tcs, pcs)
   225  			e.broker.Send(gs)
   226  			required = len(tcs) > 0
   227  			return required
   228  		}
   229  	case vegapb.DispatchMetric_DISPATCH_METRIC_VALIDATOR_RANKING:
   230  		required = true
   231  		return required
   232  	}
   233  	required = false
   234  	return required
   235  }
   236  
   237  func (e *Engine) scaleAmountByTargetNotional(ds *vegapb.DispatchStrategy, amount *num.Uint) *num.Uint {
   238  	if ds == nil {
   239  		return amount
   240  	}
   241  	if ds.TargetNotionalVolume == nil {
   242  		return amount
   243  	}
   244  	actualVolumeInWindow := e.marketActivityTracker.GetNotionalVolumeForAsset(ds.AssetForMetric, ds.Markets, int(ds.WindowLength))
   245  	if actualVolumeInWindow.IsZero() {
   246  		return num.UintZero()
   247  	}
   248  	targetNotional := num.MustUintFromString(*ds.TargetNotionalVolume, 10)
   249  	ratio := num.MinD(actualVolumeInWindow.ToDecimal().Div(targetNotional.ToDecimal()), num.DecimalOne())
   250  	amt, _ := num.UintFromDecimal(ratio.Mul(amount.ToDecimal()))
   251  	return amt
   252  }
   253  
   254  func (e *Engine) distributeRecurringTransfers(ctx context.Context, newEpoch uint64) {
   255  	var (
   256  		transfersDone = []events.Event{}
   257  		doneIDs       = []string{}
   258  		tresps        = []*types.LedgerMovement{}
   259  		currentEpoch  = num.NewUint(newEpoch).ToDecimal()
   260  	)
   261  
   262  	// iterate over all transfers
   263  	for _, v := range e.recurringTransfers {
   264  		if v.StartEpoch > newEpoch {
   265  			// not started
   266  			continue
   267  		}
   268  
   269  		// if the transfer should have been ended and has not, end it now.
   270  		if v.EndEpoch != nil && *v.EndEpoch < e.currentEpoch {
   271  			v.Status = types.TransferStatusDone
   272  			transfersDone = append(transfersDone, events.NewRecurringTransferFundsEvent(ctx, v, e.getGameID(v)))
   273  			doneIDs = append(doneIDs, v.ID)
   274  			continue
   275  		}
   276  
   277  		if v.DispatchStrategy != nil && v.DispatchStrategy.TransferInterval != nil &&
   278  			((newEpoch-v.StartEpoch+1) < uint64(*v.DispatchStrategy.TransferInterval) ||
   279  				(newEpoch-v.StartEpoch+1)%uint64(*v.DispatchStrategy.TransferInterval) != 0) {
   280  			continue
   281  		}
   282  
   283  		var (
   284  			startEpoch  = num.NewUint(v.StartEpoch).ToDecimal()
   285  			startAmount = v.Amount.ToDecimal()
   286  			amount, _   = num.UintFromDecimal(
   287  				startAmount.Mul(
   288  					v.Factor.Pow(currentEpoch.Sub(startEpoch)),
   289  				),
   290  			)
   291  		)
   292  
   293  		// scale transfer amount as necessary
   294  		amount = e.scaleAmountByTargetNotional(v.DispatchStrategy, amount)
   295  
   296  		// check if the amount is still enough
   297  		// ensure asset exists
   298  		a, err := e.assets.Get(v.Asset)
   299  		if err != nil {
   300  			// this should not be possible, asset was validated at first when
   301  			// accepting the transfer
   302  			e.log.Panic("this should never happen", logging.Error(err))
   303  		}
   304  
   305  		if err = e.ensureMinimalTransferAmount(a, amount, v.FromAccountType, v.From, v.FromDerivedKey); err != nil {
   306  			v.Status = types.TransferStatusStopped
   307  			transfersDone = append(transfersDone,
   308  				events.NewRecurringTransferFundsEventWithReason(ctx, v, err.Error(), e.getGameID(v)))
   309  			doneIDs = append(doneIDs, v.ID)
   310  			continue
   311  		}
   312  
   313  		// NB: if no dispatch strategy is defined - the transfer is made to the account as defined in the transfer.
   314  		// If a dispatch strategy is defined but there are no relevant markets in scope or no fees in scope then no transfer is made!
   315  		var resps []*types.LedgerMovement
   316  		var r []*types.LedgerMovement
   317  		if v.DispatchStrategy == nil {
   318  			resps, err = e.processTransfer(
   319  				ctx, a, v.From, v.To, "", v.FromAccountType, v.ToAccountType, amount, v.Reference, v.ID, newEpoch,
   320  				v.FromDerivedKey, nil, // last is eventual oneoff, which this is not
   321  			)
   322  		} else {
   323  			// check if the amount + fees can be covered by the party issuing the transfer
   324  			if err = e.ensureFeeForTransferFunds(a, amount, v.From, v.FromAccountType, v.FromDerivedKey, v.To, v.ToAccountType); err == nil {
   325  				// NB: if the metric is market value we're going to transfer the bonus if any directly
   326  				// to the market account of the asset/reward type - this is similar to previous behaviour and
   327  				// different to how all other metric based rewards behave. The reason is that we need the context of the funder
   328  				// and this context is lost when the transfer has already gone through
   329  				if v.DispatchStrategy.Metric == vegapb.DispatchMetric_DISPATCH_METRIC_MARKET_VALUE {
   330  					marketProposersScore := e.marketActivityTracker.GetMarketsWithEligibleProposer(v.DispatchStrategy.AssetForMetric, v.DispatchStrategy.Markets, v.Asset, v.From, v.DispatchStrategy.EligibleKeys)
   331  					for _, fms := range marketProposersScore {
   332  						amt, _ := num.UintFromDecimal(amount.ToDecimal().Mul(fms.Score))
   333  						if amt.IsZero() {
   334  							continue
   335  						}
   336  						r, err = e.processTransfer(
   337  							ctx, a, v.From, v.To, fms.Market, v.FromAccountType, v.ToAccountType, amt, v.Reference, v.ID,
   338  							newEpoch, v.FromDerivedKey, nil, // last is eventual oneoff, which this is not
   339  						)
   340  						if err != nil {
   341  							e.log.Error("failed to process transfer",
   342  								logging.String("from", v.From),
   343  								logging.String("to", v.To),
   344  								logging.String("asset", v.Asset),
   345  								logging.String("market", fms.Market),
   346  								logging.String("from-account-type", v.FromAccountType.String()),
   347  								logging.String("to-account-type", v.ToAccountType.String()),
   348  								logging.String("amount", amt.String()),
   349  								logging.String("reference", v.Reference),
   350  								logging.Error(err))
   351  							break
   352  						}
   353  						if fms.Score.IsPositive() {
   354  							e.marketActivityTracker.MarkPaidProposer(v.DispatchStrategy.AssetForMetric, fms.Market, v.Asset, v.DispatchStrategy.Markets, v.From)
   355  						}
   356  						resps = append(resps, r...)
   357  					}
   358  				}
   359  				// for any other metric, we transfer the funds (full amount) to the reward account of the asset/reward_type/market=hash(dispatch_strategy)
   360  				if e.dispatchRequired(ctx, v.DispatchStrategy) {
   361  					p, _ := proto.Marshal(v.DispatchStrategy)
   362  					hash := hex.EncodeToString(crypto.Hash(p))
   363  					r, err = e.processTransfer(
   364  						ctx, a, v.From, v.To, hash, v.FromAccountType, v.ToAccountType, amount, v.Reference, v.ID, newEpoch,
   365  						v.FromDerivedKey, nil, // last is eventual oneoff, which this is not
   366  					)
   367  					if err != nil {
   368  						e.log.Error("failed to process transfer", logging.Error(err))
   369  					}
   370  					resps = append(resps, r...)
   371  				}
   372  			} else {
   373  				err = fmt.Errorf("could not pay the fee for transfer: %w", err)
   374  			}
   375  		}
   376  		if err != nil {
   377  			e.log.Info("transferred stopped", logging.Error(err))
   378  			v.Status = types.TransferStatusStopped
   379  			transfersDone = append(transfersDone,
   380  				events.NewRecurringTransferFundsEventWithReason(ctx, v, err.Error(), e.getGameID(v)))
   381  			doneIDs = append(doneIDs, v.ID)
   382  			continue
   383  		}
   384  
   385  		tresps = append(tresps, resps...)
   386  	}
   387  
   388  	// send events
   389  	if len(tresps) > 0 {
   390  		e.broker.Send(events.NewLedgerMovements(ctx, tresps))
   391  	}
   392  	if len(transfersDone) > 0 {
   393  		for _, id := range doneIDs {
   394  			e.deleteTransfer(id)
   395  		}
   396  		// also set the state change
   397  		e.broker.SendBatch(transfersDone)
   398  	}
   399  }
   400  
   401  func (e *Engine) deleteTransfer(ID string) {
   402  	index := -1
   403  	for i, rt := range e.recurringTransfers {
   404  		if rt.ID == ID {
   405  			index = i
   406  			e.unregisterDispatchStrategy(rt.DispatchStrategy)
   407  			break
   408  		}
   409  	}
   410  	if index >= 0 {
   411  		e.recurringTransfers = append(e.recurringTransfers[:index], e.recurringTransfers[index+1:]...)
   412  		delete(e.recurringTransfersMap, ID)
   413  	}
   414  }