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