Malicious proposer can skip fee check
Description
When a transaction is submitted to the mempool, the MempoolFeeChecker
is run to ensure that the correct fees have been paid to cover the gas:
func (fc MempoolFeeChecker) CheckTxFeeWithMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return nil, 0, errors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
}
feeCoins := feeTx.GetFee()
gas := feeTx.GetGas()
priority := int64(1)
if ctx.IsCheckTx() {
minGasPrices := ctx.MinGasPrices()
feeValueInBaseUnit := math.ZeroInt()
The issue is that the check is only run on IsCheckTx
, which means it is not run for transactions proposed in a block. A malicious proposer could include a transaction with an arbitrary amount of gas and no fees.
Impact
A malicious proposer could publish a Move module containing an endless loop, then when it is their turn to propose a block, include a transaction with 0x7fffffffffffffff
gas and no fees that executes the function, essentially halting the chain with all the validators stuck in the loop.
In order for a validator to propose a block they must be bonded and have staked enough to have enough consensus power to be chosen as the proposer.
Recommendations
Consider always running the MempoolFeeChecker
; otherwise, ensure that a malicious proposer can be slashed for halting the chain.
Remediation
Initia Labs provided the following response:
I think we can assume this is not that problematic in these two reason. The Cosmos-SDK have one guard (
block_gas_limit
) to prevent this case. We plan to set this to 200_000_000 for testnet, and then the tx spending higher than this will be ignored. Probably we can also make paramstx_gas_limit
to limit the gas consumption of each contract execution. A malicious proposer can not generate a block commonly due to voting power aspect, and if the malicious proposal could be an active proposer that means this proposer invested onINIT
token.