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  }