code.vegaprotocol.io/vega@v0.79.0/core/rewards/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 rewards
    17  
    18  import (
    19  	"context"
    20  	"sort"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/events"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/core/vesting"
    26  	"code.vegaprotocol.io/vega/libs/num"
    27  	"code.vegaprotocol.io/vega/logging"
    28  	"code.vegaprotocol.io/vega/protos/vega"
    29  	proto "code.vegaprotocol.io/vega/protos/vega"
    30  )
    31  
    32  var (
    33  	decimal1, _        = num.DecimalFromString("1")
    34  	rewardAccountTypes = []types.AccountType{types.AccountTypeGlobalReward, types.AccountTypeFeesInfrastructure, types.AccountTypeMakerReceivedFeeReward, types.AccountTypeMakerPaidFeeReward, types.AccountTypeLPFeeReward, types.AccountTypeMarketProposerReward, types.AccountTypeAverageNotionalReward, types.AccountTypeRelativeReturnReward, types.AccountTypeReturnVolatilityReward, types.AccountTypeValidatorRankingReward, types.AccountTypeRealisedReturnReward, types.AccountTypeEligibleEntitiesReward}
    35  )
    36  
    37  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/rewards MarketActivityTracker,Delegation,TimeService,Topology,Transfers,Teams,Vesting,ActivityStreak
    38  
    39  // Broker for sending events.
    40  type Broker interface {
    41  	Send(event events.Event)
    42  	SendBatch(events []events.Event)
    43  }
    44  
    45  type MarketActivityTracker interface {
    46  	GetAllMarketIDs() []string
    47  	GetProposer(market string) string
    48  	CalculateMetricForIndividuals(ctx context.Context, ds *vega.DispatchStrategy) []*types.PartyContributionScore
    49  	CalculateMetricForTeams(ctx context.Context, ds *vega.DispatchStrategy) ([]*types.PartyContributionScore, map[string][]*types.PartyContributionScore)
    50  	GetLastEpochTakeFees(asset string, market []string, epochs int32) map[string]*num.Uint
    51  }
    52  
    53  // EpochEngine notifies the reward engine at the end of an epoch.
    54  type EpochEngine interface {
    55  	NotifyOnEpoch(f func(context.Context, types.Epoch), r func(context.Context, types.Epoch))
    56  }
    57  
    58  // Delegation engine for getting validation data.
    59  type Delegation interface {
    60  	ProcessEpochDelegations(ctx context.Context, epoch types.Epoch) []*types.ValidatorData
    61  	GetValidatorData() []*types.ValidatorData
    62  }
    63  
    64  // Collateral engine provides access to account data and transferring rewards.
    65  type Collateral interface {
    66  	GetAccountByID(id string) (*types.Account, error)
    67  	TransferRewards(ctx context.Context, rewardAccountID string, transfers []*types.Transfer, rewardType types.AccountType) ([]*types.LedgerMovement, error)
    68  	GetRewardAccountsByType(rewardAcccountType types.AccountType) []*types.Account
    69  	GetAssetQuantum(asset string) (num.Decimal, error)
    70  }
    71  
    72  // TimeService notifies the reward engine on time updates.
    73  type TimeService interface {
    74  	GetTimeNow() time.Time
    75  }
    76  
    77  type Topology interface {
    78  	GetRewardsScores(ctx context.Context, epochSeq string, delegationState []*types.ValidatorData, stakeScoreParams types.StakeScoreParams) (*types.ScoreData, *types.ScoreData)
    79  	RecalcValidatorSet(ctx context.Context, epochSeq string, delegationState []*types.ValidatorData, stakeScoreParams types.StakeScoreParams) []*types.PartyContributionScore
    80  }
    81  
    82  type Transfers interface {
    83  	GetDispatchStrategy(string) *proto.DispatchStrategy
    84  }
    85  
    86  type Teams interface {
    87  	GetTeamMembers([]string) map[string][]string
    88  	GetAllPartiesInTeams() []string
    89  }
    90  
    91  type Vesting interface {
    92  	AddReward(ctx context.Context, party, asset string, amount *num.Uint, lockedForEpochs uint64)
    93  	GetSingleAndSummedRewardBonusMultipliers(party string) (vesting.MultiplierAndQuantBalance, vesting.MultiplierAndQuantBalance)
    94  }
    95  
    96  type ActivityStreak interface {
    97  	GetRewardsDistributionMultiplier(party string) num.Decimal
    98  }
    99  
   100  // Engine is the reward engine handling reward payouts.
   101  type Engine struct {
   102  	log                   *logging.Logger
   103  	config                Config
   104  	timeService           TimeService
   105  	broker                Broker
   106  	topology              Topology
   107  	delegation            Delegation
   108  	collateral            Collateral
   109  	marketActivityTracker MarketActivityTracker
   110  	global                *globalRewardParams
   111  	newEpochStarted       bool // flag to signal new epoch so we can update the voting power at the end of the block
   112  	epochSeq              string
   113  	ersatzRewardFactor    num.Decimal
   114  	vesting               Vesting
   115  	transfers             Transfers
   116  	activityStreak        ActivityStreak
   117  }
   118  
   119  type globalRewardParams struct {
   120  	minValStakeD            num.Decimal
   121  	minValStakeUInt         *num.Uint
   122  	optimalStakeMultiplier  num.Decimal
   123  	compLevel               num.Decimal
   124  	minValidators           num.Decimal
   125  	maxPayoutPerParticipant *num.Uint
   126  	delegatorShare          num.Decimal
   127  	asset                   string
   128  }
   129  
   130  type payout struct {
   131  	rewardType       types.AccountType
   132  	fromAccount      string
   133  	asset            string
   134  	partyToAmount    map[string]*num.Uint
   135  	totalReward      *num.Uint
   136  	epochSeq         string
   137  	timestamp        int64
   138  	gameID           *string
   139  	lockedForEpochs  uint64
   140  	lockedUntilEpoch string
   141  }
   142  
   143  // New instantiate a new rewards engine.
   144  func New(log *logging.Logger, config Config, broker Broker, delegation Delegation, epochEngine EpochEngine, collateral Collateral, ts TimeService, marketActivityTracker MarketActivityTracker, topology Topology, vesting Vesting, transfers Transfers, activityStreak ActivityStreak) *Engine {
   145  	log = log.Named(namedLogger)
   146  	log.SetLevel(config.Level.Get())
   147  	e := &Engine{
   148  		config:                config,
   149  		log:                   log.Named(namedLogger),
   150  		timeService:           ts,
   151  		broker:                broker,
   152  		delegation:            delegation,
   153  		collateral:            collateral,
   154  		global:                &globalRewardParams{},
   155  		newEpochStarted:       false,
   156  		marketActivityTracker: marketActivityTracker,
   157  		topology:              topology,
   158  		vesting:               vesting,
   159  		transfers:             transfers,
   160  		activityStreak:        activityStreak,
   161  	}
   162  
   163  	// register for epoch end notifications
   164  	epochEngine.NotifyOnEpoch(e.OnEpochEvent, e.OnEpochRestore)
   165  
   166  	return e
   167  }
   168  
   169  func (e *Engine) UpdateAssetForStakingAndDelegation(ctx context.Context, asset string) error {
   170  	e.global.asset = asset
   171  	return nil
   172  }
   173  
   174  // UpdateErsatzRewardFactor updates the ratio of staking and delegation reward that goes to ersatz validators.
   175  func (e *Engine) UpdateErsatzRewardFactor(ctx context.Context, ersatzRewardFactor num.Decimal) error {
   176  	e.ersatzRewardFactor = ersatzRewardFactor
   177  	return nil
   178  }
   179  
   180  // UpdateMinimumValidatorStakeForStakingRewardScheme updaates the value of minimum validator stake for being considered for rewards.
   181  func (e *Engine) UpdateMinimumValidatorStakeForStakingRewardScheme(ctx context.Context, minValStake num.Decimal) error {
   182  	e.global.minValStakeD = minValStake
   183  	e.global.minValStakeUInt, _ = num.UintFromDecimal(minValStake)
   184  	return nil
   185  }
   186  
   187  // UpdateOptimalStakeMultiplierStakingRewardScheme updaates the value of optimal stake multiplier.
   188  func (e *Engine) UpdateOptimalStakeMultiplierStakingRewardScheme(ctx context.Context, optimalStakeMultiplier num.Decimal) error {
   189  	e.global.optimalStakeMultiplier = optimalStakeMultiplier
   190  	return nil
   191  }
   192  
   193  // UpdateCompetitionLevelForStakingRewardScheme is called when the competition level has changed.
   194  func (e *Engine) UpdateCompetitionLevelForStakingRewardScheme(ctx context.Context, compLevel num.Decimal) error {
   195  	e.global.compLevel = compLevel
   196  	return nil
   197  }
   198  
   199  // UpdateMinValidatorsStakingRewardScheme is called when the the network parameter for min validator has changed.
   200  func (e *Engine) UpdateMinValidatorsStakingRewardScheme(ctx context.Context, minValidators int64) error {
   201  	e.global.minValidators = num.DecimalFromInt64(minValidators)
   202  	return nil
   203  }
   204  
   205  // UpdateMaxPayoutPerParticipantForStakingRewardScheme is a callback for changes in the network param for max payout per participant.
   206  func (e *Engine) UpdateMaxPayoutPerParticipantForStakingRewardScheme(ctx context.Context, maxPayoutPerParticipant num.Decimal) error {
   207  	e.global.maxPayoutPerParticipant, _ = num.UintFromDecimal(maxPayoutPerParticipant)
   208  	return nil
   209  }
   210  
   211  // UpdateDelegatorShareForStakingRewardScheme is a callback for changes in the network param for delegator share.
   212  func (e *Engine) UpdateDelegatorShareForStakingRewardScheme(ctx context.Context, delegatorShare num.Decimal) error {
   213  	e.global.delegatorShare = delegatorShare
   214  	return nil
   215  }
   216  
   217  // OnEpochEvent calculates the reward amounts parties get for available reward schemes.
   218  func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) {
   219  	e.log.Debug("OnEpochEvent")
   220  
   221  	// on new epoch update the epoch seq and update the epoch started flag
   222  	if epoch.Action == proto.EpochAction_EPOCH_ACTION_START {
   223  		e.epochSeq = num.NewUint(epoch.Seq).String()
   224  		e.newEpochStarted = true
   225  		return
   226  	}
   227  
   228  	// we're at the end of the epoch - process rewards
   229  	e.calculateRewardPayouts(ctx, epoch)
   230  }
   231  
   232  func (e *Engine) OnEpochRestore(ctx context.Context, epoch types.Epoch) {
   233  	e.log.Debug("epoch restoration notification received", logging.String("epoch", epoch.String()))
   234  	e.epochSeq = num.NewUint(epoch.Seq).String()
   235  	e.newEpochStarted = true
   236  }
   237  
   238  // splitDelegationByStatus splits the delegation data for an epoch into tendermint and ersatz validator sets.
   239  func (e *Engine) splitDelegationByStatus(delegation []*types.ValidatorData, tmScores *types.ScoreData, ezScores *types.ScoreData) ([]*types.ValidatorData, []*types.ValidatorData) {
   240  	tm := make([]*types.ValidatorData, 0, len(tmScores.NodeIDSlice))
   241  	ez := make([]*types.ValidatorData, 0, len(ezScores.NodeIDSlice))
   242  	for _, vd := range delegation {
   243  		if _, ok := tmScores.NormalisedScores[vd.NodeID]; ok {
   244  			tm = append(tm, vd)
   245  		}
   246  		if _, ok := ezScores.NormalisedScores[vd.NodeID]; ok {
   247  			ez = append(ez, vd)
   248  		}
   249  	}
   250  	return tm, ez
   251  }
   252  
   253  func calcTotalDelegation(d []*types.ValidatorData) num.Decimal {
   254  	total := num.UintZero()
   255  	for _, vd := range d {
   256  		total.AddSum(num.Sum(vd.SelfStake, vd.StakeByDelegators))
   257  	}
   258  	return total.ToDecimal()
   259  }
   260  
   261  // calculateRewardFactors calculates the fraction of the reward given to tendermint and ersatz validators based on their scaled stake.
   262  func (e *Engine) calculateRewardFactors(sp, se num.Decimal) (num.Decimal, num.Decimal) {
   263  	st := sp.Add(se)
   264  	spFactor := num.DecimalZero()
   265  	seFactor := num.DecimalZero()
   266  	// if there's stake calculate the factors of primary vs ersatz and make sure it's <= 1
   267  	if st.IsPositive() {
   268  		spFactor = sp.Div(st)
   269  		seFactor = se.Div(st)
   270  		// if the factors add to more than 1, subtract the excess from the ersatz factors to make the total 1
   271  		overflow := num.MaxD(num.DecimalZero(), spFactor.Add(seFactor).Sub(decimal1))
   272  		seFactor = seFactor.Sub(overflow)
   273  	}
   274  
   275  	e.log.Info("tendermint/ersatz fractions of the reward", logging.String("total-delegation", st.String()), logging.String("tenderming-total-delegation", sp.String()), logging.String("ersatz-total-delegation", se.String()), logging.String("tenderming-factor", spFactor.String()), logging.String("ersatz-factor", seFactor.String()))
   276  	return spFactor, seFactor
   277  }
   278  
   279  func (e *Engine) calculateRewardPayouts(ctx context.Context, epoch types.Epoch) []*payout {
   280  	// get the validator delegation data from the delegation engine and calculate the staking and delegation rewards for the epoch
   281  	delegationState := e.delegation.ProcessEpochDelegations(ctx, epoch)
   282  
   283  	stakeScoreParams := types.StakeScoreParams{MinVal: e.global.minValidators, CompLevel: e.global.compLevel, OptimalStakeMultiplier: e.global.optimalStakeMultiplier}
   284  
   285  	// NB: performance scores for rewards are calculated with the current values of the voting power
   286  	tmValidatorsScores, ersatzValidatorsScores := e.topology.GetRewardsScores(ctx, e.epochSeq, delegationState, stakeScoreParams)
   287  	tmValidatorsDelegation, ersatzValidatorsDelegation := e.splitDelegationByStatus(delegationState, tmValidatorsScores, ersatzValidatorsScores)
   288  
   289  	// let the topology process the changes in delegation set and calculate changes to tendermint/ersatz validator sets
   290  	// again, performance scores for ranking is based on the current voting powers.
   291  	// performance data will be erased in the next block which is the first block of the new epoch
   292  	rankingScoresContributions := e.topology.RecalcValidatorSet(ctx, num.NewUint(epoch.Seq+1).String(), e.delegation.GetValidatorData(), stakeScoreParams)
   293  
   294  	sp := calcTotalDelegation(tmValidatorsDelegation)
   295  	se := calcTotalDelegation(ersatzValidatorsDelegation).Mul(e.ersatzRewardFactor)
   296  	spFactor, seFactor := e.calculateRewardFactors(sp, se)
   297  	for node, score := range tmValidatorsScores.NormalisedScores {
   298  		e.log.Info("Rewards: calculated normalised score for tendermint validators", logging.String("validator", node), logging.String("normalisedScore", score.String()))
   299  	}
   300  	for node, score := range ersatzValidatorsScores.NormalisedScores {
   301  		e.log.Info("Rewards: calculated normalised score for ersatz validator", logging.String("validator", node), logging.String("normalisedScore", score.String()))
   302  	}
   303  
   304  	now := e.timeService.GetTimeNow()
   305  	payouts := []*payout{}
   306  	for _, rewardType := range rewardAccountTypes {
   307  		accounts := e.collateral.GetRewardAccountsByType(rewardType)
   308  		for _, account := range accounts {
   309  			if account.Balance.IsZero() {
   310  				continue
   311  			}
   312  			pos := []*payout{}
   313  			if (rewardType == types.AccountTypeGlobalReward && account.Asset == e.global.asset) || rewardType == types.AccountTypeFeesInfrastructure {
   314  				e.log.Info("calculating reward for tendermint validators", logging.String("account-type", rewardType.String()))
   315  				pos = append(pos, e.calculateRewardTypeForAsset(ctx, num.NewUint(epoch.Seq).String(), account.Asset, rewardType, account, tmValidatorsDelegation, tmValidatorsScores.NormalisedScores, epoch.EndTime, spFactor, rankingScoresContributions))
   316  				e.log.Info("calculating reward for ersatz validators", logging.String("account-type", rewardType.String()))
   317  				pos = append(pos, e.calculateRewardTypeForAsset(ctx, num.NewUint(epoch.Seq).String(), account.Asset, rewardType, account, ersatzValidatorsDelegation, ersatzValidatorsScores.NormalisedScores, epoch.EndTime, seFactor, rankingScoresContributions))
   318  			} else {
   319  				pos = append(pos, e.calculateRewardTypeForAsset(ctx, num.NewUint(epoch.Seq).String(), account.Asset, rewardType, account, tmValidatorsDelegation, tmValidatorsScores.NormalisedScores, epoch.EndTime, decimal1, rankingScoresContributions))
   320  			}
   321  			for _, po := range pos {
   322  				if po != nil && !po.totalReward.IsZero() && !po.totalReward.IsNegative() {
   323  					po.rewardType = rewardType
   324  					if account.MarketID != "!" {
   325  						po.gameID = &account.MarketID
   326  					}
   327  					po.timestamp = now.UnixNano()
   328  					payouts = append(payouts, po)
   329  					e.distributePayout(ctx, po)
   330  					po.lockedUntilEpoch = num.NewUint(po.lockedForEpochs + epoch.Seq).String()
   331  					e.emitEventsForPayout(ctx, now, po)
   332  				}
   333  			}
   334  		}
   335  	}
   336  
   337  	return payouts
   338  }
   339  
   340  func (e *Engine) convertTakerFeesToRewardAsset(takerFees map[string]*num.Uint, fromAsset string, toAsset string) map[string]*num.Uint {
   341  	out := make(map[string]*num.Uint, len(takerFees))
   342  	fromQuantum, err := e.collateral.GetAssetQuantum(fromAsset)
   343  	if err != nil {
   344  		return out
   345  	}
   346  	toQuantum, err := e.collateral.GetAssetQuantum(toAsset)
   347  	if err != nil {
   348  		return out
   349  	}
   350  
   351  	quantumRatio := toQuantum.Div(fromQuantum)
   352  	for k, u := range takerFees {
   353  		toAssetAmt, _ := num.UintFromDecimal(u.ToDecimal().Mul(quantumRatio))
   354  		out[k] = toAssetAmt
   355  	}
   356  	return out
   357  }
   358  
   359  func (e *Engine) getRewardMultiplierForParty(party string) num.Decimal {
   360  	asMultiplier := e.activityStreak.GetRewardsDistributionMultiplier(party)
   361  	_, summed := e.vesting.GetSingleAndSummedRewardBonusMultipliers(party)
   362  	return asMultiplier.Mul(summed.Multiplier)
   363  }
   364  
   365  func filterEligible(ps []*types.PartyContributionScore) []*types.PartyContributionScore {
   366  	filtered := []*types.PartyContributionScore{}
   367  	for _, psEntry := range ps {
   368  		if psEntry.IsEligible {
   369  			filtered = append(filtered, psEntry)
   370  		}
   371  	}
   372  	return filtered
   373  }
   374  
   375  // calculateRewardTypeForAsset calculates the payout for a given asset and reward type.
   376  // for market based rewards, we only care about account for specific markets (as opposed to global account for an asset).
   377  func (e *Engine) calculateRewardTypeForAsset(ctx context.Context, epochSeq, asset string, rewardType types.AccountType, account *types.Account, validatorData []*types.ValidatorData, validatorNormalisedScores map[string]num.Decimal, timestamp time.Time, factor num.Decimal, rankingScoresContributions []*types.PartyContributionScore) *payout {
   378  	switch rewardType {
   379  	case types.AccountTypeGlobalReward: // given to delegator based on stake
   380  		if asset == e.global.asset {
   381  			balance, _ := num.UintFromDecimal(account.Balance.ToDecimal().Mul(factor))
   382  			e.log.Info("reward balance", logging.String("epoch", epochSeq), logging.String("reward-type", rewardType.String()), logging.String("account-balance", account.Balance.String()), logging.String("factor", factor.String()), logging.String("effective-balance", balance.String()))
   383  			return calculateRewardsByStake(epochSeq, account.Asset, account.ID, balance, validatorNormalisedScores, validatorData, e.global.delegatorShare, e.global.maxPayoutPerParticipant, e.log)
   384  		}
   385  		return nil
   386  	case types.AccountTypeFeesInfrastructure: // given to delegator based on stake
   387  		balance, _ := num.UintFromDecimal(account.Balance.ToDecimal().Mul(factor))
   388  		e.log.Info("reward balance", logging.String("epoch", epochSeq), logging.String("reward-type", rewardType.String()), logging.String("account-balance", account.Balance.String()), logging.String("factor", factor.String()), logging.String("effective-balance", balance.String()))
   389  		return calculateRewardsByStake(epochSeq, account.Asset, account.ID, balance, validatorNormalisedScores, validatorData, e.global.delegatorShare, num.UintZero(), e.log)
   390  	case types.AccountTypeMakerReceivedFeeReward, types.AccountTypeMakerPaidFeeReward, types.AccountTypeLPFeeReward, types.AccountTypeAverageNotionalReward, types.AccountTypeRelativeReturnReward, types.AccountTypeReturnVolatilityReward, types.AccountTypeRealisedReturnReward, types.AccountTypeEligibleEntitiesReward:
   391  		ds := e.transfers.GetDispatchStrategy(account.MarketID)
   392  		if ds == nil {
   393  			return nil
   394  		}
   395  		var takerFeesPaidInRewardAsset map[string]*num.Uint
   396  		if ds.CapRewardFeeMultiple != nil {
   397  			epochs := int32(1)
   398  			if ds.TransferInterval != nil {
   399  				epochs = *ds.TransferInterval
   400  			}
   401  			takerFeesPaid := e.marketActivityTracker.GetLastEpochTakeFees(ds.AssetForMetric, ds.Markets, epochs)
   402  			takerFeesPaidInRewardAsset = e.convertTakerFeesToRewardAsset(takerFeesPaid, ds.AssetForMetric, asset)
   403  		}
   404  		if ds.EntityScope == vega.EntityScope_ENTITY_SCOPE_INDIVIDUALS {
   405  			partyScores := filterEligible(e.marketActivityTracker.CalculateMetricForIndividuals(ctx, ds))
   406  			partyRewardFactors := map[string]num.Decimal{}
   407  			for _, pcs := range partyScores {
   408  				partyRewardFactors[pcs.Party] = e.getRewardMultiplierForParty(pcs.Party)
   409  			}
   410  			return calculateRewardsByContributionIndividual(epochSeq, account.Asset, account.ID, account.Balance, partyScores, partyRewardFactors, timestamp, ds, takerFeesPaidInRewardAsset)
   411  		} else {
   412  			teamScores, partyScores := e.marketActivityTracker.CalculateMetricForTeams(ctx, ds)
   413  			filteredPartyScore := map[string][]*types.PartyContributionScore{}
   414  			for t, team := range partyScores {
   415  				filtered := filterEligible(team)
   416  				if len(filtered) > 0 {
   417  					filteredPartyScore[t] = filtered
   418  				}
   419  			}
   420  			partyScores = filteredPartyScore
   421  			partyRewardFactors := map[string]num.Decimal{}
   422  			for _, team := range partyScores {
   423  				for _, pcs := range team {
   424  					partyRewardFactors[pcs.Party] = e.getRewardMultiplierForParty(pcs.Party)
   425  				}
   426  			}
   427  			return calculateRewardsByContributionTeam(epochSeq, account.Asset, account.ID, account.Balance, teamScores, partyScores, partyRewardFactors, timestamp, ds, takerFeesPaidInRewardAsset)
   428  		}
   429  
   430  	case types.AccountTypeMarketProposerReward:
   431  		p := calculateRewardForProposers(epochSeq, account.Asset, account.ID, account.Balance, e.marketActivityTracker.GetProposer(account.MarketID), timestamp)
   432  		return p
   433  	case types.AccountTypeValidatorRankingReward:
   434  		ds := e.transfers.GetDispatchStrategy(account.MarketID)
   435  		if ds == nil {
   436  			return nil
   437  		}
   438  		return calculateRewardsForValidators(epochSeq, account.Asset, account.ID, account.Balance, timestamp, rankingScoresContributions, ds.LockPeriod)
   439  	}
   440  
   441  	return nil
   442  }
   443  
   444  func (e *Engine) emitEventsForPayout(ctx context.Context, timeToSend time.Time, po *payout) {
   445  	payoutEvents := map[string]*events.RewardPayout{}
   446  	parties := []string{}
   447  	totalReward := po.totalReward.ToDecimal()
   448  	assetQuantum, _ := e.collateral.GetAssetQuantum(po.asset)
   449  	for party, amount := range po.partyToAmount {
   450  		proportion := amount.ToDecimal().Div(totalReward)
   451  		pct := proportion.Mul(num.DecimalFromInt64(100))
   452  		payoutEvents[party] = events.NewRewardPayout(ctx, timeToSend.UnixNano(), party, po.epochSeq, po.asset, amount, assetQuantum, pct, po.rewardType, po.gameID, po.lockedUntilEpoch)
   453  		parties = append(parties, party)
   454  	}
   455  	sort.Strings(parties)
   456  	payoutEventSlice := make([]events.Event, 0, len(parties))
   457  	for _, p := range parties {
   458  		payoutEventSlice = append(payoutEventSlice, *payoutEvents[p])
   459  	}
   460  	e.broker.SendBatch(payoutEventSlice)
   461  }
   462  
   463  // distributePayout creates a set of transfers corresponding to a reward payout.
   464  func (e *Engine) distributePayout(ctx context.Context, po *payout) {
   465  	partyIDs := make([]string, 0, len(po.partyToAmount))
   466  	for party := range po.partyToAmount {
   467  		partyIDs = append(partyIDs, party)
   468  	}
   469  
   470  	sort.Strings(partyIDs)
   471  	transfers := make([]*types.Transfer, 0, len(partyIDs))
   472  	for _, party := range partyIDs {
   473  		amt := po.partyToAmount[party]
   474  		transfers = append(transfers, &types.Transfer{
   475  			Owner: party,
   476  			Amount: &types.FinancialAmount{
   477  				Asset:  po.asset,
   478  				Amount: amt.Clone(),
   479  			},
   480  			Type:      types.TransferTypeRewardPayout,
   481  			MinAmount: amt.Clone(),
   482  		})
   483  	}
   484  
   485  	responses, err := e.collateral.TransferRewards(ctx, po.fromAccount, transfers, po.rewardType)
   486  	if err != nil {
   487  		e.log.Error("error in transfer rewards", logging.Error(err))
   488  		return
   489  	}
   490  
   491  	// if the reward type is not infra fee, report it to the vesting engine
   492  	if po.rewardType != types.AccountTypeFeesInfrastructure {
   493  		for _, party := range partyIDs {
   494  			amt := po.partyToAmount[party]
   495  			e.vesting.AddReward(ctx, party, po.asset, amt, po.lockedForEpochs)
   496  		}
   497  	}
   498  	e.broker.Send(events.NewLedgerMovements(ctx, responses))
   499  }