github.com/cosmos/cosmos-sdk@v0.50.10/x/distribution/keeper/delegation.go (about) 1 package keeper 2 3 import ( 4 "context" 5 "fmt" 6 7 "cosmossdk.io/math" 8 9 sdk "github.com/cosmos/cosmos-sdk/types" 10 "github.com/cosmos/cosmos-sdk/x/distribution/types" 11 stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 12 ) 13 14 // initialize starting info for a new delegation 15 func (k Keeper) initializeDelegation(ctx context.Context, val sdk.ValAddress, del sdk.AccAddress) error { 16 // period has already been incremented - we want to store the period ended by this delegation action 17 valCurrentRewards, err := k.GetValidatorCurrentRewards(ctx, val) 18 if err != nil { 19 return err 20 } 21 previousPeriod := valCurrentRewards.Period - 1 22 23 // increment reference count for the period we're going to track 24 err = k.incrementReferenceCount(ctx, val, previousPeriod) 25 if err != nil { 26 return err 27 } 28 29 validator, err := k.stakingKeeper.Validator(ctx, val) 30 if err != nil { 31 return err 32 } 33 34 delegation, err := k.stakingKeeper.Delegation(ctx, del, val) 35 if err != nil { 36 return err 37 } 38 39 // calculate delegation stake in tokens 40 // we don't store directly, so multiply delegation shares * (tokens per share) 41 // note: necessary to truncate so we don't allow withdrawing more rewards than owed 42 stake := validator.TokensFromSharesTruncated(delegation.GetShares()) 43 sdkCtx := sdk.UnwrapSDKContext(ctx) 44 return k.SetDelegatorStartingInfo(ctx, val, del, types.NewDelegatorStartingInfo(previousPeriod, stake, uint64(sdkCtx.BlockHeight()))) 45 } 46 47 // calculate the rewards accrued by a delegation between two periods 48 func (k Keeper) calculateDelegationRewardsBetween(ctx context.Context, val stakingtypes.ValidatorI, 49 startingPeriod, endingPeriod uint64, stake math.LegacyDec, 50 ) (sdk.DecCoins, error) { 51 // sanity check 52 if startingPeriod > endingPeriod { 53 panic("startingPeriod cannot be greater than endingPeriod") 54 } 55 56 // sanity check 57 if stake.IsNegative() { 58 panic("stake should not be negative") 59 } 60 61 valBz, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(val.GetOperator()) 62 if err != nil { 63 panic(err) 64 } 65 66 // return staking * (ending - starting) 67 starting, err := k.GetValidatorHistoricalRewards(ctx, valBz, startingPeriod) 68 if err != nil { 69 return sdk.DecCoins{}, err 70 } 71 72 ending, err := k.GetValidatorHistoricalRewards(ctx, valBz, endingPeriod) 73 if err != nil { 74 return sdk.DecCoins{}, err 75 } 76 77 difference := ending.CumulativeRewardRatio.Sub(starting.CumulativeRewardRatio) 78 if difference.IsAnyNegative() { 79 panic("negative rewards should not be possible") 80 } 81 // note: necessary to truncate so we don't allow withdrawing more rewards than owed 82 rewards := difference.MulDecTruncate(stake) 83 return rewards, nil 84 } 85 86 // calculate the total rewards accrued by a delegation 87 func (k Keeper) CalculateDelegationRewards(ctx context.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI, endingPeriod uint64) (rewards sdk.DecCoins, err error) { 88 addrCodec := k.authKeeper.AddressCodec() 89 delAddr, err := addrCodec.StringToBytes(del.GetDelegatorAddr()) 90 if err != nil { 91 return sdk.DecCoins{}, err 92 } 93 94 valAddr, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(del.GetValidatorAddr()) 95 if err != nil { 96 return sdk.DecCoins{}, err 97 } 98 99 // fetch starting info for delegation 100 startingInfo, err := k.GetDelegatorStartingInfo(ctx, sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)) 101 if err != nil { 102 return 103 } 104 105 sdkCtx := sdk.UnwrapSDKContext(ctx) 106 if startingInfo.Height == uint64(sdkCtx.BlockHeight()) { 107 // started this height, no rewards yet 108 return 109 } 110 111 startingPeriod := startingInfo.PreviousPeriod 112 stake := startingInfo.Stake 113 114 // Iterate through slashes and withdraw with calculated staking for 115 // distribution periods. These period offsets are dependent on *when* slashes 116 // happen - namely, in BeginBlock, after rewards are allocated... 117 // Slashes which happened in the first block would have been before this 118 // delegation existed, UNLESS they were slashes of a redelegation to this 119 // validator which was itself slashed (from a fault committed by the 120 // redelegation source validator) earlier in the same BeginBlock. 121 startingHeight := startingInfo.Height 122 // Slashes this block happened after reward allocation, but we have to account 123 // for them for the stake sanity check below. 124 endingHeight := uint64(sdkCtx.BlockHeight()) 125 if endingHeight > startingHeight { 126 k.IterateValidatorSlashEventsBetween(ctx, valAddr, startingHeight, endingHeight, 127 func(height uint64, event types.ValidatorSlashEvent) (stop bool) { 128 endingPeriod := event.ValidatorPeriod 129 if endingPeriod > startingPeriod { 130 delRewards, err := k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake) 131 if err != nil { 132 panic(err) 133 } 134 rewards = rewards.Add(delRewards...) 135 136 // Note: It is necessary to truncate so we don't allow withdrawing 137 // more rewards than owed. 138 stake = stake.MulTruncate(math.LegacyOneDec().Sub(event.Fraction)) 139 startingPeriod = endingPeriod 140 } 141 return false 142 }, 143 ) 144 } 145 146 // A total stake sanity check; Recalculated final stake should be less than or 147 // equal to current stake here. We cannot use Equals because stake is truncated 148 // when multiplied by slash fractions (see above). We could only use equals if 149 // we had arbitrary-precision rationals. 150 currentStake := val.TokensFromShares(del.GetShares()) 151 152 if stake.GT(currentStake) { 153 // AccountI for rounding inconsistencies between: 154 // 155 // currentStake: calculated as in staking with a single computation 156 // stake: calculated as an accumulation of stake 157 // calculations across validator's distribution periods 158 // 159 // These inconsistencies are due to differing order of operations which 160 // will inevitably have different accumulated rounding and may lead to 161 // the smallest decimal place being one greater in stake than 162 // currentStake. When we calculated slashing by period, even if we 163 // round down for each slash fraction, it's possible due to how much is 164 // being rounded that we slash less when slashing by period instead of 165 // for when we slash without periods. In other words, the single slash, 166 // and the slashing by period could both be rounding down but the 167 // slashing by period is simply rounding down less, thus making stake > 168 // currentStake 169 // 170 // A small amount of this error is tolerated and corrected for, 171 // however any greater amount should be considered a breach in expected 172 // behavior. 173 marginOfErr := math.LegacySmallestDec().MulInt64(3) 174 if stake.LTE(currentStake.Add(marginOfErr)) { 175 stake = currentStake 176 } else { 177 panic(fmt.Sprintf("calculated final stake for delegator %s greater than current stake"+ 178 "\n\tfinal stake:\t%s"+ 179 "\n\tcurrent stake:\t%s", 180 del.GetDelegatorAddr(), stake, currentStake)) 181 } 182 } 183 184 // calculate rewards for final period 185 delRewards, err := k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake) 186 if err != nil { 187 return sdk.DecCoins{}, err 188 } 189 190 rewards = rewards.Add(delRewards...) 191 return rewards, nil 192 } 193 194 func (k Keeper) withdrawDelegationRewards(ctx context.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI) (sdk.Coins, error) { 195 addrCodec := k.authKeeper.AddressCodec() 196 delAddr, err := addrCodec.StringToBytes(del.GetDelegatorAddr()) 197 if err != nil { 198 return nil, err 199 } 200 201 valAddr, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(del.GetValidatorAddr()) 202 if err != nil { 203 return nil, err 204 } 205 206 // check existence of delegator starting info 207 hasInfo, err := k.HasDelegatorStartingInfo(ctx, sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)) 208 if err != nil { 209 return nil, err 210 } 211 212 if !hasInfo { 213 return nil, types.ErrEmptyDelegationDistInfo 214 } 215 216 // end current period and calculate rewards 217 endingPeriod, err := k.IncrementValidatorPeriod(ctx, val) 218 if err != nil { 219 return nil, err 220 } 221 222 rewardsRaw, err := k.CalculateDelegationRewards(ctx, val, del, endingPeriod) 223 if err != nil { 224 return nil, err 225 } 226 227 outstanding, err := k.GetValidatorOutstandingRewardsCoins(ctx, sdk.ValAddress(valAddr)) 228 if err != nil { 229 return nil, err 230 } 231 232 // defensive edge case may happen on the very final digits 233 // of the decCoins due to operation order of the distribution mechanism. 234 rewards := rewardsRaw.Intersect(outstanding) 235 if !rewards.Equal(rewardsRaw) { 236 logger := k.Logger(ctx) 237 logger.Info( 238 "rounding error withdrawing rewards from validator", 239 "delegator", del.GetDelegatorAddr(), 240 "validator", val.GetOperator(), 241 "got", rewards.String(), 242 "expected", rewardsRaw.String(), 243 ) 244 } 245 246 // truncate reward dec coins, return remainder to community pool 247 finalRewards, remainder := rewards.TruncateDecimal() 248 249 // add coins to user account 250 if !finalRewards.IsZero() { 251 withdrawAddr, err := k.GetDelegatorWithdrawAddr(ctx, delAddr) 252 if err != nil { 253 return nil, err 254 } 255 256 err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, finalRewards) 257 if err != nil { 258 return nil, err 259 } 260 } 261 262 // update the outstanding rewards and the community pool only if the 263 // transaction was successful 264 err = k.SetValidatorOutstandingRewards(ctx, sdk.ValAddress(valAddr), types.ValidatorOutstandingRewards{Rewards: outstanding.Sub(rewards)}) 265 if err != nil { 266 return nil, err 267 } 268 269 feePool, err := k.FeePool.Get(ctx) 270 if err != nil { 271 return nil, err 272 } 273 274 feePool.CommunityPool = feePool.CommunityPool.Add(remainder...) 275 err = k.FeePool.Set(ctx, feePool) 276 if err != nil { 277 return nil, err 278 } 279 280 // decrement reference count of starting period 281 startingInfo, err := k.GetDelegatorStartingInfo(ctx, sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)) 282 if err != nil { 283 return nil, err 284 } 285 286 startingPeriod := startingInfo.PreviousPeriod 287 err = k.decrementReferenceCount(ctx, sdk.ValAddress(valAddr), startingPeriod) 288 if err != nil { 289 return nil, err 290 } 291 292 // remove delegator starting info 293 err = k.DeleteDelegatorStartingInfo(ctx, sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)) 294 if err != nil { 295 return nil, err 296 } 297 298 if finalRewards.IsZero() { 299 baseDenom, _ := sdk.GetBaseDenom() 300 if baseDenom == "" { 301 baseDenom = sdk.DefaultBondDenom 302 } 303 304 // Note, we do not call the NewCoins constructor as we do not want the zero 305 // coin removed. 306 finalRewards = sdk.Coins{sdk.NewCoin(baseDenom, math.ZeroInt())} 307 } 308 309 sdkCtx := sdk.UnwrapSDKContext(ctx) 310 sdkCtx.EventManager().EmitEvent( 311 sdk.NewEvent( 312 types.EventTypeWithdrawRewards, 313 sdk.NewAttribute(sdk.AttributeKeyAmount, finalRewards.String()), 314 sdk.NewAttribute(types.AttributeKeyValidator, val.GetOperator()), 315 sdk.NewAttribute(types.AttributeKeyDelegator, del.GetDelegatorAddr()), 316 ), 317 ) 318 319 return finalRewards, nil 320 }