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 }