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  }