code.vegaprotocol.io/vega@v0.79.0/core/banking/engine.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  	"errors"
    21  	"fmt"
    22  	"math/big"
    23  	"slices"
    24  	"sort"
    25  	"strings"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"code.vegaprotocol.io/vega/core/assets"
    30  	"code.vegaprotocol.io/vega/core/broker"
    31  	"code.vegaprotocol.io/vega/core/events"
    32  	"code.vegaprotocol.io/vega/core/types"
    33  	"code.vegaprotocol.io/vega/core/validators"
    34  	"code.vegaprotocol.io/vega/libs/num"
    35  	"code.vegaprotocol.io/vega/logging"
    36  	"code.vegaprotocol.io/vega/protos/vega"
    37  	proto "code.vegaprotocol.io/vega/protos/vega"
    38  
    39  	"github.com/emirpasic/gods/sets/treeset"
    40  	"golang.org/x/exp/maps"
    41  )
    42  
    43  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/banking Assets,Notary,Collateral,Witness,TimeService,EpochService,Topology,MarketActivityTracker,ERC20BridgeView,EthereumEventSource,Parties,StakeAccounting
    44  
    45  var (
    46  	ErrWrongAssetTypeUsedInBuiltinAssetChainEvent = errors.New("non builtin asset used for builtin asset chain event")
    47  	ErrWrongAssetTypeUsedInERC20ChainEvent        = errors.New("non ERC20 for ERC20 chain event")
    48  	ErrWrongAssetUsedForERC20Withdraw             = errors.New("non erc20 asset used for lock withdraw")
    49  	ErrInvalidWithdrawalState                     = errors.New("invalid withdrawal state")
    50  	ErrNotMatchingWithdrawalForReference          = errors.New("invalid reference for withdrawal chain event")
    51  	ErrWithdrawalNotReady                         = errors.New("withdrawal not ready")
    52  	ErrNotEnoughFundsToTransfer                   = errors.New("not enough funds to transfer")
    53  )
    54  
    55  type Assets interface {
    56  	Get(assetID string) (*assets.Asset, error)
    57  	Enable(ctx context.Context, assetID string) error
    58  	ApplyAssetUpdate(ctx context.Context, assetID string) error
    59  }
    60  
    61  // Notary ...
    62  
    63  type Notary interface {
    64  	StartAggregate(resID string, kind types.NodeSignatureKind, signature []byte)
    65  	IsSigned(ctx context.Context, id string, kind types.NodeSignatureKind) ([]types.NodeSignature, bool)
    66  	OfferSignatures(kind types.NodeSignatureKind, f func(resources string) []byte)
    67  }
    68  
    69  // Collateral engine.
    70  type Collateral interface {
    71  	Deposit(ctx context.Context, party, asset string, amount *num.Uint) (*types.LedgerMovement, error)
    72  	Withdraw(ctx context.Context, party, asset string, amount *num.Uint) (*types.LedgerMovement, error)
    73  	EnableAsset(ctx context.Context, asset types.Asset) error
    74  	GetPartyGeneralAccount(party, asset string) (*types.Account, error)
    75  	GetPartyLockedForStaking(party, asset string) (*types.Account, error)
    76  	GetPartyVestedRewardAccount(partyID, asset string) (*types.Account, error)
    77  	TransferFunds(ctx context.Context,
    78  		transfers []*types.Transfer,
    79  		accountTypes []types.AccountType,
    80  		references []string,
    81  		feeTransfers []*types.Transfer,
    82  		feeTransfersAccountTypes []types.AccountType,
    83  	) ([]*types.LedgerMovement, error)
    84  	GovernanceTransferFunds(ctx context.Context, transfers []*types.Transfer, accountTypes []types.AccountType, references []string) ([]*types.LedgerMovement, error)
    85  	PropagateAssetUpdate(ctx context.Context, asset types.Asset) error
    86  	GetSystemAccountBalance(asset, market string, accountType types.AccountType) (*num.Uint, error)
    87  }
    88  
    89  // Witness provide foreign chain resources validations.
    90  type Witness interface {
    91  	StartCheck(validators.Resource, func(interface{}, bool), time.Time) error
    92  	RestoreResource(validators.Resource, func(interface{}, bool)) error
    93  }
    94  
    95  // TimeService provide the time of the vega node using the tm time.
    96  type TimeService interface {
    97  	GetTimeNow() time.Time
    98  }
    99  
   100  // Epochervice ...
   101  type EpochService interface {
   102  	NotifyOnEpoch(f func(context.Context, types.Epoch), r func(context.Context, types.Epoch))
   103  }
   104  
   105  // Topology ...
   106  type Topology interface {
   107  	IsValidator() bool
   108  }
   109  
   110  // StakeAccounting ...
   111  type StakeAccounting interface {
   112  	AddEvent(ctx context.Context, evt *types.StakeLinking)
   113  }
   114  
   115  type MarketActivityTracker interface {
   116  	CalculateMetricForIndividuals(ctx context.Context, ds *vega.DispatchStrategy) []*types.PartyContributionScore
   117  	CalculateMetricForTeams(ctx context.Context, ds *vega.DispatchStrategy) ([]*types.PartyContributionScore, map[string][]*types.PartyContributionScore)
   118  	GetMarketsWithEligibleProposer(asset string, markets []string, payoutAsset string, funder string, eligibleKeys []string) []*types.MarketContributionScore
   119  	MarkPaidProposer(asset, market, payoutAsset string, marketsInScope []string, funder string)
   120  	MarketTrackedForAsset(market, asset string) bool
   121  	TeamStatsForMarkets(allMarketsForAssets, onlyTheseMarkets []string) map[string]map[string]*num.Uint
   122  	PublishGameMetric(ctx context.Context, dispatchStrategy []*vega.DispatchStrategy, now time.Time)
   123  	GameFinished(gameID string)
   124  	GetNotionalVolumeForAsset(asset string, markets []string, windowSize int) *num.Uint
   125  }
   126  
   127  type EthereumEventSource interface {
   128  	UpdateContractBlock(string, string, uint64)
   129  }
   130  
   131  type Parties interface {
   132  	CheckDerivedKeyOwnership(party types.PartyID, derivedKey string) bool
   133  }
   134  
   135  const (
   136  	pendingState uint32 = iota
   137  	okState
   138  	rejectedState
   139  )
   140  
   141  var defaultValidationDuration = 30 * 24 * time.Hour
   142  
   143  type dispatchStrategyCacheEntry struct {
   144  	ds       *proto.DispatchStrategy
   145  	refCount int
   146  }
   147  
   148  type Engine struct {
   149  	cfg         Config
   150  	log         *logging.Logger
   151  	timeService TimeService
   152  	broker      broker.Interface
   153  	col         Collateral
   154  	witness     Witness
   155  	notary      Notary
   156  	assets      Assets
   157  	top         Topology
   158  	parties     Parties
   159  
   160  	// assetActions tracks all the asset actions the engine must process on network
   161  	// tick.
   162  	assetActions map[string]*assetAction
   163  	// seenAssetActions keeps track of all asset action the node has seen.
   164  	seenAssetActions *treeset.Set
   165  
   166  	// primaryEthChainID stores the Ethereum Mainnet chain ID. It is used during the
   167  	// chain event deduplication phase, to ensure we correctly deduplicate
   168  	// chain events that have been seen before the introduce of the second bridge.
   169  	primaryEthChainID string
   170  	// lastSeenPrimaryEthBlock holds the block height of the latest ERC20 chain
   171  	// event, from the primary chain, processed by the engine.
   172  	lastSeenPrimaryEthBlock uint64
   173  	primaryBridgeState      *bridgeState
   174  	primaryBridgeView       ERC20BridgeView
   175  
   176  	// lastSeenSecondaryEthBlock holds the block height of the latest ERC20 chain
   177  	// event, from the secondary chain, processed by the engine.
   178  	lastSeenSecondaryEthBlock uint64
   179  	secondaryEthChainID       string
   180  	secondaryBridgeState      *bridgeState
   181  	secondaryBridgeView       ERC20BridgeView
   182  
   183  	// map from chain-id -> collateral contract address
   184  	bridgeAddresses map[string]string
   185  
   186  	ethEventSource EthereumEventSource
   187  
   188  	withdrawals   map[string]withdrawalRef
   189  	withdrawalCnt *big.Int
   190  	deposits      map[string]*types.Deposit
   191  
   192  	currentEpoch uint64
   193  	bss          *bankingSnapshotState
   194  
   195  	marketActivityTracker MarketActivityTracker
   196  
   197  	// transfer fee related stuff
   198  	scheduledTransfers         map[int64][]scheduledTransfer
   199  	transferFeeFactor          num.Decimal
   200  	minTransferQuantumMultiple num.Decimal
   201  	maxQuantumAmount           num.Decimal
   202  
   203  	feeDiscountDecayFraction        num.Decimal
   204  	feeDiscountMinimumTrackedAmount num.Decimal
   205  
   206  	// assetID -> partyID -> fee discount
   207  	pendingPerAssetAndPartyFeeDiscountUpdates map[string]map[string]*num.Uint
   208  	feeDiscountPerPartyAndAsset               map[partyAssetKey]*num.Uint
   209  
   210  	scheduledGovernanceTransfers    map[int64][]*types.GovernanceTransfer
   211  	recurringGovernanceTransfers    []*types.GovernanceTransfer
   212  	recurringGovernanceTransfersMap map[string]*types.GovernanceTransfer
   213  
   214  	// a hash of a dispatch strategy to the dispatch strategy details
   215  	hashToStrategy map[string]*dispatchStrategyCacheEntry
   216  
   217  	// recurring transfers in the order they were created
   218  	recurringTransfers []*types.RecurringTransfer
   219  	// transfer id to recurringTransfers
   220  	recurringTransfersMap map[string]*types.RecurringTransfer
   221  
   222  	minWithdrawQuantumMultiple num.Decimal
   223  
   224  	maxGovTransferQunatumMultiplier num.Decimal
   225  	maxGovTransferFraction          num.Decimal
   226  
   227  	metricUpdateFrequency time.Duration
   228  	nextMetricUpdate      time.Time
   229  
   230  	// transient cache used to market a dispatch strategy as checked for eligibility for this round so we don't check again.
   231  	dispatchRequiredCache map[string]bool
   232  
   233  	stakingAsset    string
   234  	stakeAccounting StakeAccounting
   235  }
   236  
   237  type withdrawalRef struct {
   238  	w   *types.Withdrawal
   239  	ref *big.Int
   240  }
   241  
   242  func New(log *logging.Logger,
   243  	cfg Config,
   244  	col Collateral,
   245  	witness Witness,
   246  	tsvc TimeService,
   247  	assets Assets,
   248  	notary Notary,
   249  	broker broker.Interface,
   250  	top Topology,
   251  	marketActivityTracker MarketActivityTracker,
   252  	primaryBridgeView ERC20BridgeView,
   253  	secondaryBridgeView ERC20BridgeView,
   254  	ethEventSource EthereumEventSource,
   255  	parties Parties,
   256  	stakeAccounting StakeAccounting,
   257  ) (e *Engine) {
   258  	log = log.Named(namedLogger)
   259  	log.SetLevel(cfg.Level.Get())
   260  
   261  	return &Engine{
   262  		cfg:                             cfg,
   263  		log:                             log,
   264  		timeService:                     tsvc,
   265  		broker:                          broker,
   266  		col:                             col,
   267  		witness:                         witness,
   268  		assets:                          assets,
   269  		notary:                          notary,
   270  		top:                             top,
   271  		ethEventSource:                  ethEventSource,
   272  		parties:                         parties,
   273  		assetActions:                    map[string]*assetAction{},
   274  		seenAssetActions:                treeset.NewWithStringComparator(),
   275  		withdrawals:                     map[string]withdrawalRef{},
   276  		deposits:                        map[string]*types.Deposit{},
   277  		withdrawalCnt:                   big.NewInt(0),
   278  		bss:                             &bankingSnapshotState{},
   279  		scheduledTransfers:              map[int64][]scheduledTransfer{},
   280  		recurringTransfers:              []*types.RecurringTransfer{},
   281  		recurringTransfersMap:           map[string]*types.RecurringTransfer{},
   282  		scheduledGovernanceTransfers:    map[int64][]*types.GovernanceTransfer{},
   283  		recurringGovernanceTransfers:    []*types.GovernanceTransfer{},
   284  		recurringGovernanceTransfersMap: map[string]*types.GovernanceTransfer{},
   285  		transferFeeFactor:               num.DecimalZero(),
   286  		minTransferQuantumMultiple:      num.DecimalZero(),
   287  		minWithdrawQuantumMultiple:      num.DecimalZero(),
   288  		marketActivityTracker:           marketActivityTracker,
   289  		nextMetricUpdate:                time.Time{},
   290  		hashToStrategy:                  map[string]*dispatchStrategyCacheEntry{},
   291  		primaryBridgeState: &bridgeState{
   292  			active: true,
   293  		},
   294  		secondaryBridgeState: &bridgeState{
   295  			active: true,
   296  		},
   297  		bridgeAddresses:                           map[string]string{},
   298  		feeDiscountPerPartyAndAsset:               map[partyAssetKey]*num.Uint{},
   299  		pendingPerAssetAndPartyFeeDiscountUpdates: map[string]map[string]*num.Uint{},
   300  		primaryBridgeView:                         primaryBridgeView,
   301  		secondaryBridgeView:                       secondaryBridgeView,
   302  		dispatchRequiredCache:                     map[string]bool{},
   303  		stakeAccounting:                           stakeAccounting,
   304  	}
   305  }
   306  
   307  func (e *Engine) OnStakingAsset(_ context.Context, a string) error {
   308  	e.stakingAsset = a
   309  	return nil
   310  }
   311  
   312  func (e *Engine) OnMaxFractionChanged(ctx context.Context, f num.Decimal) error {
   313  	e.maxGovTransferFraction = f
   314  	return nil
   315  }
   316  
   317  func (e *Engine) OnMaxAmountChanged(ctx context.Context, f num.Decimal) error {
   318  	e.maxGovTransferQunatumMultiplier = f
   319  	return nil
   320  }
   321  
   322  func (e *Engine) OnMinWithdrawQuantumMultiple(ctx context.Context, f num.Decimal) error {
   323  	e.minWithdrawQuantumMultiple = f
   324  	return nil
   325  }
   326  
   327  func (e *Engine) OnPrimaryEthChainIDUpdated(chainID, collateralAddress string) {
   328  	e.primaryEthChainID = chainID
   329  	e.bridgeAddresses[chainID] = collateralAddress
   330  }
   331  
   332  func (e *Engine) OnSecondaryEthChainIDUpdated(chainID, collateralAddress string) {
   333  	e.secondaryEthChainID = chainID
   334  	e.bridgeAddresses[chainID] = collateralAddress
   335  }
   336  
   337  // ReloadConf updates the internal configuration.
   338  func (e *Engine) ReloadConf(cfg Config) {
   339  	e.log.Info("reloading configuration")
   340  	if e.log.GetLevel() != cfg.Level.Get() {
   341  		e.log.Info("updating log level",
   342  			logging.String("old", e.log.GetLevel().String()),
   343  			logging.String("new", cfg.Level.String()),
   344  		)
   345  		e.log.SetLevel(cfg.Level.Get())
   346  	}
   347  
   348  	e.cfg = cfg
   349  }
   350  
   351  func (e *Engine) OnBlockEnd(ctx context.Context, now time.Time) {
   352  	if !now.Before(e.nextMetricUpdate) {
   353  		e.publishMetricData(ctx, now)
   354  		e.nextMetricUpdate = now.Add(e.metricUpdateFrequency)
   355  	}
   356  }
   357  
   358  // publishMetricData requests the market activity tracker to publish and event
   359  // for each game with the current metric data for each party.
   360  func (e *Engine) publishMetricData(ctx context.Context, now time.Time) {
   361  	hashes := make([]string, 0, len(e.hashToStrategy))
   362  	for hash := range e.hashToStrategy {
   363  		hashes = append(hashes, hash)
   364  	}
   365  	sort.Strings(hashes)
   366  	dss := make([]*vega.DispatchStrategy, 0, len(hashes))
   367  	for _, hash := range hashes {
   368  		dss = append(dss, e.hashToStrategy[hash].ds)
   369  	}
   370  	e.marketActivityTracker.PublishGameMetric(ctx, dss, now)
   371  }
   372  
   373  func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) {
   374  	switch ep.Action {
   375  	case proto.EpochAction_EPOCH_ACTION_START:
   376  		e.currentEpoch = ep.Seq
   377  		e.cleanupStaleDispatchStrategies()
   378  	case proto.EpochAction_EPOCH_ACTION_END:
   379  		e.distributeRecurringTransfers(ctx, e.currentEpoch)
   380  		e.distributeRecurringGovernanceTransfers(ctx)
   381  		e.applyPendingFeeDiscountsUpdates(ctx)
   382  		e.sendTeamsStats(ctx, ep.Seq)
   383  		e.dispatchRequiredCache = map[string]bool{}
   384  		// as the metrics are going to be published here, we want to progress the next update.
   385  		e.nextMetricUpdate = e.timeService.GetTimeNow().Add(e.metricUpdateFrequency)
   386  	default:
   387  		e.log.Panic("epoch action should never be UNSPECIFIED", logging.String("epoch", ep.String()))
   388  	}
   389  }
   390  
   391  func (e *Engine) OnTick(ctx context.Context, now time.Time) {
   392  	e.processAssetActions(ctx, now)
   393  
   394  	e.notary.OfferSignatures(types.NodeSignatureKindAssetWithdrawal, e.offerERC20NotarySignatures)
   395  
   396  	// then process all scheduledTransfers
   397  	if err := e.distributeScheduledTransfers(ctx, now); err != nil {
   398  		e.log.Error("could not process scheduled transfers",
   399  			logging.Error(err),
   400  		)
   401  	}
   402  
   403  	// process governance transfers
   404  	e.distributeScheduledGovernanceTransfers(ctx, now)
   405  }
   406  
   407  func (e *Engine) processAssetActions(ctx context.Context, now time.Time) {
   408  	sortedAssetActionKeys := maps.Keys(e.assetActions)
   409  	sort.Strings(sortedAssetActionKeys)
   410  
   411  	for _, key := range sortedAssetActionKeys {
   412  		action := e.assetActions[key]
   413  
   414  		switch action.state.Load() {
   415  		case pendingState:
   416  			// The verification of the action has not been completed yet, so
   417  			// we skip it until it is.
   418  			continue
   419  		case okState:
   420  			if err := e.deduplicateAssetAction(ctx, action); err != nil {
   421  				e.log.Warn("an error occurred during asset action deduplication",
   422  					logging.Error(err),
   423  					logging.String("action", action.String()),
   424  					logging.String("tx-hash", action.txHash),
   425  					logging.String("chain-id", action.chainID))
   426  				break
   427  			}
   428  
   429  			if err := e.finalizeAction(ctx, action, now); err != nil {
   430  				e.log.Error("unable to finalize action",
   431  					logging.String("action", action.String()),
   432  					logging.Error(err))
   433  			}
   434  		case rejectedState:
   435  			e.log.Error("network rejected banking action",
   436  				logging.String("action", action.String()))
   437  		}
   438  
   439  		delete(e.assetActions, key)
   440  	}
   441  }
   442  
   443  func (e *Engine) onCheckDone(i interface{}, valid bool) {
   444  	aa, ok := i.(*assetAction)
   445  	if !ok {
   446  		return
   447  	}
   448  
   449  	newState := rejectedState
   450  	if valid {
   451  		newState = okState
   452  	}
   453  	aa.state.Store(newState)
   454  }
   455  
   456  func (e *Engine) getWithdrawalFromRef(ref *big.Int) (*types.Withdrawal, error) {
   457  	// sort withdraws to check deterministically
   458  	withdrawalsK := make([]string, 0, len(e.withdrawals))
   459  	for k := range e.withdrawals {
   460  		withdrawalsK = append(withdrawalsK, k)
   461  	}
   462  	sort.Strings(withdrawalsK)
   463  
   464  	for _, k := range withdrawalsK {
   465  		v := e.withdrawals[k]
   466  		if v.ref.Cmp(ref) == 0 {
   467  			return v.w, nil
   468  		}
   469  	}
   470  
   471  	return nil, ErrNotMatchingWithdrawalForReference
   472  }
   473  
   474  func (e *Engine) dedupAction(ctx context.Context, aa *assetAction) error {
   475  	switch {
   476  	case aa.IsBuiltinAssetDeposit():
   477  		dep := e.deposits[aa.id]
   478  		return e.dedupDeposit(ctx, dep)
   479  	case aa.IsERC20Deposit():
   480  		dep := e.deposits[aa.id]
   481  		return e.dedupDeposit(ctx, dep)
   482  	}
   483  	// the bridge stop/resume actions don't send events, and the asset listing/updates share the same
   484  	// underlying asset ID, so they don't result in duplicates. Only deposits need to be handled.
   485  	e.log.Warn("unable to deduplicate asset action",
   486  		logging.String("action", aa.String()))
   487  	return nil
   488  }
   489  
   490  func (e *Engine) finalizeAction(ctx context.Context, aa *assetAction, now time.Time) error {
   491  	// tell the evt forwarder tracker about this block height
   492  	if addr, ok := e.bridgeAddresses[aa.chainID]; ok {
   493  		e.ethEventSource.UpdateContractBlock(addr, aa.chainID, aa.blockHeight)
   494  	}
   495  
   496  	switch {
   497  	case aa.IsBuiltinAssetDeposit():
   498  		dep := e.deposits[aa.id]
   499  		return e.finalizeDeposit(ctx, dep, now)
   500  	case aa.IsERC20Deposit():
   501  		dep := e.deposits[aa.id]
   502  		return e.finalizeDeposit(ctx, dep, now)
   503  	case aa.IsERC20AssetList():
   504  		return e.finalizeAssetList(ctx, aa.erc20AL.VegaAssetID)
   505  	case aa.IsERC20AssetLimitsUpdated():
   506  		return e.finalizeAssetLimitsUpdated(ctx, aa.erc20AssetLimitsUpdated.VegaAssetID)
   507  	case aa.IsERC20BridgeStopped():
   508  		b, err := e.bridgeStateForChainID(aa.chainID)
   509  		if err != nil {
   510  			return err
   511  		}
   512  		b.NewBridgeStopped(aa.blockHeight, aa.logIndex)
   513  		return nil
   514  	case aa.IsERC20BridgeResumed():
   515  		b, err := e.bridgeStateForChainID(aa.chainID)
   516  		if err != nil {
   517  			return err
   518  		}
   519  		b.NewBridgeResumed(aa.blockHeight, aa.logIndex)
   520  		return nil
   521  	default:
   522  		return ErrUnknownAssetAction
   523  	}
   524  }
   525  
   526  func (e *Engine) finalizeAssetList(ctx context.Context, assetID string) error {
   527  	asset, err := e.assets.Get(assetID)
   528  	if err != nil {
   529  		e.log.Error("invalid asset id used to finalise asset list",
   530  			logging.Error(err),
   531  			logging.AssetID(assetID))
   532  		return nil
   533  	}
   534  	if err := e.assets.Enable(ctx, assetID); err != nil {
   535  		e.log.Error("unable to enable asset",
   536  			logging.Error(err),
   537  			logging.AssetID(assetID))
   538  		return err
   539  	}
   540  	return e.col.EnableAsset(ctx, *asset.ToAssetType())
   541  }
   542  
   543  func (e *Engine) finalizeAssetLimitsUpdated(ctx context.Context, assetID string) error {
   544  	asset, err := e.assets.Get(assetID)
   545  	if err != nil {
   546  		e.log.Error("invalid asset id used to finalise asset list",
   547  			logging.Error(err),
   548  			logging.AssetID(assetID))
   549  		return nil
   550  	}
   551  	if err := e.assets.ApplyAssetUpdate(ctx, assetID); err != nil {
   552  		e.log.Error("couldn't apply asset update",
   553  			logging.Error(err),
   554  			logging.AssetID(assetID))
   555  		return err
   556  	}
   557  	return e.col.PropagateAssetUpdate(ctx, *asset.ToAssetType())
   558  }
   559  
   560  func (e *Engine) finalizeDeposit(ctx context.Context, d *types.Deposit, now time.Time) error {
   561  	defer func() {
   562  		e.broker.Send(events.NewDepositEvent(ctx, *d))
   563  		// whatever happens, the deposit is in its final state (cancelled or finalized)
   564  		delete(e.deposits, d.ID)
   565  	}()
   566  	res, err := e.col.Deposit(ctx, d.PartyID, d.Asset, d.Amount)
   567  	if err != nil {
   568  		d.Status = types.DepositStatusCancelled
   569  		return err
   570  	}
   571  
   572  	d.Status = types.DepositStatusFinalized
   573  	d.CreditDate = now.UnixNano()
   574  	e.broker.Send(events.NewLedgerMovements(ctx, []*types.LedgerMovement{res}))
   575  	return nil
   576  }
   577  
   578  func (e *Engine) dedupDeposit(ctx context.Context, d *types.Deposit) error {
   579  	d.Status = types.DepositStatusDuplicateRejected
   580  	e.broker.Send(events.NewDepositEvent(ctx, *d))
   581  	return nil
   582  }
   583  
   584  func (e *Engine) finalizeWithdraw(ctx context.Context, w *types.Withdrawal) error {
   585  	// always send the withdrawal event, don't delete it from the map because we
   586  	// may still receive events
   587  	defer func() {
   588  		e.broker.Send(events.NewWithdrawalEvent(ctx, *w))
   589  	}()
   590  
   591  	res, err := e.col.Withdraw(ctx, w.PartyID, w.Asset, w.Amount.Clone())
   592  	if err != nil {
   593  		w.Status = types.WithdrawalStatusRejected
   594  		return err
   595  	}
   596  
   597  	w.Status = types.WithdrawalStatusFinalized
   598  	e.broker.Send(events.NewLedgerMovements(ctx, []*types.LedgerMovement{res}))
   599  	return nil
   600  }
   601  
   602  func (e *Engine) newWithdrawal(
   603  	id, partyID, asset string,
   604  	amount *num.Uint,
   605  	wext *types.WithdrawExt,
   606  ) (w *types.Withdrawal, ref *big.Int) {
   607  	partyID = strings.TrimPrefix(partyID, "0x")
   608  	asset = strings.TrimPrefix(asset, "0x")
   609  	now := e.timeService.GetTimeNow()
   610  
   611  	// reference needs to be an int, deterministic for the contracts
   612  	ref = big.NewInt(0).Add(e.withdrawalCnt, big.NewInt(now.Unix()))
   613  	e.withdrawalCnt.Add(e.withdrawalCnt, big.NewInt(1))
   614  	w = &types.Withdrawal{
   615  		ID:           id,
   616  		Status:       types.WithdrawalStatusOpen,
   617  		PartyID:      partyID,
   618  		Asset:        asset,
   619  		Amount:       amount,
   620  		Ext:          wext,
   621  		CreationDate: now.UnixNano(),
   622  		Ref:          ref.String(),
   623  	}
   624  	return
   625  }
   626  
   627  func (e *Engine) newDeposit(
   628  	id, partyID, asset string,
   629  	amount *num.Uint,
   630  	txHash string,
   631  ) *types.Deposit {
   632  	partyID = strings.TrimPrefix(partyID, "0x")
   633  	asset = strings.TrimPrefix(asset, "0x")
   634  	return &types.Deposit{
   635  		ID:           id,
   636  		Status:       types.DepositStatusOpen,
   637  		PartyID:      partyID,
   638  		Asset:        asset,
   639  		Amount:       amount,
   640  		CreationDate: e.timeService.GetTimeNow().UnixNano(),
   641  		TxHash:       txHash,
   642  	}
   643  }
   644  
   645  func (e *Engine) GetDispatchStrategy(hash string) *proto.DispatchStrategy {
   646  	ds, ok := e.hashToStrategy[hash]
   647  	if !ok {
   648  		e.log.Warn("could not find dispatch strategy in banking engine", logging.String("hash", hash))
   649  		return nil
   650  	}
   651  
   652  	if ds.refCount == 0 {
   653  		return nil
   654  	}
   655  
   656  	return ds.ds
   657  }
   658  
   659  // sendTeamsStats sends the teams statistics, which only account for games.
   660  // This is located here not because this is where it should be, but because
   661  // we don't know where to put it, as we need to have access to the dispatch
   662  // strategy.
   663  func (e *Engine) sendTeamsStats(ctx context.Context, seq uint64) {
   664  	onlyTheseMarkets := map[string]interface{}{}
   665  	allMarketsForAssets := map[string]interface{}{}
   666  	for _, ds := range e.hashToStrategy {
   667  		if ds.ds.EntityScope == proto.EntityScope_ENTITY_SCOPE_TEAMS {
   668  			if len(ds.ds.Markets) != 0 {
   669  				// If there is no markets specified, then we need gather data from
   670  				// all markets tied to this asset.
   671  				allMarketsForAssets[ds.ds.AssetForMetric] = nil
   672  			} else {
   673  				for _, market := range ds.ds.Markets {
   674  					onlyTheseMarkets[market] = nil
   675  				}
   676  			}
   677  		}
   678  	}
   679  
   680  	if len(allMarketsForAssets) == 0 && len(onlyTheseMarkets) == 0 {
   681  		return
   682  	}
   683  
   684  	allMarketsForAssetsS := maps.Keys(allMarketsForAssets)
   685  	slices.Sort(allMarketsForAssetsS)
   686  	onlyTheseMarketsS := maps.Keys(onlyTheseMarkets)
   687  	slices.Sort(onlyTheseMarketsS)
   688  
   689  	teamsStats := e.marketActivityTracker.TeamStatsForMarkets(allMarketsForAssetsS, onlyTheseMarketsS)
   690  
   691  	if len(teamsStats) > 0 {
   692  		e.broker.Send(events.NewTeamsStatsUpdatedEvent(ctx, seq, teamsStats))
   693  	}
   694  }
   695  
   696  func (e *Engine) bridgeViewForChainID(chainID string) (ERC20BridgeView, error) {
   697  	switch chainID {
   698  	case e.primaryEthChainID:
   699  		return e.primaryBridgeView, nil
   700  	case e.secondaryEthChainID:
   701  		return e.secondaryBridgeView, nil
   702  	default:
   703  		return nil, fmt.Errorf("chain id %q is not supported", chainID)
   704  	}
   705  }
   706  
   707  func (e *Engine) bridgeStateForChainID(chainID string) (*bridgeState, error) {
   708  	switch chainID {
   709  	case e.primaryEthChainID:
   710  		return e.primaryBridgeState, nil
   711  	case e.secondaryEthChainID:
   712  		return e.secondaryBridgeState, nil
   713  	default:
   714  		return nil, fmt.Errorf("chain id %q is not supported", chainID)
   715  	}
   716  }
   717  
   718  func newPendingState() *atomic.Uint32 {
   719  	state := &atomic.Uint32{}
   720  	state.Store(pendingState)
   721  	return state
   722  }