Skip to content

fix(evm): align debug_trace fee with virtual ante path#967

Open
thomas-nguy wants to merge 1 commit into
crypto-org-chain:developfrom
thomas-nguy:thomas/debug-trace-balance-issue
Open

fix(evm): align debug_trace fee with virtual ante path#967
thomas-nguy wants to merge 1 commit into
crypto-org-chain:developfrom
thomas-nguy:thomas/debug-trace-balance-issue

Conversation

@thomas-nguy
Copy link
Copy Markdown

Closes: #XXX

Description

Debug trace used StateDB burn/refund for gasLimit×gasPrice, which
diverged from CheckEthGasConsume (virtual fee collector) and could
RPC-fail with bank insufficient funds on txs that actually executed.
Use DeductTxCostsFromUserBalance + RefundGas when DebugTrace is set,
compute effective gas price for EIP-1559 holds, and parse TraceTx
BaseFee via BigInt to avoid truncation.


For contributor use:

  • Targeted PR against correct branch (see CONTRIBUTING.md)
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the module structure standards.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/)
  • Added relevant godoc comments.
  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer

For admin use:

  • Added appropriate labels to PR (ex. WIP, R4R, docs, etc)
  • Reviewers assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@thomas-nguy thomas-nguy requested a review from a team as a code owner May 27, 2026 08:38
@thomas-nguy thomas-nguy requested review from JayT106 and randy-cro and removed request for a team May 27, 2026 08:38
@github-actions
Copy link
Copy Markdown

@thomas-nguy your pull request is missing a changelog!

@github-actions github-actions Bot added the evm label May 27, 2026
@thomas-nguy
Copy link
Copy Markdown
Author

@claude review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

Claude finished @thomas-nguy's task in 5m 44s —— View job


PR Review: fix(evm): align debug_trace fee with virtual ante path

  • Gather context and understand the request
  • Read changed files
  • Analyze code changes
  • Post review feedback

Summary

The overall direction is correct — replacing the raw StateDB.SubBalance/AddBalance with DeductTxCostsFromUserBalance + RefundGas aligns the debug trace path with the virtual fee collector approach used in CheckEthGasConsume / ApplyTransaction. The BaseFee.BigInt() fix in TraceTx is a clean correctness improvement.

One significant bug was found, plus a few smaller concerns:


Issues Found

🔴 Deduct/Refund Price Asymmetry (EIP-1559)

debugTraceFeeAmount (in utils.go) correctly computes the effective gas price as min(tip+baseFee, feeCap), so the deduction uses the right amount. However, RefundGas (in gas.go:52) computes the refund as leftoverGas * msg.GasPrice — and for EIP-1559 transactions, msg.GasPrice is set to GasFeeCap by core.TransactionToMessage.

For a tx with baseFee=1gwei, tip=0, feeCap=5000gwei, gasLimit=2M, leftoverGas=1M:

  • Deducted: 1 * 2M = 2M gwei
  • Refund attempt: 5000 * 1M = 5B gwei ❌ — fee collector won't have enough → error/panic

The test masks this because gasTipCap=0 makes effectiveGasPrice == baseFee, so both sides use the same value. See inline comments for details.

🟡 Test Setup Ordering (state_transition_test.go:670)

SetupTestWithCb is called then immediately overridden by SetupTest. Whether the callback actually takes effect depends on internal test suite wiring — worth a double-check.

🟡 Nonce Semantics for Contract Creation in DebugTrace (state_transition.go:367)

The nonce is incremented eagerly on line 367 for all DebugTrace calls (including contractCreation). The Create branch later resets the nonce to msg.Nonce (line 444), which would undo the increment. This was the same in the old code, but worth documenting that the nonce reset in Create handles this.

🟢 Missing CHANGELOG Entry

The bot already flagged this. An entry under ## Unreleased like:

* (rpc) [#967](https://github.com/crypto-org-chain/ethermint/pull/967) fix(evm): align debug_traceTransaction fee deduction with virtual ante path (EIP-1559 effective gas price, BigInt BaseFee parse).

🟢 No Breaking State Changes

This PR only affects the debug_trace* RPC path (no ante handler, no consensus, no state serialization changes). It is not a state-breaking change and is safe to merge to a release branch from a consensus perspective.


Verdict

The core issue being fixed is real and the approach is right. The EIP-1559 refund asymmetry should be resolved before merge — it would cause RefundGas to fail or over-refund whenever gasFeeCap > effectiveGasPrice (which is the common case for EIP-1559 txs). All inline comments have been posted above.

Comment thread x/evm/keeper/utils.go
if baseFee != nil && msg.GasFeeCap != nil && msg.GasTipCap != nil {
gasPrice = ethermint.BigMin(new(big.Int).Add(msg.GasTipCap, baseFee), msg.GasFeeCap)
}
return new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(msg.GasLimit))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential asymmetry with RefundGas: debugTraceFeeAmount correctly uses the effective gas price (min(tip+baseFee, feeCap)) to compute the initial deduction. However, RefundGas (in gas.go) still computes the refund as leftoverGas * msg.GasPrice, where msg.GasPrice is the raw GasFeeCap for EIP-1559 txs.

This creates an asymmetry for EIP-1559 transactions in the debug trace path:

  • Deducted: effectiveGasPrice * gasLimit
  • Refunded: gasFeeCap * leftoverGas

For example with baseFee=1gwei, tipCap=0, feeCap=5000gwei, gasLimit=2M, leftoverGas=1M:

  • Deducted: 1 * 2M = 2M gwei
  • Refund attempt: 5000 * 1M = 5B gwei → fee collector would not have enough → panic/error

The test in state_transition_test.go uses gasTipCap=0 so effectiveGasPrice == baseFee and doesn't cover this case. Consider passing the effective gas price to RefundGas or fixing the refund to also use effective price for the debug trace path. Fix this →

suite.mintFeeCollector = true
suite.SetupTest()

baseFee := big.NewInt(1_000_000_000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only covers the case where gasTipCap = 0, so effectiveGasPrice == baseFee, which means deduction and refund both happen at the same price and the asymmetry between debugTraceFeeAmount (effective price) and RefundGas (msg.GasPrice = gasFeeCap) doesn't manifest.

Consider adding a second case where gasTipCap > 0 and gasFeeCap >> baseFee + tip, setting the balance to exactly effectiveFee (not gasFeeCap * gasLimit), to validate the whole round-trip deduct→execute→refund without over-refunding.

feemarketGenesis.Params.NoBaseFee = false
genesis[feemarkettypes.ModuleName] = a.AppCodec().MustMarshalJSON(feemarketGenesis)
return genesis
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: calling suite.SetupTestWithCb followed immediately by suite.SetupTest on line 672 means the callback-customized genesis is discarded — SetupTest reinitialises the suite with default genesis. The SetupTestWithCb call appears redundant here unless SetupTest is designed to call the previously registered callback. Worth confirming the intent.

return nil, err
}
}
tracingStateDB.SetNonce(sender, stateDB.GetNonce(sender)+1, tracing.NonceChangeEoACall)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nonce is incremented here before the EVM call, but for the non-DebugTrace contract-creation path the code explicitly resets the nonce to msg.Nonce before calling evm.Create (line 444) to match ante-handler pre-increment semantics. For DebugTrace + contractCreation, the nonce increment here plus the reset to msg.Nonce in the Create branch would interact in an unexpected way — confirm this is correct or add a note.

}
if req.BaseFee != nil {
baseFee = big.NewInt(req.BaseFee.Int64())
if req.BaseFee != nil && !req.BaseFee.IsNil() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix — using .BigInt() avoids silent truncation on values > math.MaxInt64. The added !req.BaseFee.IsNil() guard is also correct since a non-nil *sdkmath.Int can hold a nil internal value.

One minor note: sdkmath.Int.BigInt() returns a copy of the internal *big.Int, so the caller can mutate it safely — no defensive copy needed.

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.

1 participant