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 }