Skip to content

[F-2026-17736] : Derived call can commit state even when commit=false#15

Merged
0xNilesh merged 2 commits into
audit-fixesfrom
audit/derived-tx-commit-state-issue
Jun 11, 2026
Merged

[F-2026-17736] : Derived call can commit state even when commit=false#15
0xNilesh merged 2 commits into
audit-fixesfrom
audit/derived-tx-commit-state-issue

Conversation

@AryaLanjewar3005

Copy link
Copy Markdown
Collaborator

[F-2026-17736] DerivedEVMCallWithData Ignores commit Flag

Issue

DerivedEVMCallWithData accepts a commit bool parameter but silently ignores it in two places, causing state to always be persisted regardless of what the caller passes.

File: x/vm/keeper/call_evm.go

Bug 1 — Hardcoded true in ApplyMessageWithConfig

// pass true to commit the StateDB
res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, true, cfg, txConfig)

The StateDB (which holds EVM balance changes, storage writes, and newly created contracts) is always told to flush its dirty state to the underlying store, regardless of the commit parameter.

Bug 2 — Unconditional commitState()

if !res.Failed() {
    commitState()
}

The cache context sandbox is always propagated back to the parent context on success, regardless of the commit parameter.

Impact

Any caller passing commit=false expects non-mutating, dry-run behavior — simulations, gas estimations, speculative IBC callback checks. Instead, both the StateDB and the cache context are committed, silently mutating chain state. The bug is invisible at the call site because the caller's context object is not directly modified; the mutation happens at the underlying store level.

There is also an internal contradiction: isFake on the EVM message is correctly set to !commit, so the EVM itself treats the execution as a simulation — but its output is still persisted.


Solution

Two targeted changes in x/vm/keeper/call_evm.go:

1. Pass commit into ApplyMessageWithConfig instead of hardcoded true:

res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, commit, cfg, txConfig)

2. Gate commitState() on the commit flag:

if commit && !res.Failed() {
    commitState()
}

Behavior after fix

commit Execution succeeds StateDB flushed Cache context committed
true yes yes yes
true no yes no
false yes no no
false no no no

When commit=false, execution still runs and the result is returned to the caller, but zero state changes escape to the underlying store. True dry-run semantics are restored.

Closes: #XXXX


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • tackled an existing issue or discussed with a team member
  • left instructions on how to review the changes
  • targeted the main branch

@0xNilesh 0xNilesh self-requested a review June 10, 2026 07:27
…7736)

Asserts a derived call with commit=false does not persist state changes and
commit=true does. Deploys an ERC20 and performs a transfer via DerivedEVMCall,
reading the recipient balance back through the independent CallEVM path so the
assertion reflects committed store state.

Verified: fails against the pre-fix code (state leaked to the store on
commit=false) and passes with the fix.
@github-actions github-actions Bot added the tests label Jun 11, 2026
@0xNilesh 0xNilesh merged commit f7b229f into audit-fixes Jun 11, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants