Assessment reports>Mitosis>High findings>Slashing bypass through unbonding mechanism
Category: Coding Mistakes

Slashing bypass through unbonding mechanism

High Impact
High Severity
Low Likelihood

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.

L101-L139

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.

L28-L30

// 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.

L36-L40

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.

Zellic © 2025Back to top ↑