Repository navigation
fix: F-2026-18182 | [Dual Defense] Unbounded UEA Payload Gas Can Stall Synchronous EVM Execution - #49
Merged
Merged
Conversation
CallEVMWithData and DerivedEVMCallWithData returned on res.Failed() before reaching ctx.GasMeter().ConsumeGas(res.GasUsed), so a reverting or deliberately out-of-gas internal call was free at the Cosmos meter. DerivedEVMCallWithData also clamps a caller-supplied gasLimit to config.DefaultGasCap: on an out-of-gas halt res.GasUsed equals the cap, so without the clamp the caller would choose how much gas the enclosing Cosmos tx is forced to consume and could panic it with OutOfGas.
Keeps this branch's IsUint64 rejection and takes the updated DefaultGasCap comment from audit-fixes (#46 made the cap caller-narrowable).
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.
F-2026-18182 — Unbounded UEA Payload Gas Can Stall Synchronous EVM Execution
Severity: Low (Impact 1 / Likelihood 1) · Scope: recommendation 1 only.
What was already fixed, and is not redone here
The bulk of this finding — Hacken's recommendations 2 and 3, a protocol cap on payload gas — is
already delivered by #43, which clamps the caller-supplied budget in
x/vm/keeper/call_evm.go:25M is the confirmed intended limit. This PR does not add a second cap, change
DefaultGasCap,or touch that clamp.
The remaining gap — recommendation 1
gasLimit.Uint64()truncates silently.big.Int.Uint64is documented as undefined for valuesthat do not fit; in practice it returns the low 64 bits.
Node-side, push-chain-node#335
(F-2026-18798) bounds
UniversalPayload.GasLimitto uint256 (BitLen() <= 256) — so everything from2^64 up to 2^256 passes validation and then reaches this call. A payload asking for 2^64 + 5
therefore received 5 gas and failed with a confusing out-of-gas error instead of a clear
rejection.
This is not dangerous: truncation only ever shrinks the value, and the
minclamps whateversurvives. But the semantics are wrong and the error misleads whoever has to debug it. A value can
also truncate upward into a plausible budget —
2^64 + 5_000_000truncates to exactly 5,000,000and would execute cleanly against a budget the caller never asked for.
The fix
Guard with
IsUint64()before calling.Uint64(), and reject with an error that names the problem:IsUint64also rejects a negative limit, for whichUint64is equally undefined.math.MaxUint64exactly still fits, is still accepted, and is still clamped toDefaultGasCapby#43's
min— the guard is invisible to every value that was ever meaningful.CallEVMWithData— checked, out of scopeOn this base
CallEVMWithDatahard-codesGasLimit: config.DefaultGasCapand never calls.Uint64()on the caller's
gasCap, so it cannot truncate. The parameter it ignores is F-2026-18818, fixedseparately in #46 — and that PR already
carries its own
gasCap.BitLen() <= 64guard on the newly-wired value. So there is no second site tofix, on this base or after #46 lands. A grep of the fork confirms
x/vm/keeper/call_evm.go:223is the only.Uint64()on a gas value outside tests.Note the two guards deliberately differ: #46 falls back to the default ceiling, which is fine for
internal keeper callers; this path rejects, because the value is user-controlled and a silent
substitution would hide a malformed payload.
Declined recommendations
max_gas: -1, which is what donut runsMsgExecutePayloadLive traffic
Scanning 600 donut blocks from height 22,075,183: the client declares 50,000,000 gas on every
payload, but observed
gasUsedis 610,477 (two samples, identical) — about 40x below the25M cap. Neither #43's cap nor this guard touches real traffic; the guard only fires on values that
were never executable in the first place.
Tests
Added to
tests/integration/x/vm/test_derived_call.go, reusing #43'sdeployRawCode/gasBurnerSuccessCode/invalidOpcodeCodefixtures:TestDerivedEVMCallWithDataRejectsNonUint64GasLimit— table over2^64,2^64 + 5,2^64 + 5_000_000and2^256 - 1. Each must returnErrInvalidGasLimit, produce no executionresult, and leave the parent gas meter essentially untouched. The
+5_000_000and2^256 - 1cases are the load-bearing ones: they truncate into budgets large enough for the ~2.2M-gas burner
to run to completion, so a regression shows up as a successful call rather than a different error.
TestDerivedEVMCallWithDataAcceptsMaxUint64GasLimit— the boundary:math.MaxUint64fits, is notrejected, and comes back clamped to
DefaultGasCap(asserted via an INVALID opcode, which burnsthe whole frame so
res.GasUsedreports the effective cap).TestDerivedEVMCallWithDataNormalGasLimitUnaffected— the live donut value, 610,477, still works.TestDerivedEVMCallWithDataNilGasLimitUsesEstimation— anillimit still takes the estimationpath and never reaches the guard.
Mutation-verified. With the
IsUint64guard disabled, all four subtests fail, in the twodistinct shapes the finding describes:
The first two show the misleading
intrinsic gas too low; the last two show 2,244,209 gas actuallyburned against a budget the caller never requested. With the guard restored, all four pass.
Test run
evmd'sTestKeeperTestSuitepasses apart fromTestRefundGas/Case_invalid_GasPrice_in_message,which fails identically on clean
audit-fixesand is unrelated to this change.go build ./...isclean. Two
go test ./x/vm/...unit failures (TestExtendedEipsinx/vm/types,TestMintAmountToAccountinx/vm/wrappers) are likewise pre-existing: both packages arebyte-identical to
origin/audit-fixeson this branch and neither importsx/vm/keeper.