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, &notionalVolume{
   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  }