github.com/Finschia/finschia-sdk@v0.48.1/x/staking/keeper/slash.go (about)

     1  package keeper
     2  
     3  import (
     4  	"fmt"
     5  
     6  	sdk "github.com/Finschia/finschia-sdk/types"
     7  	types "github.com/Finschia/finschia-sdk/x/staking/types"
     8  )
     9  
    10  // Slash a validator for an infraction committed at a known height
    11  // Find the contributing stake at that height and burn the specified slashFactor
    12  // of it, updating unbonding delegations & redelegations appropriately
    13  //
    14  // CONTRACT:
    15  //
    16  //	slashFactor is non-negative
    17  //
    18  // CONTRACT:
    19  //
    20  //	Infraction was committed equal to or less than an unbonding period in the past,
    21  //	so all unbonding delegations and redelegations from that height are stored
    22  //
    23  // CONTRACT:
    24  //
    25  //	Slash will not slash unbonded validators (for the above reason)
    26  //
    27  // CONTRACT:
    28  //
    29  //	Infraction was committed at the current height or at a past height,
    30  //	not at a height in the future
    31  func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) {
    32  	logger := k.Logger(ctx)
    33  
    34  	if slashFactor.IsNegative() {
    35  		panic(fmt.Errorf("attempted to slash with a negative slash factor: %v", slashFactor))
    36  	}
    37  
    38  	// Amount of slashing = slash slashFactor * power at time of infraction
    39  	amount := k.TokensFromConsensusPower(ctx, power)
    40  	slashAmountDec := amount.ToDec().Mul(slashFactor)
    41  	slashAmount := slashAmountDec.TruncateInt()
    42  
    43  	// ref https://github.com/cosmos/cosmos-sdk/issues/1348
    44  
    45  	validator, found := k.GetValidatorByConsAddr(ctx, consAddr)
    46  	if !found {
    47  		// If not found, the validator must have been overslashed and removed - so we don't need to do anything
    48  		// NOTE:  Correctness dependent on invariant that unbonding delegations / redelegations must also have been completely
    49  		//        slashed in this case - which we don't explicitly check, but should be true.
    50  		// Log the slash attempt for future reference (maybe we should tag it too)
    51  		logger.Error(
    52  			"WARNING: ignored attempt to slash a nonexistent validator; we recommend you investigate immediately",
    53  			"validator", consAddr.String(),
    54  		)
    55  		return
    56  	}
    57  
    58  	// should not be slashing an unbonded validator
    59  	if validator.IsUnbonded() {
    60  		panic(fmt.Sprintf("should not be slashing unbonded validator: %s", validator.GetOperator()))
    61  	}
    62  
    63  	operatorAddress := validator.GetOperator()
    64  
    65  	// call the before-modification hook
    66  	k.BeforeValidatorModified(ctx, operatorAddress)
    67  
    68  	// Track remaining slash amount for the validator
    69  	// This will decrease when we slash unbondings and
    70  	// redelegations, as that stake has since unbonded
    71  	remainingSlashAmount := slashAmount
    72  
    73  	switch {
    74  	case infractionHeight > ctx.BlockHeight():
    75  		// Can't slash infractions in the future
    76  		panic(fmt.Sprintf(
    77  			"impossible attempt to slash future infraction at height %d but we are at height %d",
    78  			infractionHeight, ctx.BlockHeight()))
    79  
    80  	case infractionHeight == ctx.BlockHeight():
    81  		// Special-case slash at current height for efficiency - we don't need to
    82  		// look through unbonding delegations or redelegations.
    83  		logger.Info(
    84  			"slashing at current height; not scanning unbonding delegations & redelegations",
    85  			"height", infractionHeight,
    86  		)
    87  
    88  	case infractionHeight < ctx.BlockHeight():
    89  		// Iterate through unbonding delegations from slashed validator
    90  		unbondingDelegations := k.GetUnbondingDelegationsFromValidator(ctx, operatorAddress)
    91  		for _, unbondingDelegation := range unbondingDelegations {
    92  			amountSlashed := k.SlashUnbondingDelegation(ctx, unbondingDelegation, infractionHeight, slashFactor)
    93  			if amountSlashed.IsZero() {
    94  				continue
    95  			}
    96  
    97  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
    98  		}
    99  
   100  		// Iterate through redelegations from slashed source validator
   101  		redelegations := k.GetRedelegationsFromSrcValidator(ctx, operatorAddress)
   102  		for _, redelegation := range redelegations {
   103  			amountSlashed := k.SlashRedelegation(ctx, validator, redelegation, infractionHeight, slashFactor)
   104  			if amountSlashed.IsZero() {
   105  				continue
   106  			}
   107  
   108  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
   109  		}
   110  	}
   111  
   112  	// cannot decrease balance below zero
   113  	tokensToBurn := sdk.MinInt(remainingSlashAmount, validator.Tokens)
   114  	tokensToBurn = sdk.MaxInt(tokensToBurn, sdk.ZeroInt()) // defensive.
   115  
   116  	// we need to calculate the *effective* slash fraction for distribution
   117  	if validator.Tokens.IsPositive() {
   118  		effectiveFraction := tokensToBurn.ToDec().QuoRoundUp(validator.Tokens.ToDec())
   119  		// possible if power has changed
   120  		if effectiveFraction.GT(sdk.OneDec()) {
   121  			effectiveFraction = sdk.OneDec()
   122  		}
   123  		// call the before-slashed hook
   124  		k.BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction)
   125  	}
   126  
   127  	// Deduct from validator's bonded tokens and update the validator.
   128  	// Burn the slashed tokens from the pool account and decrease the total supply.
   129  	validator = k.RemoveValidatorTokens(ctx, validator, tokensToBurn)
   130  
   131  	switch validator.GetStatus() {
   132  	case types.Bonded:
   133  		if err := k.burnBondedTokens(ctx, tokensToBurn); err != nil {
   134  			panic(err)
   135  		}
   136  	case types.Unbonding, types.Unbonded:
   137  		if err := k.burnNotBondedTokens(ctx, tokensToBurn); err != nil {
   138  			panic(err)
   139  		}
   140  	default:
   141  		panic("invalid validator status")
   142  	}
   143  
   144  	logger.Info(
   145  		"validator slashed by slash factor",
   146  		"validator", validator.GetOperator().String(),
   147  		"slash_factor", slashFactor.String(),
   148  		"burned", tokensToBurn,
   149  	)
   150  }
   151  
   152  // jail a validator
   153  func (k Keeper) Jail(ctx sdk.Context, consAddr sdk.ConsAddress) {
   154  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   155  	k.jailValidator(ctx, validator)
   156  	logger := k.Logger(ctx)
   157  	logger.Info("validator jailed", "validator", consAddr)
   158  }
   159  
   160  // unjail a validator
   161  func (k Keeper) Unjail(ctx sdk.Context, consAddr sdk.ConsAddress) {
   162  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   163  	k.unjailValidator(ctx, validator)
   164  	logger := k.Logger(ctx)
   165  	logger.Info("validator un-jailed", "validator", consAddr)
   166  }
   167  
   168  // slash an unbonding delegation and update the pool
   169  // return the amount that would have been slashed assuming
   170  // the unbonding delegation had enough stake to slash
   171  // (the amount actually slashed may be less if there's
   172  // insufficient stake remaining)
   173  func (k Keeper) SlashUnbondingDelegation(ctx sdk.Context, unbondingDelegation types.UnbondingDelegation,
   174  	infractionHeight int64, slashFactor sdk.Dec,
   175  ) (totalSlashAmount sdk.Int) {
   176  	now := ctx.BlockHeader().Time
   177  	totalSlashAmount = sdk.ZeroInt()
   178  	burnedAmount := sdk.ZeroInt()
   179  
   180  	// perform slashing on all entries within the unbonding delegation
   181  	for i, entry := range unbondingDelegation.Entries {
   182  		// If unbonding started before this height, stake didn't contribute to infraction
   183  		if entry.CreationHeight < infractionHeight {
   184  			continue
   185  		}
   186  
   187  		if entry.IsMature(now) {
   188  			// Unbonding delegation no longer eligible for slashing, skip it
   189  			continue
   190  		}
   191  
   192  		// Calculate slash amount proportional to stake contributing to infraction
   193  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   194  		slashAmount := slashAmountDec.TruncateInt()
   195  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   196  
   197  		// Don't slash more tokens than held
   198  		// Possible since the unbonding delegation may already
   199  		// have been slashed, and slash amounts are calculated
   200  		// according to stake held at time of infraction
   201  		unbondingSlashAmount := sdk.MinInt(slashAmount, entry.Balance)
   202  
   203  		// Update unbonding delegation if necessary
   204  		if unbondingSlashAmount.IsZero() {
   205  			continue
   206  		}
   207  
   208  		burnedAmount = burnedAmount.Add(unbondingSlashAmount)
   209  		entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
   210  		unbondingDelegation.Entries[i] = entry
   211  		k.SetUnbondingDelegation(ctx, unbondingDelegation)
   212  	}
   213  
   214  	if err := k.burnNotBondedTokens(ctx, burnedAmount); err != nil {
   215  		panic(err)
   216  	}
   217  
   218  	return totalSlashAmount
   219  }
   220  
   221  // slash a redelegation and update the pool
   222  // return the amount that would have been slashed assuming
   223  // the unbonding delegation had enough stake to slash
   224  // (the amount actually slashed may be less if there's
   225  // insufficient stake remaining)
   226  // NOTE this is only slashing for prior infractions from the source validator
   227  func (k Keeper) SlashRedelegation(ctx sdk.Context, srcValidator types.Validator, redelegation types.Redelegation,
   228  	infractionHeight int64, slashFactor sdk.Dec,
   229  ) (totalSlashAmount sdk.Int) {
   230  	now := ctx.BlockHeader().Time
   231  	totalSlashAmount = sdk.ZeroInt()
   232  	bondedBurnedAmount, notBondedBurnedAmount := sdk.ZeroInt(), sdk.ZeroInt()
   233  
   234  	// perform slashing on all entries within the redelegation
   235  	for _, entry := range redelegation.Entries {
   236  		// If redelegation started before this height, stake didn't contribute to infraction
   237  		if entry.CreationHeight < infractionHeight {
   238  			continue
   239  		}
   240  
   241  		if entry.IsMature(now) {
   242  			// Redelegation no longer eligible for slashing, skip it
   243  			continue
   244  		}
   245  
   246  		// Calculate slash amount proportional to stake contributing to infraction
   247  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   248  		slashAmount := slashAmountDec.TruncateInt()
   249  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   250  
   251  		// Unbond from target validator
   252  		sharesToUnbond := slashFactor.Mul(entry.SharesDst)
   253  		if sharesToUnbond.IsZero() {
   254  			continue
   255  		}
   256  
   257  		valDstAddr, err := sdk.ValAddressFromBech32(redelegation.ValidatorDstAddress)
   258  		if err != nil {
   259  			panic(err)
   260  		}
   261  
   262  		delegatorAddress := sdk.MustAccAddressFromBech32(redelegation.DelegatorAddress)
   263  
   264  		delegation, found := k.GetDelegation(ctx, delegatorAddress, valDstAddr)
   265  		if !found {
   266  			// If deleted, delegation has zero shares, and we can't unbond any more
   267  			continue
   268  		}
   269  
   270  		if sharesToUnbond.GT(delegation.Shares) {
   271  			sharesToUnbond = delegation.Shares
   272  		}
   273  
   274  		tokensToBurn, err := k.Unbond(ctx, delegatorAddress, valDstAddr, sharesToUnbond)
   275  		if err != nil {
   276  			panic(fmt.Errorf("error unbonding delegator: %v", err))
   277  		}
   278  
   279  		dstValidator, found := k.GetValidator(ctx, valDstAddr)
   280  		if !found {
   281  			panic("destination validator not found")
   282  		}
   283  
   284  		// tokens of a redelegation currently live in the destination validator
   285  		// therefor we must burn tokens from the destination-validator's bonding status
   286  		switch {
   287  		case dstValidator.IsBonded():
   288  			bondedBurnedAmount = bondedBurnedAmount.Add(tokensToBurn)
   289  		case dstValidator.IsUnbonded() || dstValidator.IsUnbonding():
   290  			notBondedBurnedAmount = notBondedBurnedAmount.Add(tokensToBurn)
   291  		default:
   292  			panic("unknown validator status")
   293  		}
   294  	}
   295  
   296  	if err := k.burnBondedTokens(ctx, bondedBurnedAmount); err != nil {
   297  		panic(err)
   298  	}
   299  
   300  	if err := k.burnNotBondedTokens(ctx, notBondedBurnedAmount); err != nil {
   301  		panic(err)
   302  	}
   303  
   304  	return totalSlashAmount
   305  }