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 }