code.vegaprotocol.io/vega@v0.79.0/core/referral/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 referral 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "sort" 23 "time" 24 25 "code.vegaprotocol.io/vega/core/events" 26 "code.vegaprotocol.io/vega/core/types" 27 "code.vegaprotocol.io/vega/libs/num" 28 vegapb "code.vegaprotocol.io/vega/protos/vega" 29 snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 30 31 "golang.org/x/exp/maps" 32 "golang.org/x/exp/slices" 33 ) 34 35 const MaximumWindowLength uint64 = 200 36 37 var ( 38 ErrIsAlreadyAReferee = func(party types.PartyID) error { 39 return fmt.Errorf("party %q has already been referred", party) 40 } 41 42 ErrIsAlreadyAReferrer = func(party types.PartyID) error { 43 return fmt.Errorf("party %q is already a referrer", party) 44 } 45 46 ErrUnknownReferralCode = func(code types.ReferralSetID) error { 47 return fmt.Errorf("no referral set for referral code %q", code) 48 } 49 50 ErrNotEligibleForReferralRewards = func(party string, balance, required *num.Uint) error { 51 return fmt.Errorf("party %q not eligible for referral rewards, staking balance required of %s got %s", party, required.String(), balance.String()) 52 } 53 54 ErrNotPartOfAReferralSet = func(party types.PartyID) error { 55 return fmt.Errorf("party %q is not part of a referral set", party) 56 } 57 58 ErrPartyDoesNotOwnReferralSet = func(party types.PartyID) error { 59 return fmt.Errorf("party %q does not own the referral set", party) 60 } 61 62 ErrUnknownSetID = errors.New("unknown set ID") 63 ) 64 65 type Engine struct { 66 broker Broker 67 marketActivityTracker MarketActivityTracker 68 timeSvc TimeService 69 70 currentEpoch uint64 71 staking StakingBalances 72 73 // referralSetsNotionalVolumes tracks the notional volumes per teams. Each 74 // element of the num.Uint array is an epoch. 75 referralSetsNotionalVolumes *runningVolumes 76 factorsByReferee map[types.PartyID]*types.RefereeStats 77 78 // referralProgramMinStakedVegaTokens defines the minimum number of token a 79 // party must possess to become and stay a referrer. 80 referralProgramMinStakedVegaTokens *num.Uint 81 82 // referralProgramMaxRewardProportion limits the proportion of referee taker 83 // fees which can be given to the referrer. 84 referralProgramMaxRewardProportion num.Decimal 85 86 // minBalanceToApplyCode defines the minimum balance a party should possess 87 // to apply a referral code. 88 minBalanceToApplyCode *num.Uint 89 90 // minBalanceForReferralProgram defines the minimum balance a party should possess 91 // to create/update referral program. 92 minBalanceForReferralSet *num.Uint 93 94 // latestProgramVersion tracks the latest version of the program. It used to 95 // value any new program that comes in. It starts at 1. 96 // It's incremented every time an update is received. Therefore, if, during 97 // the same epoch, we have 2 successive updates, this field will be incremented 98 // twice. 99 latestProgramVersion uint64 100 101 // currentProgram is the program currently in used against which the reward 102 // are computed. 103 // It's `nil` is there is none. 104 currentProgram *types.ReferralProgram 105 106 // programHasEnded tells if the current program has reached it's 107 // end. It's flipped at the end of the epoch. 108 programHasEnded bool 109 // newProgram is the program born from the last enacted UpdateReferralProgram 110 // proposal to apply at the start of the next epoch. 111 // It's `nil` is there is none. 112 newProgram *types.ReferralProgram 113 114 sets map[types.ReferralSetID]*types.ReferralSet 115 referrers map[types.PartyID]types.ReferralSetID 116 referees map[types.PartyID]types.ReferralSetID 117 } 118 119 func (e *Engine) CheckSufficientBalanceForApplyReferralCode(party types.PartyID, balance *num.Uint) error { 120 if balance.LT(e.minBalanceToApplyCode) { 121 return fmt.Errorf("party %q does not have sufficient balance to apply referral code, required balance %s available balance %s", party, e.minBalanceToApplyCode.String(), balance.String()) 122 } 123 return nil 124 } 125 126 func (e *Engine) CheckSufficientBalanceForCreateOrUpdateReferralSet(party types.PartyID, balance *num.Uint) error { 127 if balance.LT(e.minBalanceForReferralSet) { 128 return fmt.Errorf("party %q does not have sufficient balance to create or update a referral set, required balance %s available balance %s", party, e.minBalanceForReferralSet.String(), balance.String()) 129 } 130 return nil 131 } 132 133 func (e *Engine) OnMinBalanceForReferralProgramUpdated(_ context.Context, min *num.Uint) error { 134 e.minBalanceForReferralSet = min 135 return nil 136 } 137 138 func (e *Engine) OnMinBalanceForApplyReferralCodeUpdated(_ context.Context, min *num.Uint) error { 139 e.minBalanceToApplyCode = min 140 return nil 141 } 142 143 func (e *Engine) GetReferrer(referee types.PartyID) (types.PartyID, error) { 144 setID, ok := e.referees[referee] 145 if !ok { 146 return "", ErrNotPartOfAReferralSet(referee) 147 } 148 149 return e.sets[setID].Referrer.PartyID, nil 150 } 151 152 func (e *Engine) PartyOwnsReferralSet(referer types.PartyID, setID types.ReferralSetID) error { 153 rf, ok := e.sets[setID] 154 if !ok { 155 return ErrUnknownSetID 156 } 157 158 if referer != rf.Referrer.PartyID { 159 return ErrPartyDoesNotOwnReferralSet(referer) 160 } 161 return nil 162 } 163 164 func (e *Engine) CreateReferralSet(ctx context.Context, party types.PartyID, deterministicSetID types.ReferralSetID) error { 165 if _, ok := e.referrers[party]; ok { 166 return ErrIsAlreadyAReferrer(party) 167 } 168 if _, ok := e.referees[party]; ok { 169 return ErrIsAlreadyAReferee(party) 170 } 171 172 if err := e.isPartyEligible(party); err != nil { 173 return err 174 } 175 176 now := e.timeSvc.GetTimeNow() 177 178 newSet := types.ReferralSet{ 179 ID: deterministicSetID, 180 CreatedAt: now, 181 UpdatedAt: now, 182 Referrer: &types.Membership{ 183 PartyID: party, 184 JoinedAt: now, 185 StartedAtEpoch: e.currentEpoch, 186 }, 187 CurrentRewardFactors: types.EmptyFactors, 188 CurrentRewardsMultiplier: num.DecimalZero(), 189 CurrentRewardsFactorMultiplier: types.EmptyFactors, 190 } 191 192 e.sets[deterministicSetID] = &newSet 193 e.referrers[party] = deterministicSetID 194 195 e.broker.Send(events.NewReferralSetCreatedEvent(ctx, &newSet)) 196 197 return nil 198 } 199 200 func (e *Engine) ApplyReferralCode(ctx context.Context, party types.PartyID, setID types.ReferralSetID) error { 201 if _, ok := e.referrers[party]; ok { 202 return ErrIsAlreadyAReferrer(party) 203 } 204 205 var ( 206 isSwitching bool 207 prevSet types.ReferralSetID 208 ok bool 209 ) 210 if prevSet, ok = e.referees[party]; ok { 211 isSwitching = e.canSwitchReferralSet(party, setID) 212 if !isSwitching { 213 return ErrIsAlreadyAReferee(party) 214 } 215 } 216 217 set, ok := e.sets[setID] 218 if !ok { 219 return ErrUnknownReferralCode(setID) 220 } 221 222 now := e.timeSvc.GetTimeNow() 223 224 set.UpdatedAt = now 225 226 membership := &types.Membership{ 227 PartyID: party, 228 JoinedAt: now, 229 StartedAtEpoch: e.currentEpoch, 230 } 231 set.Referees = append(set.Referees, membership) 232 233 e.referees[party] = set.ID 234 235 e.broker.Send(events.NewRefereeJoinedReferralSetEvent(ctx, setID, membership)) 236 237 if isSwitching { 238 e.removeFromSet(party, prevSet) 239 } 240 241 return nil 242 } 243 244 func (e *Engine) removeFromSet(party types.PartyID, prevSet types.ReferralSetID) { 245 set := e.sets[prevSet] 246 247 var idx int 248 for i, r := range set.Referees { 249 if r.PartyID == party { 250 idx = i 251 break 252 } 253 } 254 255 set.Referees = append(set.Referees[:idx], set.Referees[idx+1:]...) 256 } 257 258 func (e *Engine) UpdateProgram(newProgram *types.ReferralProgram) { 259 e.latestProgramVersion += 1 260 e.newProgram = newProgram 261 262 sort.Slice(e.newProgram.BenefitTiers, func(i, j int) bool { 263 if e.newProgram.BenefitTiers[i].MinimumRunningNotionalTakerVolume.EQ(e.newProgram.BenefitTiers[j].MinimumRunningNotionalTakerVolume) { 264 return e.newProgram.BenefitTiers[i].MinimumEpochs.LT(e.newProgram.BenefitTiers[j].MinimumEpochs) 265 } 266 return e.newProgram.BenefitTiers[i].MinimumRunningNotionalTakerVolume.LT(e.newProgram.BenefitTiers[j].MinimumRunningNotionalTakerVolume) 267 }) 268 269 sort.Slice(e.newProgram.StakingTiers, func(i, j int) bool { 270 return e.newProgram.StakingTiers[i].MinimumStakedTokens.LT(e.newProgram.StakingTiers[j].MinimumStakedTokens) 271 }) 272 273 e.newProgram.Version = e.latestProgramVersion 274 } 275 276 func (e *Engine) HasProgramEnded() bool { 277 return e.programHasEnded 278 } 279 280 func (e *Engine) ReferralDiscountFactorsForParty(party types.PartyID) types.Factors { 281 if e.programHasEnded { 282 return types.EmptyFactors 283 } 284 285 factors, ok := e.factorsByReferee[party] 286 if !ok { 287 return types.EmptyFactors 288 } 289 290 return factors.DiscountFactors 291 } 292 293 func (e *Engine) RewardsFactorForParty(party types.PartyID) types.Factors { 294 if e.programHasEnded { 295 return types.EmptyFactors 296 } 297 298 setID, ok := e.referees[party] 299 if !ok { 300 return types.EmptyFactors 301 } 302 303 return e.sets[setID].CurrentRewardFactors 304 } 305 306 func (e *Engine) RewardsFactorsMultiplierAppliedForParty(party types.PartyID) types.Factors { 307 setID, ok := e.referees[party] 308 if !ok { 309 return types.EmptyFactors 310 } 311 312 return e.sets[setID].CurrentRewardsFactorMultiplier 313 } 314 315 func (e *Engine) RewardsMultiplierForParty(party types.PartyID) num.Decimal { 316 setID, ok := e.referees[party] 317 if !ok { 318 return num.DecimalZero() 319 } 320 321 return e.sets[setID].CurrentRewardsMultiplier 322 } 323 324 func (e *Engine) OnReferralProgramMaxReferralRewardProportionUpdate(_ context.Context, value num.Decimal) error { 325 e.referralProgramMaxRewardProportion = value 326 return nil 327 } 328 329 func (e *Engine) OnReferralProgramMinStakedVegaTokensUpdate(_ context.Context, value *num.Uint) error { 330 e.referralProgramMinStakedVegaTokens = value 331 return nil 332 } 333 334 func (e *Engine) OnReferralProgramMaxPartyNotionalVolumeByQuantumPerEpochUpdate(_ context.Context, value *num.Uint) error { 335 e.referralSetsNotionalVolumes.maxPartyNotionalVolumeByQuantumPerEpoch = value 336 return nil 337 } 338 339 func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { 340 switch ep.Action { 341 case vegapb.EpochAction_EPOCH_ACTION_START: 342 pp := e.currentProgram 343 e.currentEpoch = ep.Seq 344 e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) 345 // we have an active program, and it's new (pp could be nil, or a pointer to the program before it was updated) 346 if !e.programHasEnded && pp != e.currentProgram { 347 e.computeReferralSetsStats(ctx, ep, false) 348 } 349 case vegapb.EpochAction_EPOCH_ACTION_END: 350 e.computeReferralSetsStats(ctx, ep, true) 351 } 352 } 353 354 func (e *Engine) OnEpochRestore(_ context.Context, ep types.Epoch) { 355 if ep.Action == vegapb.EpochAction_EPOCH_ACTION_START { 356 e.currentEpoch = ep.Seq 357 } 358 } 359 360 func (e *Engine) applyProgramUpdate(ctx context.Context, startEpochTime time.Time, epoch uint64) { 361 if e.newProgram != nil { 362 if e.currentProgram != nil { 363 e.endCurrentProgram() 364 e.startNewProgram() 365 e.notifyReferralProgramUpdated(ctx, startEpochTime, epoch) 366 } else { 367 e.startNewProgram() 368 e.notifyReferralProgramStarted(ctx, startEpochTime, epoch) 369 } 370 } 371 372 // This handles a edge case where the new program ends before the next 373 // epoch starts. It can happen when the proposal updating the referral 374 // program specifies an end date that is within the same epoch as the enactment 375 // time. 376 if e.currentProgram != nil && !e.currentProgram.EndOfProgramTimestamp.IsZero() && !e.currentProgram.EndOfProgramTimestamp.After(startEpochTime) { 377 e.notifyReferralProgramEnded(ctx, startEpochTime, epoch) 378 e.endCurrentProgram() 379 } 380 } 381 382 func (e *Engine) endCurrentProgram() { 383 e.programHasEnded = true 384 e.currentProgram = nil 385 } 386 387 func (e *Engine) startNewProgram() { 388 e.programHasEnded = false 389 e.currentProgram = e.newProgram 390 e.newProgram = nil 391 } 392 393 func (e *Engine) notifyReferralProgramStarted(ctx context.Context, epochTime time.Time, epoch uint64) { 394 e.broker.Send(events.NewReferralProgramStartedEvent(ctx, e.currentProgram, epochTime, epoch)) 395 } 396 397 func (e *Engine) notifyReferralProgramUpdated(ctx context.Context, epochTime time.Time, epoch uint64) { 398 e.broker.Send(events.NewReferralProgramUpdatedEvent(ctx, e.currentProgram, epochTime, epoch)) 399 } 400 401 func (e *Engine) notifyReferralProgramEnded(ctx context.Context, epochTime time.Time, epoch uint64) { 402 e.broker.Send(events.NewReferralProgramEndedEvent(ctx, e.currentProgram.Version, e.currentProgram.ID, epochTime, epoch)) 403 } 404 405 func (e *Engine) notifyReferralSetStatsUpdated(ctx context.Context, stats *types.ReferralSetStats) { 406 e.broker.Send(events.NewReferralSetStatsUpdatedEvent(ctx, stats)) 407 } 408 409 func (e *Engine) load(referralProgramState *types.PayloadReferralProgramState) { 410 if referralProgramState.CurrentProgram != nil { 411 e.currentProgram = types.NewReferralProgramFromProto(referralProgramState.CurrentProgram) 412 } 413 if referralProgramState.NewProgram != nil { 414 e.newProgram = types.NewReferralProgramFromProto(referralProgramState.NewProgram) 415 } 416 e.latestProgramVersion = referralProgramState.LastProgramVersion 417 e.programHasEnded = referralProgramState.ProgramHasEnded 418 e.loadReferralSetsFromSnapshot(referralProgramState.Sets) 419 e.loadFactorsByReferee(referralProgramState.FactorByReferee) 420 } 421 422 func (e *Engine) loadFactorsByReferee(factors []*snapshotpb.FactorByReferee) { 423 e.factorsByReferee = make(map[types.PartyID]*types.RefereeStats, len(factors)) 424 for _, fbr := range factors { 425 party := types.PartyID(fbr.Party) 426 takerVolume := num.UintFromBytes(fbr.TakerVolume) 427 428 factors := types.Factors{} 429 if fbr.DiscountFactors != nil { 430 factors.Infra, _ = num.DecimalFromString(fbr.DiscountFactors.InfrastructureDiscountFactor) 431 factors.Liquidity, _ = num.DecimalFromString(fbr.DiscountFactors.LiquidityDiscountFactor) 432 factors.Maker, _ = num.DecimalFromString(fbr.DiscountFactors.MakerDiscountFactor) 433 } 434 if len(fbr.DiscountFactor) > 0 { 435 defaultDF, _ := num.UnmarshalBinaryDecimal(fbr.DiscountFactor) 436 factors.Infra = defaultDF 437 factors.Liquidity = defaultDF 438 factors.Maker = defaultDF 439 } 440 e.factorsByReferee[party] = &types.RefereeStats{ 441 DiscountFactors: factors, 442 TakerVolume: takerVolume, 443 } 444 } 445 } 446 447 func (e *Engine) loadReferralSetsFromSnapshot(setsProto []*snapshotpb.ReferralSet) { 448 for _, setProto := range setsProto { 449 setID := types.ReferralSetID(setProto.Id) 450 451 newSet := &types.ReferralSet{ 452 ID: setID, 453 CreatedAt: time.Unix(0, setProto.CreatedAt), 454 UpdatedAt: time.Unix(0, setProto.UpdatedAt), 455 Referrer: &types.Membership{ 456 PartyID: types.PartyID(setProto.Referrer.PartyId), 457 JoinedAt: time.Unix(0, setProto.Referrer.JoinedAt), 458 StartedAtEpoch: setProto.Referrer.StartedAtEpoch, 459 }, 460 CurrentRewardFactors: types.FactorsFromRewardFactorsWithDefault(setProto.CurrentRewardFactors, setProto.CurrentRewardFactor), 461 CurrentRewardsMultiplier: num.MustDecimalFromString(setProto.CurrentRewardsMultiplier), 462 CurrentRewardsFactorMultiplier: types.FactorsFromRewardFactorsWithDefault(setProto.CurrentRewardsFactorsMultiplier, setProto.CurrentRewardsFactorMultiplier), 463 } 464 465 e.referrers[types.PartyID(setProto.Referrer.PartyId)] = setID 466 467 for _, r := range setProto.Referees { 468 partyID := types.PartyID(r.PartyId) 469 e.referees[partyID] = setID 470 newSet.Referees = append(newSet.Referees, 471 &types.Membership{ 472 PartyID: partyID, 473 JoinedAt: time.Unix(0, r.JoinedAt), 474 StartedAtEpoch: r.StartedAtEpoch, 475 }, 476 ) 477 } 478 479 runningVolumes := make([]*notionalVolume, 0, len(setProto.RunningVolumes)) 480 for _, volume := range setProto.RunningVolumes { 481 var volumeNum *num.Uint 482 if len(volume.Volume) > 0 { 483 volumeNum = num.UintFromBytes(volume.Volume) 484 } 485 runningVolumes = append(runningVolumes, ¬ionalVolume{ 486 epoch: volume.Epoch, 487 value: volumeNum, 488 }) 489 } 490 491 // set only if the running volume is not empty, or it will panic 492 // down the line when trying to add new ones. 493 // the creation of runningVolumeBySet is done in the Add method of the 494 // runningVolumes type. 495 if len(runningVolumes) > 0 { 496 e.referralSetsNotionalVolumes.runningVolumesBySet[setID] = runningVolumes 497 } 498 499 e.sets[setID] = newSet 500 } 501 } 502 503 func (e *Engine) computeReferralSetsStats(ctx context.Context, epoch types.Epoch, sendEvents bool) { 504 priorEpoch := uint64(0) 505 if epoch.Seq > MaximumWindowLength { 506 priorEpoch = epoch.Seq - MaximumWindowLength 507 } 508 e.referralSetsNotionalVolumes.RemovePriorEpoch(priorEpoch) 509 510 referrersTakerVolume := map[types.PartyID]*num.Uint{} 511 512 for partyID, setID := range e.referrers { 513 volumeForEpoch := e.marketActivityTracker.NotionalTakerVolumeForParty(string(partyID)) 514 e.referralSetsNotionalVolumes.Add(epoch.Seq, setID, volumeForEpoch) 515 referrersTakerVolume[partyID] = volumeForEpoch 516 } 517 518 takerVolumePerReferee := map[types.PartyID]*num.Uint{} 519 520 for partyID, setID := range e.referees { 521 volumeForEpoch := e.marketActivityTracker.NotionalTakerVolumeForParty(string(partyID)) 522 e.referralSetsNotionalVolumes.Add(epoch.Seq, setID, volumeForEpoch) 523 takerVolumePerReferee[partyID] = volumeForEpoch 524 } 525 526 if e.programHasEnded { 527 return 528 } 529 530 e.computeFactorsByReferee(ctx, epoch.Seq, takerVolumePerReferee, referrersTakerVolume, sendEvents) 531 } 532 533 func (e *Engine) computeFactorsByReferee(ctx context.Context, epoch uint64, takerVolumePerReferee, referrersTakesVolume map[types.PartyID]*num.Uint, sendEvents bool) { 534 e.factorsByReferee = map[types.PartyID]*types.RefereeStats{} 535 536 allStats := map[types.ReferralSetID]*types.ReferralSetStats{} 537 538 for setID, set := range e.sets { 539 referrerTakerVolume := num.UintZero() 540 if takerVolume := referrersTakesVolume[set.Referrer.PartyID]; takerVolume != nil { 541 referrerTakerVolume = takerVolume 542 } 543 setStats := &types.ReferralSetStats{ 544 AtEpoch: epoch, 545 SetID: setID, 546 WasEligible: false, 547 ReferralSetRunningVolume: num.UintZero(), 548 RefereesStats: map[types.PartyID]*types.RefereeStats{}, 549 ReferrerTakerVolume: referrerTakerVolume, 550 RewardFactors: types.EmptyFactors, 551 RewardsMultiplier: num.DecimalOne(), 552 RewardsFactorsMultiplier: types.EmptyFactors, 553 } 554 555 setStats.ReferralSetRunningVolume = e.referralSetsNotionalVolumes.RunningSetVolumeForWindow(setID, e.currentProgram.WindowLength) 556 557 stakingBalance, _ := e.staking.GetAvailableBalance(set.Referrer.PartyID.String()) 558 setStats.WasEligible = stakingBalance.GTE(e.referralProgramMinStakedVegaTokens) 559 560 if setStats.WasEligible { 561 setStats.RewardFactors = e.matchRewardFactor(setStats.ReferralSetRunningVolume) 562 setStats.RewardsMultiplier = e.matchRewardMultiplier(stakingBalance) 563 setStats.RewardsFactorsMultiplier = setStats.RewardFactors.CapRewardFactors(setStats.RewardsMultiplier, e.referralProgramMaxRewardProportion) 564 } 565 566 set.CurrentRewardFactors = setStats.RewardFactors 567 set.CurrentRewardsMultiplier = setStats.RewardsMultiplier 568 set.CurrentRewardsFactorMultiplier = setStats.RewardsFactorsMultiplier 569 570 allStats[setID] = setStats 571 } 572 573 for referee, setID := range e.referees { 574 set := e.sets[setID] 575 576 epochCount := uint64(0) 577 for _, refereeMembership := range set.Referees { 578 if refereeMembership.PartyID == referee { 579 epochCount = e.currentEpoch - refereeMembership.StartedAtEpoch + 1 580 break 581 } 582 } 583 584 partyTakerVolume := num.UintZero() 585 if takerVolume := takerVolumePerReferee[referee]; takerVolume != nil { 586 partyTakerVolume = takerVolume 587 } 588 589 refereeStats := &types.RefereeStats{ 590 TakerVolume: partyTakerVolume, 591 DiscountFactors: types.EmptyFactors, 592 } 593 594 setStats := allStats[setID] 595 setStats.RefereesStats[referee] = refereeStats 596 e.factorsByReferee[referee] = refereeStats 597 598 if setStats.WasEligible { 599 refereeStats.DiscountFactors = e.matchDiscountFactor(epochCount, setStats.ReferralSetRunningVolume) 600 } 601 } 602 603 if !sendEvents { 604 return 605 } 606 setIDs := maps.Keys(allStats) 607 slices.Sort(setIDs) 608 for _, setID := range setIDs { 609 e.notifyReferralSetStatsUpdated(ctx, allStats[setID]) 610 } 611 } 612 613 func (e *Engine) matchDiscountFactor(epochCount uint64, setRunningVolume *num.Uint) types.Factors { 614 factors := types.EmptyFactors 615 for _, tier := range e.currentProgram.BenefitTiers { 616 if epochCount < tier.MinimumEpochs.Uint64() || setRunningVolume.LT(tier.MinimumRunningNotionalTakerVolume) { 617 break 618 } 619 factors = tier.ReferralDiscountFactors 620 } 621 622 return factors 623 } 624 625 func (e *Engine) matchRewardFactor(setRunningVolume *num.Uint) types.Factors { 626 factors := types.EmptyFactors 627 for _, tier := range e.currentProgram.BenefitTiers { 628 // NB: intentionally only checking the running notional here ignoring the epochs. 629 // This way if there are multiple entries with identical running volume we'll choose the last one, i.e. having most epochs 630 if setRunningVolume.LT(tier.MinimumRunningNotionalTakerVolume) { 631 break 632 } 633 factors = tier.ReferralRewardFactors 634 } 635 636 return factors 637 } 638 639 func (e *Engine) matchRewardMultiplier(stakingBalance *num.Uint) num.Decimal { 640 // This is set to 1 as the minimum value of a reward multiplier is 1. 641 multiplier := num.DecimalOne() 642 for _, tier := range e.currentProgram.StakingTiers { 643 if stakingBalance.LT(tier.MinimumStakedTokens) { 644 break 645 } 646 multiplier = tier.ReferralRewardMultiplier 647 } 648 649 return multiplier 650 } 651 652 func (e *Engine) isSetEligible(setID types.ReferralSetID) error { 653 set, ok := e.sets[setID] 654 if !ok { 655 return ErrUnknownSetID 656 } 657 658 return e.isPartyEligible(set.Referrer.PartyID) 659 } 660 661 func (e *Engine) canSwitchReferralSet(party types.PartyID, newSet types.ReferralSetID) bool { 662 currentSet := e.referees[party] 663 if currentSet == newSet { 664 return false 665 } 666 667 // if the current set is not eligible for rewards, 668 // then we can switch 669 if e.isSetEligible(currentSet) != nil { 670 return true 671 } 672 673 return false 674 } 675 676 func (e *Engine) isPartyEligible(party types.PartyID) error { 677 partyStr := party.String() 678 // Ignore error, function returns zero balance anyway. 679 balance, _ := e.staking.GetAvailableBalance(partyStr) 680 681 if balance.GTE(e.referralProgramMinStakedVegaTokens) { 682 return nil 683 } 684 685 return ErrNotEligibleForReferralRewards(partyStr, balance, e.referralProgramMinStakedVegaTokens) 686 } 687 688 func NewEngine(broker Broker, timeSvc TimeService, mat MarketActivityTracker, staking StakingBalances) *Engine { 689 engine := &Engine{ 690 broker: broker, 691 timeSvc: timeSvc, 692 marketActivityTracker: mat, 693 694 // There is no program yet, so we mark it has ended so consumer of this 695 // engine can know there is no reward computation to be done. 696 programHasEnded: true, 697 698 referralSetsNotionalVolumes: newRunningVolumes(), 699 700 referralProgramMinStakedVegaTokens: num.UintZero(), 701 702 sets: map[types.ReferralSetID]*types.ReferralSet{}, 703 referrers: map[types.PartyID]types.ReferralSetID{}, 704 referees: map[types.PartyID]types.ReferralSetID{}, 705 staking: staking, 706 } 707 708 return engine 709 }