code.vegaprotocol.io/vega@v0.79.0/core/rewards/staking_reward_calculator.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  	"sort"
    20  
    21  	"code.vegaprotocol.io/vega/core/types"
    22  	"code.vegaprotocol.io/vega/libs/num"
    23  	"code.vegaprotocol.io/vega/logging"
    24  )
    25  
    26  var minThresholdDelegatorReward, _ = num.DecimalFromString("0.001")
    27  
    28  // distribute rewards for a given asset account with the given settings of delegation and reward constraints.
    29  func calculateRewardsByStake(epochSeq, asset, accountID string, rewardBalance *num.Uint, valScore map[string]num.Decimal, validatorDelegation []*types.ValidatorData, delegatorShare num.Decimal, maxPayout *num.Uint, log *logging.Logger) *payout {
    30  	minLeftOverForDistribution := num.UintZero()
    31  	if !maxPayout.IsZero() {
    32  		minLeftOverForDistribution, _ = num.UintFromDecimal(minThresholdDelegatorReward.Mul(maxPayout.ToDecimal()))
    33  	}
    34  
    35  	// if there is no reward to give, return no payout
    36  	rewards := map[string]*num.Uint{}
    37  	totalRewardPayout := num.UintZero()
    38  	reward := rewardBalance.Clone()
    39  	if reward.IsZero() {
    40  		return &payout{
    41  			partyToAmount: rewards,
    42  			totalReward:   totalRewardPayout,
    43  			asset:         asset,
    44  			epochSeq:      epochSeq,
    45  		}
    46  	}
    47  
    48  	for _, vd := range validatorDelegation {
    49  		valScore := valScore[vd.NodeID] // normalised score
    50  		if valScore.IsZero() {
    51  			// if the validator isn't eligible for reward this round, nothing to do here
    52  			continue
    53  		}
    54  
    55  		// how much reward is assigned to the validator and its delegators
    56  		epochPayoutForValidatorAndDelegators := valScore.Mul(reward.ToDecimal())
    57  
    58  		// calculate the fraction delegators to the validator get
    59  		totalStakeForValidator := vd.StakeByDelegators.ToDecimal().Add(vd.SelfStake.ToDecimal())
    60  		delegatorFraction := delegatorShare.Mul(vd.StakeByDelegators.ToDecimal()).Div(totalStakeForValidator) // totalStakeForValidator must be non zero as valScore is non zero
    61  		validatorFraction := num.DecimalFromInt64(1).Sub(delegatorFraction)
    62  
    63  		// how much delegators take
    64  		amountToGiveToDelegators, _ := num.UintFromDecimal(delegatorFraction.Mul(epochPayoutForValidatorAndDelegators))
    65  		// calculate the potential reward for delegators and the validator
    66  		amountToKeepByValidator, _ := num.UintFromDecimal(validatorFraction.Mul(epochPayoutForValidatorAndDelegators))
    67  
    68  		log.Info("Rewards: reward calculation for validator",
    69  			logging.String("epochSeq", epochSeq),
    70  			logging.String("epochPayoutForValidatorAndDelegators", epochPayoutForValidatorAndDelegators.String()),
    71  			logging.String("totalStakeForValidator", totalStakeForValidator.String()),
    72  			logging.String("delegatorFraction", delegatorFraction.String()),
    73  			logging.String("validatorFraction", validatorFraction.String()),
    74  			logging.String("amountToGiveToDelegators", amountToGiveToDelegators.String()),
    75  			logging.String("amountToKeepByValidator", amountToKeepByValidator.String()))
    76  
    77  		// check how much reward the validator can accept with the cap per participant
    78  		rewardForNode, ok := rewards[vd.PubKey]
    79  		if !ok {
    80  			rewardForNode = num.UintZero()
    81  		}
    82  		// if there is no cap just add the total payout for the validator
    83  		if maxPayout.IsZero() {
    84  			rewards[vd.PubKey] = num.Sum(rewardForNode, amountToKeepByValidator)
    85  			totalRewardPayout.AddSum(amountToKeepByValidator)
    86  		} else {
    87  			balanceWithPayout := num.UintZero().Add(rewardForNode, amountToKeepByValidator)
    88  			if balanceWithPayout.LTE(maxPayout) {
    89  				rewards[vd.PubKey] = balanceWithPayout
    90  				totalRewardPayout.AddSum(amountToKeepByValidator)
    91  			} else {
    92  				rewards[vd.PubKey] = maxPayout
    93  				totalRewardPayout.AddSum(num.UintZero().Sub(maxPayout, rewardForNode))
    94  			}
    95  		}
    96  
    97  		log.Info("Rewards: reward kept by validator for epoch (post max payout cap)", logging.String("epoch", epochSeq), logging.String("validator", vd.NodeID), logging.String("amountToKeepByValidator", rewards[vd.PubKey].String()))
    98  
    99  		remainingRewardForDelegators := amountToGiveToDelegators
   100  
   101  		// if there's nothing to give to delegators move on
   102  		if remainingRewardForDelegators.IsZero() {
   103  			continue
   104  		}
   105  
   106  		// calculate the weight of each delegator out of the delegated stake to the validator
   107  		delegatorWeights := make(map[string]num.Decimal, len(vd.Delegators))
   108  		weightSums := num.DecimalZero()
   109  		decimalOne := num.DecimalFromInt64(1)
   110  		sortedParties := make([]string, 0, len(vd.Delegators))
   111  
   112  		for party, delegatorAmt := range vd.Delegators {
   113  			delegatorWeight := delegatorAmt.ToDecimal().Div(vd.StakeByDelegators.ToDecimal()) // this is not entered if there are no delegators
   114  			weightSums = weightSums.Add(delegatorWeight)
   115  			// if the party has 0 delegation, ignore it
   116  			if !delegatorAmt.IsZero() {
   117  				delegatorWeights[party] = delegatorWeight
   118  				sortedParties = append(sortedParties, party)
   119  			}
   120  		}
   121  		sort.Strings(sortedParties)
   122  
   123  		adjustWeights(delegatorWeights, weightSums, sortedParties, decimalOne)
   124  
   125  		// calculate delegator amounts
   126  		// this may take a few rounds due to the cap on the reward a party can get
   127  		// if we still have parties that haven't maxed their reward, they are split the remaining balance
   128  		roundsRemaining := 10
   129  		for {
   130  			log.Info("Reward remaining to distribute to delegators", logging.String("epoch", epochSeq), logging.String("remainingRewardForDelegators", remainingRewardForDelegators.String()))
   131  
   132  			totalAwardedThisRound := num.UintZero()
   133  			for _, party := range sortedParties {
   134  				// check if the party has already rewards from other validators or previous rounds (this epoch)
   135  				rewardForParty, ok := rewards[party]
   136  				if !ok {
   137  					rewardForParty = num.UintZero()
   138  				}
   139  
   140  				delegatorWeight := delegatorWeights[party]
   141  				rewardAsUint, _ := num.UintFromDecimal(delegatorWeight.Mul(remainingRewardForDelegators.ToDecimal()))
   142  				if maxPayout.IsZero() {
   143  					totalAwardedThisRound.AddSum(rewardAsUint)
   144  					totalRewardPayout.AddSum(rewardAsUint)
   145  					rewards[party] = num.UintZero().Add(rewardForParty, rewardAsUint)
   146  				} else {
   147  					balanceWithPayout := num.UintZero().Add(rewardForParty, rewardAsUint)
   148  					if balanceWithPayout.LTE(maxPayout) {
   149  						rewards[party] = balanceWithPayout
   150  						totalAwardedThisRound.AddSum(rewardAsUint)
   151  						totalRewardPayout.AddSum(rewardAsUint)
   152  					} else {
   153  						rewards[party] = maxPayout
   154  						totalAwardedThisRound.AddSum(num.UintZero().Sub(maxPayout, rewardForParty))
   155  						totalRewardPayout.AddSum(num.UintZero().Sub(maxPayout, rewardForParty))
   156  					}
   157  				}
   158  			}
   159  			roundsRemaining--
   160  
   161  			// if we finished a round without distributing anything, we should stop
   162  			// if this is the final round, stop
   163  			// if the left over is too small for retrying to distribute, stop
   164  			remainingRewardForDelegators = num.UintZero().Sub(remainingRewardForDelegators, totalAwardedThisRound)
   165  			if roundsRemaining == 0 || remainingRewardForDelegators.LT(minLeftOverForDistribution) || totalAwardedThisRound.IsZero() {
   166  				break
   167  			}
   168  		}
   169  	}
   170  
   171  	if totalRewardPayout.GT(rewardBalance) {
   172  		log.Error("The reward payout is greater than the reward balance, this should never happen", logging.String("reward-payout", totalRewardPayout.String()), logging.String("reward-balance", rewardBalance.String()))
   173  	}
   174  
   175  	return &payout{
   176  		fromAccount:   accountID,
   177  		partyToAmount: rewards,
   178  		totalReward:   totalRewardPayout,
   179  		asset:         asset,
   180  		epochSeq:      epochSeq,
   181  	}
   182  }
   183  
   184  func adjustWeights(delegatorWeights map[string]num.Decimal, weightSums num.Decimal, sortedParties []string, decimalOne num.Decimal) {
   185  	// NB: due to rounding errors this sum can be greater than 1
   186  	// to avoid overflow, we choose the one with the highest weight, if that's not sufficient, we'll again choose the one with the highest weight and adjust
   187  	// it marginally until convergence.
   188  
   189  	for weightSums.GreaterThan(decimalOne) {
   190  		precisionError := weightSums.Sub(decimalOne)
   191  		maxWeight := num.DecimalZero()
   192  		maxWeightParty := ""
   193  
   194  		for _, p := range sortedParties {
   195  			if delegatorWeights[p].GreaterThan(maxWeight) {
   196  				maxWeight = delegatorWeights[p]
   197  				maxWeightParty = p
   198  			}
   199  		}
   200  
   201  		delegatorWeights[maxWeightParty] = num.MaxD(num.DecimalZero(), delegatorWeights[maxWeightParty].Sub(precisionError))
   202  		weightSums = num.DecimalZero()
   203  		for _, d := range delegatorWeights {
   204  			weightSums = weightSums.Add(d)
   205  		}
   206  	}
   207  }