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 }