github.com/cosmos/cosmos-sdk@v0.50.10/x/staking/keeper/slash.go (about)

     1  package keeper
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"cosmossdk.io/math"
     9  
    10  	sdk "github.com/cosmos/cosmos-sdk/types"
    11  	types "github.com/cosmos/cosmos-sdk/x/staking/types"
    12  )
    13  
    14  // Slash a validator for an infraction committed at a known height
    15  // Find the contributing stake at that height and burn the specified slashFactor
    16  // of it, updating unbonding delegations & redelegations appropriately
    17  //
    18  // CONTRACT:
    19  //
    20  //	slashFactor is non-negative
    21  //
    22  // CONTRACT:
    23  //
    24  //	Infraction was committed equal to or less than an unbonding period in the past,
    25  //	so all unbonding delegations and redelegations from that height are stored
    26  //
    27  // CONTRACT:
    28  //
    29  //	Slash will not slash unbonded validators (for the above reason)
    30  //
    31  // CONTRACT:
    32  //
    33  //	Infraction was committed at the current height or at a past height,
    34  //	but not at a height in the future
    35  func (k Keeper) Slash(ctx context.Context, consAddr sdk.ConsAddress, infractionHeight, power int64, slashFactor math.LegacyDec) (math.Int, error) {
    36  	logger := k.Logger(ctx)
    37  	sdkCtx := sdk.UnwrapSDKContext(ctx)
    38  
    39  	if slashFactor.IsNegative() {
    40  		return math.NewInt(0), fmt.Errorf("attempted to slash with a negative slash factor: %v", slashFactor)
    41  	}
    42  
    43  	// Amount of slashing = slash slashFactor * power at time of infraction
    44  	amount := k.TokensFromConsensusPower(ctx, power)
    45  	slashAmountDec := math.LegacyNewDecFromInt(amount).Mul(slashFactor)
    46  	slashAmount := slashAmountDec.TruncateInt()
    47  
    48  	// ref https://github.com/cosmos/cosmos-sdk/issues/1348
    49  
    50  	validator, err := k.GetValidatorByConsAddr(ctx, consAddr)
    51  	if errors.Is(err, types.ErrNoValidatorFound) {
    52  		// If not found, the validator must have been overslashed and removed - so we don't need to do anything
    53  		// NOTE:  Correctness dependent on invariant that unbonding delegations / redelegations must also have been completely
    54  		//        slashed in this case - which we don't explicitly check, but should be true.
    55  		// Log the slash attempt for future reference (maybe we should tag it too)
    56  		conStr, err := k.consensusAddressCodec.BytesToString(consAddr)
    57  		if err != nil {
    58  			panic(err)
    59  		}
    60  
    61  		logger.Error(
    62  			"WARNING: ignored attempt to slash a nonexistent validator; we recommend you investigate immediately",
    63  			"validator", conStr,
    64  		)
    65  		return math.NewInt(0), nil
    66  	} else if err != nil {
    67  		return math.NewInt(0), err
    68  	}
    69  
    70  	// should not be slashing an unbonded validator
    71  	if validator.IsUnbonded() {
    72  		return math.NewInt(0), fmt.Errorf("should not be slashing unbonded validator: %s", validator.GetOperator())
    73  	}
    74  
    75  	operatorAddress, err := k.ValidatorAddressCodec().StringToBytes(validator.GetOperator())
    76  	if err != nil {
    77  		return math.Int{}, err
    78  	}
    79  
    80  	// call the before-modification hook
    81  	if err := k.Hooks().BeforeValidatorModified(ctx, operatorAddress); err != nil {
    82  		k.Logger(ctx).Error("failed to call before validator modified hook", "error", err)
    83  	}
    84  
    85  	// Track remaining slash amount for the validator
    86  	// This will decrease when we slash unbondings and
    87  	// redelegations, as that stake has since unbonded
    88  	remainingSlashAmount := slashAmount
    89  
    90  	switch {
    91  	case infractionHeight > sdkCtx.BlockHeight():
    92  		// Can't slash infractions in the future
    93  		return math.NewInt(0), fmt.Errorf(
    94  			"impossible attempt to slash future infraction at height %d but we are at height %d",
    95  			infractionHeight, sdkCtx.BlockHeight())
    96  
    97  	case infractionHeight == sdkCtx.BlockHeight():
    98  		// Special-case slash at current height for efficiency - we don't need to
    99  		// look through unbonding delegations or redelegations.
   100  		logger.Info(
   101  			"slashing at current height; not scanning unbonding delegations & redelegations",
   102  			"height", infractionHeight,
   103  		)
   104  
   105  	case infractionHeight < sdkCtx.BlockHeight():
   106  		// Iterate through unbonding delegations from slashed validator
   107  		unbondingDelegations, err := k.GetUnbondingDelegationsFromValidator(ctx, operatorAddress)
   108  		if err != nil {
   109  			return math.NewInt(0), err
   110  		}
   111  
   112  		for _, unbondingDelegation := range unbondingDelegations {
   113  			amountSlashed, err := k.SlashUnbondingDelegation(ctx, unbondingDelegation, infractionHeight, slashFactor)
   114  			if err != nil {
   115  				return math.ZeroInt(), err
   116  			}
   117  			if amountSlashed.IsZero() {
   118  				continue
   119  			}
   120  
   121  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
   122  		}
   123  
   124  		// Iterate through redelegations from slashed source validator
   125  		redelegations, err := k.GetRedelegationsFromSrcValidator(ctx, operatorAddress)
   126  		if err != nil {
   127  			return math.NewInt(0), err
   128  		}
   129  
   130  		for _, redelegation := range redelegations {
   131  			amountSlashed, err := k.SlashRedelegation(ctx, validator, redelegation, infractionHeight, slashFactor)
   132  			if err != nil {
   133  				return math.NewInt(0), err
   134  			}
   135  
   136  			if amountSlashed.IsZero() {
   137  				continue
   138  			}
   139  
   140  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
   141  		}
   142  	}
   143  
   144  	// cannot decrease balance below zero
   145  	tokensToBurn := math.MinInt(remainingSlashAmount, validator.Tokens)
   146  	tokensToBurn = math.MaxInt(tokensToBurn, math.ZeroInt()) // defensive.
   147  
   148  	if tokensToBurn.IsZero() {
   149  		// Nothing to burn, we can end this route immediately! We also don't
   150  		// need to call the k.Hooks().BeforeValidatorSlashed hook as we won't
   151  		// be slashing at all.
   152  		logger.Info(
   153  			"no validator slashing because slash amount is zero",
   154  			"validator", validator.GetOperator(),
   155  			"slash_factor", slashFactor.String(),
   156  			"burned", tokensToBurn,
   157  			"validatorTokens", validator.Tokens,
   158  		)
   159  		return math.NewInt(0), nil
   160  	}
   161  
   162  	// we need to calculate the *effective* slash fraction for distribution
   163  	if validator.Tokens.IsPositive() {
   164  		effectiveFraction := math.LegacyNewDecFromInt(tokensToBurn).QuoRoundUp(math.LegacyNewDecFromInt(validator.Tokens))
   165  		// possible if power has changed
   166  		if oneDec := math.LegacyOneDec(); effectiveFraction.GT(oneDec) {
   167  			effectiveFraction = oneDec
   168  		}
   169  		// call the before-slashed hook
   170  		if err := k.Hooks().BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction); err != nil {
   171  			k.Logger(ctx).Error("failed to call before validator slashed hook", "error", err)
   172  		}
   173  	}
   174  
   175  	// Deduct from validator's bonded tokens and update the validator.
   176  	// Burn the slashed tokens from the pool account and decrease the total supply.
   177  	validator, err = k.RemoveValidatorTokens(ctx, validator, tokensToBurn)
   178  	if err != nil {
   179  		return math.NewInt(0), err
   180  	}
   181  
   182  	switch validator.GetStatus() {
   183  	case types.Bonded:
   184  		if err := k.burnBondedTokens(ctx, tokensToBurn); err != nil {
   185  			return math.NewInt(0), err
   186  		}
   187  	case types.Unbonding, types.Unbonded:
   188  		if err := k.burnNotBondedTokens(ctx, tokensToBurn); err != nil {
   189  			return math.NewInt(0), err
   190  		}
   191  	default:
   192  		panic("invalid validator status")
   193  	}
   194  
   195  	logger.Info(
   196  		"validator slashed by slash factor",
   197  		"validator", validator.GetOperator(),
   198  		"slash_factor", slashFactor.String(),
   199  		"burned", tokensToBurn,
   200  	)
   201  	return tokensToBurn, nil
   202  }
   203  
   204  // SlashWithInfractionReason implementation doesn't require the infraction (types.Infraction) to work but is required by Interchain Security.
   205  func (k Keeper) SlashWithInfractionReason(ctx context.Context, consAddr sdk.ConsAddress, infractionHeight, power int64, slashFactor math.LegacyDec, _ types.Infraction) (math.Int, error) {
   206  	return k.Slash(ctx, consAddr, infractionHeight, power, slashFactor)
   207  }
   208  
   209  // jail a validator
   210  func (k Keeper) Jail(ctx context.Context, consAddr sdk.ConsAddress) error {
   211  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   212  	if err := k.jailValidator(ctx, validator); err != nil {
   213  		return err
   214  	}
   215  
   216  	logger := k.Logger(ctx)
   217  	logger.Info("validator jailed", "validator", consAddr)
   218  	return nil
   219  }
   220  
   221  // unjail a validator
   222  func (k Keeper) Unjail(ctx context.Context, consAddr sdk.ConsAddress) error {
   223  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   224  	if err := k.unjailValidator(ctx, validator); err != nil {
   225  		return err
   226  	}
   227  	logger := k.Logger(ctx)
   228  	logger.Info("validator un-jailed", "validator", consAddr)
   229  	return nil
   230  }
   231  
   232  // slash an unbonding delegation and update the pool
   233  // return the amount that would have been slashed assuming
   234  // the unbonding delegation had enough stake to slash
   235  // (the amount actually slashed may be less if there's
   236  // insufficient stake remaining)
   237  func (k Keeper) SlashUnbondingDelegation(ctx context.Context, unbondingDelegation types.UnbondingDelegation,
   238  	infractionHeight int64, slashFactor math.LegacyDec,
   239  ) (totalSlashAmount math.Int, err error) {
   240  	sdkCtx := sdk.UnwrapSDKContext(ctx)
   241  	now := sdkCtx.BlockHeader().Time
   242  	totalSlashAmount = math.ZeroInt()
   243  	burnedAmount := math.ZeroInt()
   244  
   245  	// perform slashing on all entries within the unbonding delegation
   246  	for i, entry := range unbondingDelegation.Entries {
   247  		// If unbonding started before this height, stake didn't contribute to infraction
   248  		if entry.CreationHeight < infractionHeight {
   249  			continue
   250  		}
   251  
   252  		if entry.IsMature(now) && !entry.OnHold() {
   253  			// Unbonding delegation no longer eligible for slashing, skip it
   254  			continue
   255  		}
   256  
   257  		// Calculate slash amount proportional to stake contributing to infraction
   258  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   259  		slashAmount := slashAmountDec.TruncateInt()
   260  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   261  
   262  		// Don't slash more tokens than held
   263  		// Possible since the unbonding delegation may already
   264  		// have been slashed, and slash amounts are calculated
   265  		// according to stake held at time of infraction
   266  		unbondingSlashAmount := math.MinInt(slashAmount, entry.Balance)
   267  
   268  		// Update unbonding delegation if necessary
   269  		if unbondingSlashAmount.IsZero() {
   270  			continue
   271  		}
   272  
   273  		burnedAmount = burnedAmount.Add(unbondingSlashAmount)
   274  		entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
   275  		unbondingDelegation.Entries[i] = entry
   276  		if err = k.SetUnbondingDelegation(ctx, unbondingDelegation); err != nil {
   277  			return math.ZeroInt(), err
   278  		}
   279  	}
   280  
   281  	if err := k.burnNotBondedTokens(ctx, burnedAmount); err != nil {
   282  		return math.ZeroInt(), err
   283  	}
   284  
   285  	return totalSlashAmount, nil
   286  }
   287  
   288  // slash a redelegation and update the pool
   289  // return the amount that would have been slashed assuming
   290  // the unbonding delegation had enough stake to slash
   291  // (the amount actually slashed may be less if there's
   292  // insufficient stake remaining)
   293  // NOTE this is only slashing for prior infractions from the source validator
   294  func (k Keeper) SlashRedelegation(ctx context.Context, srcValidator types.Validator, redelegation types.Redelegation,
   295  	infractionHeight int64, slashFactor math.LegacyDec,
   296  ) (totalSlashAmount math.Int, err error) {
   297  	sdkCtx := sdk.UnwrapSDKContext(ctx)
   298  	now := sdkCtx.BlockHeader().Time
   299  	totalSlashAmount = math.ZeroInt()
   300  	bondedBurnedAmount, notBondedBurnedAmount := math.ZeroInt(), math.ZeroInt()
   301  
   302  	valDstAddr, err := k.validatorAddressCodec.StringToBytes(redelegation.ValidatorDstAddress)
   303  	if err != nil {
   304  		return math.ZeroInt(), fmt.Errorf("SlashRedelegation: could not parse validator destination address: %w", err)
   305  	}
   306  
   307  	delegatorAddress, err := k.authKeeper.AddressCodec().StringToBytes(redelegation.DelegatorAddress)
   308  	if err != nil {
   309  		return math.ZeroInt(), fmt.Errorf("SlashRedelegation: could not parse delegator address: %w", err)
   310  	}
   311  
   312  	// perform slashing on all entries within the redelegation
   313  	for _, entry := range redelegation.Entries {
   314  		// If redelegation started before this height, stake didn't contribute to infraction
   315  		if entry.CreationHeight < infractionHeight {
   316  			continue
   317  		}
   318  
   319  		if entry.IsMature(now) && !entry.OnHold() {
   320  			// Redelegation no longer eligible for slashing, skip it
   321  			continue
   322  		}
   323  
   324  		// Calculate slash amount proportional to stake contributing to infraction
   325  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   326  		slashAmount := slashAmountDec.TruncateInt()
   327  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   328  
   329  		validatorDstAddress, err := sdk.ValAddressFromBech32(redelegation.ValidatorDstAddress)
   330  		if err != nil {
   331  			panic(err)
   332  		}
   333  		// Handle undelegation after redelegation
   334  		// Prioritize slashing unbondingDelegation than delegation
   335  		unbondingDelegation, err := k.GetUnbondingDelegation(ctx, sdk.MustAccAddressFromBech32(redelegation.DelegatorAddress), validatorDstAddress)
   336  		if err == nil {
   337  			for i, entry := range unbondingDelegation.Entries {
   338  				// slash with the amount of `slashAmount` if possible, else slash all unbonding token
   339  				unbondingSlashAmount := math.MinInt(slashAmount, entry.Balance)
   340  
   341  				switch {
   342  				// There's no token to slash
   343  				case unbondingSlashAmount.IsZero():
   344  					continue
   345  				// If unbonding started before this height, stake didn't contribute to infraction
   346  				case entry.CreationHeight < infractionHeight:
   347  					continue
   348  				// Unbonding delegation no longer eligible for slashing, skip it
   349  				case entry.IsMature(now) && !entry.OnHold():
   350  					continue
   351  				// Slash the unbonding delegation
   352  				default:
   353  					// update remaining slashAmount
   354  					slashAmount = slashAmount.Sub(unbondingSlashAmount)
   355  
   356  					notBondedBurnedAmount = notBondedBurnedAmount.Add(unbondingSlashAmount)
   357  					entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
   358  					unbondingDelegation.Entries[i] = entry
   359  					if err = k.SetUnbondingDelegation(ctx, unbondingDelegation); err != nil {
   360  						return math.ZeroInt(), err
   361  					}
   362  				}
   363  			}
   364  		}
   365  
   366  		// Slash the moved delegation
   367  
   368  		// Unbond from target validator
   369  		sharesToUnbond := slashFactor.Mul(entry.SharesDst)
   370  		if sharesToUnbond.IsZero() || slashAmount.IsZero() {
   371  			continue
   372  		}
   373  
   374  		delegation, err := k.GetDelegation(ctx, delegatorAddress, valDstAddr)
   375  		if err != nil {
   376  			// If deleted, delegation has zero shares, and we can't unbond any more
   377  			continue
   378  		}
   379  
   380  		if sharesToUnbond.GT(delegation.Shares) {
   381  			sharesToUnbond = delegation.Shares
   382  		}
   383  
   384  		tokensToBurn, err := k.Unbond(ctx, delegatorAddress, valDstAddr, sharesToUnbond)
   385  		if err != nil {
   386  			return math.ZeroInt(), err
   387  		}
   388  
   389  		dstValidator, err := k.GetValidator(ctx, valDstAddr)
   390  		if err != nil {
   391  			return math.ZeroInt(), err
   392  		}
   393  
   394  		// tokens of a redelegation currently live in the destination validator
   395  		// therefor we must burn tokens from the destination-validator's bonding status
   396  		switch {
   397  		case dstValidator.IsBonded():
   398  			bondedBurnedAmount = bondedBurnedAmount.Add(tokensToBurn)
   399  		case dstValidator.IsUnbonded() || dstValidator.IsUnbonding():
   400  			notBondedBurnedAmount = notBondedBurnedAmount.Add(tokensToBurn)
   401  		default:
   402  			panic("unknown validator status")
   403  		}
   404  	}
   405  
   406  	if err := k.burnBondedTokens(ctx, bondedBurnedAmount); err != nil {
   407  		return math.ZeroInt(), err
   408  	}
   409  
   410  	if err := k.burnNotBondedTokens(ctx, notBondedBurnedAmount); err != nil {
   411  		return math.ZeroInt(), err
   412  	}
   413  
   414  	return totalSlashAmount, nil
   415  }