Slashing bypass through unbonding mechanism
Description
In Mitosis, when a validator fails to secure sufficient voting power to be included in the validator set that participates in the consensus process, their Bonded field is set to false.
k.IterateLastValidatorPowers(sdkCtx, func(valAddr mitotypes.EthAddress, power int64) bool {
if bondedVals[valAddr] {
// This validator is still bonded in the active set
return false
}
// This validator is no longer bonded in the active set. So we need to unbond it.
validator, found := k.GetValidator(sdkCtx, valAddr)
if !found {
err = errors.Wrap(types.ErrValidatorNotFound, "validator not found for address %s [BUG]", valAddr)
return true
}
// Remove from last validator powers since it's no longer active validator
k.DeleteLastValidatorPower(sdkCtx, valAddr)
// Set the validator as not bonded
validator.Bonded = false
k.SetValidator(sdkCtx, validator)
// Append to validator updates
abciUpdate, err2 := validator.ABCIValidatorUpdateForUnbonding()
if err2 != nil {
err = errors.Wrap(err2, "create abci validator update")
return true
}
validatorUpdates = append(validatorUpdates, abciUpdate)
// Log the removal
k.Logger(sdkCtx).Info("Active Validator Set: Unbonded",
"val_addr", valAddr.String(),
"val_pubkey", fmt.Sprintf("%X", validator.Pubkey),
"cons_addr_hex", fmt.Sprintf("%X", validator.MustConsAddr().Bytes()),
"previous_power", power,
)
return false // continue iteration
})And when the Bonded field is set to false, the IsUnbonded function returns true.
// IsUnbonded implements ValidatorI
func (v Validator) IsUnbonded() bool {
return !v.Bonded
}The problem lies in the fact that when this function returns true, the handleEquivocationEvidence function, which is provided by the Cosmos SDK by default for double-signing punishment, does not execute the actual slashing logic.
func (k Keeper) handleEquivocationEvidence(ctx context.Context, evidence *types.Equivocation) error {
sdkCtx := sdk.UnwrapSDKContext(ctx)
logger := k.Logger(ctx)
consAddr := evidence.GetConsensusAddress(k.stakingKeeper.ConsensusAddressCodec())
validator, err := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr)
if err != nil {
return err
}
if validator == nil || validator.IsUnbonded() {
// Defensive: Simulation doesn't take unbonding periods into account, and
// CometBFT might break this assumption at some point.
return nil
}
...
}Impact
If a malicious validator performs double signing at a specific block and then deliberately chooses to be pushed out of the voting-power ranking, they will not be included in the active validator set when the slashing evidence is submitted, resulting in no slashing despite the evidence being submitted.
Recommendations
Remove the part that calls the IsUnbonded function in the Cosmos SDK evidence module.
Remediation
This issue has been acknowledged by Mitosis, and a fix was implemented in commit 0037f639↗.