328 lines
11 KiB
Go
328 lines
11 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"cosmossdk.io/collections"
|
|
"cosmossdk.io/core/event"
|
|
"cosmossdk.io/math"
|
|
"cosmossdk.io/x/distribution/types"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
)
|
|
|
|
// initialize starting info for a new delegation
|
|
func (k Keeper) initializeDelegation(ctx context.Context, val sdk.ValAddress, del sdk.AccAddress) error {
|
|
// period has already been incremented - we want to store the period ended by this delegation action
|
|
valCurrentRewards, err := k.ValidatorCurrentRewards.Get(ctx, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
previousPeriod := valCurrentRewards.Period - 1
|
|
|
|
// increment reference count for the period we're going to track
|
|
err = k.incrementReferenceCount(ctx, val, previousPeriod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
validator, err := k.stakingKeeper.Validator(ctx, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delegation, err := k.stakingKeeper.Delegation(ctx, del, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// calculate delegation stake in tokens
|
|
// we don't store directly, so multiply delegation shares * (tokens per share)
|
|
// note: necessary to truncate so we don't allow withdrawing more rewards than owed
|
|
stake := validator.TokensFromSharesTruncated(delegation.GetShares())
|
|
headerinfo := k.HeaderService.HeaderInfo(ctx)
|
|
return k.DelegatorStartingInfo.Set(ctx, collections.Join(val, del), types.NewDelegatorStartingInfo(previousPeriod, stake, uint64(headerinfo.Height)))
|
|
}
|
|
|
|
// calculateDelegationRewardsBetween calculates the rewards accrued by a delegation between two periods
|
|
func (k Keeper) calculateDelegationRewardsBetween(ctx context.Context, val sdk.ValidatorI,
|
|
startingPeriod, endingPeriod uint64, stake math.LegacyDec,
|
|
) (sdk.DecCoins, error) {
|
|
// sanity check
|
|
if startingPeriod > endingPeriod {
|
|
return sdk.DecCoins{}, errors.New("startingPeriod cannot be greater than endingPeriod")
|
|
}
|
|
|
|
// sanity check
|
|
if stake.IsNegative() {
|
|
return sdk.DecCoins{}, errors.New("stake should not be negative")
|
|
}
|
|
|
|
valBz, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(val.GetOperator())
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
// return staking * (ending - starting)
|
|
starting, err := k.ValidatorHistoricalRewards.Get(ctx, collections.Join(sdk.ValAddress(valBz), startingPeriod))
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
ending, err := k.ValidatorHistoricalRewards.Get(ctx, collections.Join(sdk.ValAddress(valBz), endingPeriod))
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
difference := ending.CumulativeRewardRatio.Sub(starting.CumulativeRewardRatio)
|
|
if difference.IsAnyNegative() {
|
|
return sdk.DecCoins{}, errors.New("negative rewards should not be possible")
|
|
}
|
|
// note: necessary to truncate so we don't allow withdrawing more rewards than owed
|
|
rewards := difference.MulDecTruncate(stake)
|
|
return rewards, nil
|
|
}
|
|
|
|
// CalculateDelegationRewards calculate the total rewards accrued by a delegation
|
|
func (k Keeper) CalculateDelegationRewards(ctx context.Context, val sdk.ValidatorI, del sdk.DelegationI, endingPeriod uint64) (rewards sdk.DecCoins, err error) {
|
|
addrCodec := k.addrCdc
|
|
delAddr, err := addrCodec.StringToBytes(del.GetDelegatorAddr())
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
valAddr, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(del.GetValidatorAddr())
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
// fetch starting info for delegation
|
|
startingInfo, err := k.DelegatorStartingInfo.Get(ctx, collections.Join(sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)))
|
|
if err != nil && !errors.Is(err, collections.ErrNotFound) {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
headerinfo := k.HeaderService.HeaderInfo(ctx)
|
|
if startingInfo.Height == uint64(headerinfo.Height) { // started this height, no rewards yet
|
|
return sdk.DecCoins{}, nil
|
|
}
|
|
|
|
startingPeriod := startingInfo.PreviousPeriod
|
|
stake := startingInfo.Stake
|
|
|
|
// Iterate through slashes and withdraw with calculated staking for
|
|
// distribution periods. These period offsets are dependent on *when* slashes
|
|
// happen - namely, in BeginBlock, after rewards are allocated...
|
|
// Slashes which happened in the first block would have been before this
|
|
// delegation existed, UNLESS they were slashes of a redelegation to this
|
|
// validator which was itself slashed (from a fault committed by the
|
|
// redelegation source validator) earlier in the same BeginBlock.
|
|
startingHeight := startingInfo.Height
|
|
// Slashes this block happened after reward allocation, but we have to account
|
|
// for them for the stake sanity check below.
|
|
endingHeight := uint64(headerinfo.Height)
|
|
var iterErr error
|
|
if endingHeight > startingHeight {
|
|
err = k.IterateValidatorSlashEventsBetween(ctx, valAddr, startingHeight, endingHeight,
|
|
func(height uint64, event types.ValidatorSlashEvent) (stop bool) {
|
|
endingPeriod := event.ValidatorPeriod
|
|
if endingPeriod > startingPeriod {
|
|
delRewards, err := k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)
|
|
if err != nil {
|
|
iterErr = err
|
|
return true
|
|
}
|
|
rewards = rewards.Add(delRewards...)
|
|
|
|
// Note: It is necessary to truncate so we don't allow withdrawing
|
|
// more rewards than owed.
|
|
stake = stake.MulTruncate(math.LegacyOneDec().Sub(event.Fraction))
|
|
startingPeriod = endingPeriod
|
|
}
|
|
return false
|
|
},
|
|
)
|
|
if iterErr != nil {
|
|
return sdk.DecCoins{}, iterErr
|
|
}
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
}
|
|
|
|
// A total stake sanity check; Recalculated final stake should be less than or
|
|
// equal to current stake here. We cannot use Equals because stake is truncated
|
|
// when multiplied by slash fractions (see above). We could only use equals if
|
|
// we had arbitrary-precision rationals.
|
|
currentStake := val.TokensFromShares(del.GetShares())
|
|
|
|
if stake.GT(currentStake) {
|
|
// AccountI for rounding inconsistencies between:
|
|
//
|
|
// currentStake: calculated as in staking with a single computation
|
|
// stake: calculated as an accumulation of stake
|
|
// calculations across validator's distribution periods
|
|
//
|
|
// These inconsistencies are due to differing order of operations which
|
|
// will inevitably have different accumulated rounding and may lead to
|
|
// the smallest decimal place being one greater in stake than
|
|
// currentStake. When we calculated slashing by period, even if we
|
|
// round down for each slash fraction, it's possible due to how much is
|
|
// being rounded that we slash less when slashing by period instead of
|
|
// for when we slash without periods. In other words, the single slash,
|
|
// and the slashing by period could both be rounding down but the
|
|
// slashing by period is simply rounding down less, thus making stake >
|
|
// currentStake
|
|
//
|
|
// A small amount of this error is tolerated and corrected for,
|
|
// however any greater amount should be considered a breach in expected
|
|
// behavior.
|
|
marginOfErr := math.LegacySmallestDec().MulInt64(3)
|
|
if stake.LTE(currentStake.Add(marginOfErr)) {
|
|
stake = currentStake
|
|
} else {
|
|
return sdk.DecCoins{}, fmt.Errorf("calculated final stake for delegator %s greater than current stake"+
|
|
"\n\tfinal stake:\t%s"+
|
|
"\n\tcurrent stake:\t%s",
|
|
del.GetDelegatorAddr(), stake, currentStake)
|
|
}
|
|
}
|
|
|
|
// calculate rewards for final period
|
|
delRewards, err := k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)
|
|
if err != nil {
|
|
return sdk.DecCoins{}, err
|
|
}
|
|
|
|
rewards = rewards.Add(delRewards...)
|
|
return rewards, nil
|
|
}
|
|
|
|
// withdrawDelegationRewards withdraws the rewards accrued by a delegation.
|
|
func (k Keeper) withdrawDelegationRewards(ctx context.Context, val sdk.ValidatorI, del sdk.DelegationI) (sdk.Coins, error) {
|
|
addrCodec := k.addrCdc
|
|
delAddr, err := addrCodec.StringToBytes(del.GetDelegatorAddr())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
valAddr, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(del.GetValidatorAddr())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check existence of delegator starting info
|
|
hasInfo, err := k.DelegatorStartingInfo.Has(ctx, collections.Join(sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !hasInfo {
|
|
return nil, types.ErrEmptyDelegationDistInfo
|
|
}
|
|
|
|
// end current period and calculate rewards
|
|
endingPeriod, err := k.IncrementValidatorPeriod(ctx, val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rewardsRaw, err := k.CalculateDelegationRewards(ctx, val, del, endingPeriod)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outstanding, err := k.GetValidatorOutstandingRewardsCoins(ctx, sdk.ValAddress(valAddr))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// defensive edge case may happen on the very final digits
|
|
// of the decCoins due to operation order of the distribution mechanism.
|
|
rewards := rewardsRaw.Intersect(outstanding)
|
|
if !rewards.Equal(rewardsRaw) {
|
|
k.Logger.Info(
|
|
"rounding error withdrawing rewards from validator",
|
|
"delegator", del.GetDelegatorAddr(),
|
|
"validator", val.GetOperator(),
|
|
"got", rewards.String(),
|
|
"expected", rewardsRaw.String(),
|
|
)
|
|
}
|
|
|
|
// truncate reward dec coins, return remainder to decimal pool
|
|
finalRewards, remainder := rewards.TruncateDecimal()
|
|
|
|
// add coins to user account
|
|
if !finalRewards.IsZero() {
|
|
withdrawAddr, err := k.GetDelegatorWithdrawAddr(ctx, delAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, finalRewards)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// update the outstanding rewards and the decimal pool only if the transaction was successful
|
|
if err := k.ValidatorOutstandingRewards.Set(ctx, sdk.ValAddress(valAddr), types.ValidatorOutstandingRewards{Rewards: outstanding.Sub(rewards)}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
feePool, err := k.FeePool.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
feePool.DecimalPool = feePool.DecimalPool.Add(remainder...)
|
|
err = k.FeePool.Set(ctx, feePool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// decrement reference count of starting period
|
|
startingInfo, err := k.DelegatorStartingInfo.Get(ctx, collections.Join(sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)))
|
|
if err != nil && !errors.Is(err, collections.ErrNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
startingPeriod := startingInfo.PreviousPeriod
|
|
err = k.decrementReferenceCount(ctx, sdk.ValAddress(valAddr), startingPeriod)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// remove delegator starting info
|
|
err = k.DelegatorStartingInfo.Remove(ctx, collections.Join(sdk.ValAddress(valAddr), sdk.AccAddress(delAddr)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if finalRewards.IsZero() {
|
|
baseDenom, err := k.stakingKeeper.BondDenom(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Note, we do not call the NewCoins constructor as we do not want the zero
|
|
// coin removed.
|
|
finalRewards = sdk.Coins{sdk.NewCoin(baseDenom, math.ZeroInt())}
|
|
}
|
|
|
|
err = k.EventService.EventManager(ctx).EmitKV(
|
|
types.EventTypeWithdrawRewards,
|
|
event.NewAttribute(sdk.AttributeKeyAmount, finalRewards.String()),
|
|
event.NewAttribute(types.AttributeKeyValidator, val.GetOperator()),
|
|
event.NewAttribute(types.AttributeKeyDelegator, del.GetDelegatorAddr()),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return finalRewards, nil
|
|
}
|