github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/libs/cosmos-sdk/x/staking/keeper/slash.go (about)

     1  package keeper
     2  
     3  import (
     4  	"fmt"
     5  
     6  	sdk "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types"
     7  	types "github.com/fibonacci-chain/fbc/libs/cosmos-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 := sdk.TokensFromConsensusPower(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(fmt.Sprintf(
    52  			"WARNING: Ignored attempt to slash a nonexistent validator with address %s, we recommend you investigate immediately",
    53  			consAddr))
    54  		return
    55  	}
    56  
    57  	// should not be slashing an unbonded validator
    58  	if validator.IsUnbonded() {
    59  		panic(fmt.Sprintf("should not be slashing unbonded validator: %s", validator.GetOperator()))
    60  	}
    61  
    62  	operatorAddress := validator.GetOperator()
    63  
    64  	// call the before-modification hook
    65  	k.BeforeValidatorModified(ctx, operatorAddress)
    66  
    67  	// Track remaining slash amount for the validator
    68  	// This will decrease when we slash unbondings and
    69  	// redelegations, as that stake has since unbonded
    70  	remainingSlashAmount := slashAmount
    71  
    72  	switch {
    73  	case infractionHeight > ctx.BlockHeight():
    74  
    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  
    82  		// Special-case slash at current height for efficiency - we don't need to look through unbonding delegations or redelegations
    83  		logger.Info(fmt.Sprintf(
    84  			"slashing at current height %d, not scanning unbonding delegations & redelegations",
    85  			infractionHeight))
    86  
    87  	case infractionHeight < ctx.BlockHeight():
    88  
    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  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
    97  		}
    98  
    99  		// Iterate through redelegations from slashed source validator
   100  		redelegations := k.GetRedelegationsFromSrcValidator(ctx, operatorAddress)
   101  		for _, redelegation := range redelegations {
   102  			amountSlashed := k.slashRedelegation(ctx, validator, redelegation, infractionHeight, slashFactor)
   103  			if amountSlashed.IsZero() {
   104  				continue
   105  			}
   106  			remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed)
   107  		}
   108  	}
   109  
   110  	// cannot decrease balance below zero
   111  	tokensToBurn := sdk.MinInt(remainingSlashAmount, validator.Tokens)
   112  	tokensToBurn = sdk.MaxInt(tokensToBurn, sdk.ZeroInt()) // defensive.
   113  
   114  	// we need to calculate the *effective* slash fraction for distribution
   115  	if validator.Tokens.IsPositive() {
   116  		effectiveFraction := tokensToBurn.ToDec().QuoRoundUp(validator.Tokens.ToDec())
   117  		// possible if power has changed
   118  		if effectiveFraction.GT(sdk.OneDec()) {
   119  			effectiveFraction = sdk.OneDec()
   120  		}
   121  		// call the before-slashed hook
   122  		k.BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction)
   123  	}
   124  
   125  	// Deduct from validator's bonded tokens and update the validator.
   126  	// Burn the slashed tokens from the pool account and decrease the total supply.
   127  	validator = k.RemoveValidatorTokens(ctx, validator, tokensToBurn)
   128  
   129  	switch validator.GetStatus() {
   130  	case sdk.Bonded:
   131  		if err := k.burnBondedTokens(ctx, tokensToBurn); err != nil {
   132  			panic(err)
   133  		}
   134  	case sdk.Unbonding, sdk.Unbonded:
   135  		if err := k.burnNotBondedTokens(ctx, tokensToBurn); err != nil {
   136  			panic(err)
   137  		}
   138  	default:
   139  		panic("invalid validator status")
   140  	}
   141  
   142  	// Log that a slash occurred!
   143  	logger.Info(fmt.Sprintf(
   144  		"validator %s slashed by slash factor of %s; burned %v tokens",
   145  		validator.GetOperator(), slashFactor.String(), tokensToBurn))
   146  
   147  }
   148  
   149  // jail a validator
   150  func (k Keeper) Jail(ctx sdk.Context, consAddr sdk.ConsAddress) {
   151  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   152  	k.jailValidator(ctx, validator)
   153  	logger := k.Logger(ctx)
   154  	logger.Info(fmt.Sprintf("validator %s jailed", consAddr))
   155  }
   156  
   157  // unjail a validator
   158  func (k Keeper) Unjail(ctx sdk.Context, consAddr sdk.ConsAddress) {
   159  	validator := k.mustGetValidatorByConsAddr(ctx, consAddr)
   160  	k.unjailValidator(ctx, validator)
   161  	logger := k.Logger(ctx)
   162  	logger.Info(fmt.Sprintf("validator %s unjailed", consAddr))
   163  }
   164  
   165  // slash an unbonding delegation and update the pool
   166  // return the amount that would have been slashed assuming
   167  // the unbonding delegation had enough stake to slash
   168  // (the amount actually slashed may be less if there's
   169  // insufficient stake remaining)
   170  func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation types.UnbondingDelegation,
   171  	infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) {
   172  
   173  	now := ctx.BlockHeader().Time
   174  	totalSlashAmount = sdk.ZeroInt()
   175  	burnedAmount := sdk.ZeroInt()
   176  
   177  	// perform slashing on all entries within the unbonding delegation
   178  	for i, entry := range unbondingDelegation.Entries {
   179  
   180  		// If unbonding started before this height, stake didn't contribute to infraction
   181  		if entry.CreationHeight < infractionHeight {
   182  			continue
   183  		}
   184  
   185  		if entry.IsMature(now) {
   186  			// Unbonding delegation no longer eligible for slashing, skip it
   187  			continue
   188  		}
   189  
   190  		// Calculate slash amount proportional to stake contributing to infraction
   191  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   192  		slashAmount := slashAmountDec.TruncateInt()
   193  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   194  
   195  		// Don't slash more tokens than held
   196  		// Possible since the unbonding delegation may already
   197  		// have been slashed, and slash amounts are calculated
   198  		// according to stake held at time of infraction
   199  		unbondingSlashAmount := sdk.MinInt(slashAmount, entry.Balance)
   200  
   201  		// Update unbonding delegation if necessary
   202  		if unbondingSlashAmount.IsZero() {
   203  			continue
   204  		}
   205  
   206  		burnedAmount = burnedAmount.Add(unbondingSlashAmount)
   207  		entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
   208  		unbondingDelegation.Entries[i] = entry
   209  		k.SetUnbondingDelegation(ctx, unbondingDelegation)
   210  	}
   211  
   212  	if err := k.burnNotBondedTokens(ctx, burnedAmount); err != nil {
   213  		panic(err)
   214  	}
   215  
   216  	return totalSlashAmount
   217  }
   218  
   219  // slash a redelegation and update the pool
   220  // return the amount that would have been slashed assuming
   221  // the unbonding delegation had enough stake to slash
   222  // (the amount actually slashed may be less if there's
   223  // insufficient stake remaining)
   224  // NOTE this is only slashing for prior infractions from the source validator
   225  func (k Keeper) slashRedelegation(ctx sdk.Context, srcValidator types.Validator, redelegation types.Redelegation,
   226  	infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) {
   227  
   228  	now := ctx.BlockHeader().Time
   229  	totalSlashAmount = sdk.ZeroInt()
   230  	bondedBurnedAmount, notBondedBurnedAmount := sdk.ZeroInt(), sdk.ZeroInt()
   231  
   232  	// perform slashing on all entries within the redelegation
   233  	for _, entry := range redelegation.Entries {
   234  
   235  		// If redelegation started before this height, stake didn't contribute to infraction
   236  		if entry.CreationHeight < infractionHeight {
   237  			continue
   238  		}
   239  
   240  		if entry.IsMature(now) {
   241  			// Redelegation no longer eligible for slashing, skip it
   242  			continue
   243  		}
   244  
   245  		// Calculate slash amount proportional to stake contributing to infraction
   246  		slashAmountDec := slashFactor.MulInt(entry.InitialBalance)
   247  		slashAmount := slashAmountDec.TruncateInt()
   248  		totalSlashAmount = totalSlashAmount.Add(slashAmount)
   249  
   250  		// Unbond from target validator
   251  		sharesToUnbond := slashFactor.Mul(entry.SharesDst)
   252  		if sharesToUnbond.IsZero() {
   253  			continue
   254  		}
   255  		delegation, found := k.GetDelegation(ctx, redelegation.DelegatorAddress, redelegation.ValidatorDstAddress)
   256  		if !found {
   257  			// If deleted, delegation has zero shares, and we can't unbond any more
   258  			continue
   259  		}
   260  		if sharesToUnbond.GT(delegation.Shares) {
   261  			sharesToUnbond = delegation.Shares
   262  		}
   263  
   264  		tokensToBurn, err := k.unbond(ctx, redelegation.DelegatorAddress, redelegation.ValidatorDstAddress, sharesToUnbond)
   265  		if err != nil {
   266  			panic(fmt.Errorf("error unbonding delegator: %v", err))
   267  		}
   268  
   269  		dstValidator, found := k.GetValidator(ctx, redelegation.ValidatorDstAddress)
   270  		if !found {
   271  			panic("destination validator not found")
   272  		}
   273  
   274  		// tokens of a redelegation currently live in the destination validator
   275  		// therefor we must burn tokens from the destination-validator's bonding status
   276  		switch {
   277  		case dstValidator.IsBonded():
   278  			bondedBurnedAmount = bondedBurnedAmount.Add(tokensToBurn)
   279  		case dstValidator.IsUnbonded() || dstValidator.IsUnbonding():
   280  			notBondedBurnedAmount = notBondedBurnedAmount.Add(tokensToBurn)
   281  		default:
   282  			panic("unknown validator status")
   283  		}
   284  	}
   285  
   286  	if err := k.burnBondedTokens(ctx, bondedBurnedAmount); err != nil {
   287  		panic(err)
   288  	}
   289  
   290  	if err := k.burnNotBondedTokens(ctx, notBondedBurnedAmount); err != nil {
   291  		panic(err)
   292  	}
   293  
   294  	return totalSlashAmount
   295  }