fix: F-2026-18816 | [Dual Defense] Empty authz.MsgExec Vacuously Gasless Can Stuff Declared Block Gas - #332
Merged
Merged
Conversation
An empty inner message list passed the allowlist loop vacuously, making the tx gasless and skipping the fee and min-gas-price decorators.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
IsGaslessTxtreats anauthz.MsgExecas gasless when every inner message is allowlisted. WithMsgs: []that loop runs zero times, falls through, and the function returnstrueby vacuous truth —so a zero-fee tx skips
DeductFeeDecoratorandMinGasPriceDecorator.The outer message list already had exactly this guard six lines earlier (
if len(msgs) == 0 { return false });only the nested list was missed. This PR adds the matching guard to the nested list.
That is the entire production change (5 lines in
app/txpolicy/gasless.go).Why nothing upstream catches it
authz.MsgExechas noValidateBasicin SDK v0.53.7 (onlyUnpackInterfaces/GetMessages),so
ante.NewValidateBasicDecorator()cannot reject it.x/authz/keeper/msg_server.go— that is DeliverTx, i.e. after thefee decorators have already been skipped.
AuthzLimiterDecoratorfilters onlyMsgEthereumTxandMsgCreateVestingAccountinside aMsgExec.Impact assessment — Hacken's headline does not apply to Push
Hacken's stated impact is block-gas censorship via declared gas. That vector needs a finite
max_gasto monopolise. It does not apply here:max_gas: "-1".update_max_block_gas.jsongovernance proposal also setsmax_gas: "-1"— unlimited isdeliberate, not drift.
With no block gas budget, declared gas cannot monopolise block capacity, so the censorship scenario is moot.
What is real, and what this PR fixes:
max_bytes(22020096, ~21 MiB) ratherthan by fees. This is the substantive issue and it is closed by the guard.
app/ante/account_init_decorator.go:34gates on this sameIsGaslessTx, so a vacuously-gasless tx also gets a free on-chain account created for its signer(permanent state bloat, and notably no finite
max_gasrequired).Honest caveat on (2): this account-init amplifier is largely shared with the gasless account-init
behaviour already tested and accepted under F-2026-18200. An empty
MsgExecmakes it cheaper totrigger, not newly possible — it is not an independent new capability.
Tests
Added to the existing
app/txpolicy/gasless_test.go, in its existing style:TestIsGaslessTxAuthzExecNesting(table):nil) → not gasless (the regression case)MsgExecnested inside aMsgExec→ not gasless — confirmed:/cosmos.authz.v1beta1.MsgExecis not itself in the allowlist, so the inner
Any's TypeURL check rejects it and there is norecursion into a vacuous pass
TestIsGaslessTxEmptyExecAlongsideAllowedMsg— an emptyMsgExecsitting next to an allowlistedsibling message must still disqualify the whole tx.
Mutation-verified
With the
len(m.Msgs) == 0guard removed, exactly the three empty-nest assertions fail and everyunchanged-behaviour case still passes:
Guard restored, all pass.
Deliberately out of scope
MsgExecin the ante chainmax_gasever goes finitePrepareProposalmax_gas: -1GaslessMsgTypesmembership is untouched.