package keeper import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/x/staking/types" ) // Slash a validator for an infraction committed at a known height // Find the contributing stake at that height and burn the specified slashFactor // of it, updating unbonding delegations & redelegations appropriately // // CONTRACT: // slashFactor is non-negative // CONTRACT: // Infraction was committed equal to or less than an unbonding period in the past, // so all unbonding delegations and redelegations from that height are stored // CONTRACT: // Slash will not slash unbonded validators (for the above reason) // CONTRACT: // Infraction was committed at the current height or at a past height, // not at a height in the future func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) { logger := k.Logger(ctx) if slashFactor.LT(sdk.ZeroDec()) { panic(fmt.Errorf("attempted to slash with a negative slash factor: %v", slashFactor)) } // Amount of slashing = slash slashFactor * power at time of infraction amount := sdk.TokensFromTendermintPower(power) slashAmountDec := amount.ToDec().Mul(slashFactor) slashAmount := slashAmountDec.TruncateInt() // ref https://github.com/cosmos/cosmos-sdk/issues/1348 validator, found := k.GetValidatorByConsAddr(ctx, consAddr) if !found { // If not found, the validator must have been overslashed and removed - so we don't need to do anything // NOTE: Correctness dependent on invariant that unbonding delegations / redelegations must also have been completely // slashed in this case - which we don't explicitly check, but should be true. // Log the slash attempt for future reference (maybe we should tag it too) logger.Error(fmt.Sprintf( "WARNING: Ignored attempt to slash a nonexistent validator with address %s, we recommend you investigate immediately", consAddr)) return } // should not be slashing an unbonded validator if validator.Status == sdk.Unbonded { panic(fmt.Sprintf("should not be slashing unbonded validator: %s", validator.GetOperator())) } operatorAddress := validator.GetOperator() // call the before-modification hook k.BeforeValidatorModified(ctx, operatorAddress) // Track remaining slash amount for the validator // This will decrease when we slash unbondings and // redelegations, as that stake has since unbonded remainingSlashAmount := slashAmount switch { case infractionHeight > ctx.BlockHeight(): // Can't slash infractions in the future panic(fmt.Sprintf( "impossible attempt to slash future infraction at height %d but we are at height %d", infractionHeight, ctx.BlockHeight())) case infractionHeight == ctx.BlockHeight(): // Special-case slash at current height for efficiency - we don't need to look through unbonding delegations or redelegations logger.Info(fmt.Sprintf( "slashing at current height %d, not scanning unbonding delegations & redelegations", infractionHeight)) case infractionHeight < ctx.BlockHeight(): // Iterate through unbonding delegations from slashed validator unbondingDelegations := k.GetUnbondingDelegationsFromValidator(ctx, operatorAddress) for _, unbondingDelegation := range unbondingDelegations { amountSlashed := k.slashUnbondingDelegation(ctx, unbondingDelegation, infractionHeight, slashFactor) if amountSlashed.IsZero() { continue } remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed) } // Iterate through redelegations from slashed validator redelegations := k.GetRedelegationsFromValidator(ctx, operatorAddress) for _, redelegation := range redelegations { amountSlashed := k.slashRedelegation(ctx, validator, redelegation, infractionHeight, slashFactor) if amountSlashed.IsZero() { continue } remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed) } } // cannot decrease balance below zero tokensToBurn := sdk.MinInt(remainingSlashAmount, validator.Tokens) tokensToBurn = sdk.MaxInt(tokensToBurn, sdk.ZeroInt()) // defensive. // we need to calculate the *effective* slash fraction for distribution if validator.Tokens.GT(sdk.ZeroInt()) { effectiveFraction := tokensToBurn.ToDec().QuoRoundUp(validator.Tokens.ToDec()) // possible if power has changed if effectiveFraction.GT(sdk.OneDec()) { effectiveFraction = sdk.OneDec() } // call the before-slashed hook k.BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction) } // Deduct from validator's bonded tokens and update the validator. // The deducted tokens are returned to pool.NotBondedTokens. // TODO: Move the token accounting outside of `RemoveValidatorTokens` so it is less confusing validator = k.RemoveValidatorTokens(ctx, validator, tokensToBurn) pool := k.GetPool(ctx) // Burn the slashed tokens, which are now loose. pool.NotBondedTokens = pool.NotBondedTokens.Sub(tokensToBurn) k.SetPool(ctx, pool) // Log that a slash occurred! logger.Info(fmt.Sprintf( "validator %s slashed by slash factor of %s; burned %v tokens", validator.GetOperator(), slashFactor.String(), tokensToBurn)) // TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803 return } // jail a validator func (k Keeper) Jail(ctx sdk.Context, consAddr sdk.ConsAddress) { validator := k.mustGetValidatorByConsAddr(ctx, consAddr) k.jailValidator(ctx, validator) logger := k.Logger(ctx) logger.Info(fmt.Sprintf("validator %s jailed", consAddr)) // TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803 return } // unjail a validator func (k Keeper) Unjail(ctx sdk.Context, consAddr sdk.ConsAddress) { validator := k.mustGetValidatorByConsAddr(ctx, consAddr) k.unjailValidator(ctx, validator) logger := k.Logger(ctx) logger.Info(fmt.Sprintf("validator %s unjailed", consAddr)) // TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803 return } // slash an unbonding delegation and update the pool // return the amount that would have been slashed assuming // the unbonding delegation had enough stake to slash // (the amount actually slashed may be less if there's // insufficient stake remaining) func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation types.UnbondingDelegation, infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) { now := ctx.BlockHeader().Time totalSlashAmount = sdk.ZeroInt() // perform slashing on all entries within the unbonding delegation for i, entry := range unbondingDelegation.Entries { // If unbonding started before this height, stake didn't contribute to infraction if entry.CreationHeight < infractionHeight { continue } if entry.IsMature(now) { // Unbonding delegation no longer eligible for slashing, skip it continue } // Calculate slash amount proportional to stake contributing to infraction slashAmountDec := slashFactor.MulInt(entry.InitialBalance) slashAmount := slashAmountDec.TruncateInt() totalSlashAmount = totalSlashAmount.Add(slashAmount) // Don't slash more tokens than held // Possible since the unbonding delegation may already // have been slashed, and slash amounts are calculated // according to stake held at time of infraction unbondingSlashAmount := sdk.MinInt(slashAmount, entry.Balance) // Update unbonding delegation if necessary if unbondingSlashAmount.IsZero() { continue } entry.Balance = entry.Balance.Sub(unbondingSlashAmount) unbondingDelegation.Entries[i] = entry k.SetUnbondingDelegation(ctx, unbondingDelegation) pool := k.GetPool(ctx) // Burn not-bonded tokens // Ref https://github.com/cosmos/cosmos-sdk/pull/1278#discussion_r198657760 pool.NotBondedTokens = pool.NotBondedTokens.Sub(unbondingSlashAmount) k.SetPool(ctx, pool) } return totalSlashAmount } // slash a redelegation and update the pool // return the amount that would have been slashed assuming // the unbonding delegation had enough stake to slash // (the amount actually slashed may be less if there's // insufficient stake remaining) // nolint: unparam func (k Keeper) slashRedelegation(ctx sdk.Context, validator types.Validator, redelegation types.Redelegation, infractionHeight int64, slashFactor sdk.Dec) (totalSlashAmount sdk.Int) { now := ctx.BlockHeader().Time totalSlashAmount = sdk.ZeroInt() // perform slashing on all entries within the redelegation for _, entry := range redelegation.Entries { // If redelegation started before this height, stake didn't contribute to infraction if entry.CreationHeight < infractionHeight { continue } if entry.IsMature(now) { // Redelegation no longer eligible for slashing, skip it continue } // Calculate slash amount proportional to stake contributing to infraction slashAmountDec := slashFactor.MulInt(entry.InitialBalance) slashAmount := slashAmountDec.TruncateInt() totalSlashAmount = totalSlashAmount.Add(slashAmount) // Unbond from target validator sharesToUnbond := slashFactor.Mul(entry.SharesDst) if sharesToUnbond.IsZero() { continue } delegation, found := k.GetDelegation(ctx, redelegation.DelegatorAddress, redelegation.ValidatorDstAddress) if !found { // If deleted, delegation has zero shares, and we can't unbond any more continue } if sharesToUnbond.GT(delegation.Shares) { sharesToUnbond = delegation.Shares } tokensToBurn, err := k.unbond(ctx, redelegation.DelegatorAddress, redelegation.ValidatorDstAddress, sharesToUnbond) if err != nil { panic(fmt.Errorf("error unbonding delegator: %v", err)) } // Burn not-bonded tokens pool := k.GetPool(ctx) pool.NotBondedTokens = pool.NotBondedTokens.Sub(tokensToBurn) k.SetPool(ctx, pool) } return totalSlashAmount }