Skip to content

fix: evm fees refund#17

Merged
mattkii merged 3 commits into
mainfrom
fix/evm-fee-refunds
Jun 23, 2026
Merged

fix: evm fees refund#17
mattkii merged 3 commits into
mainfrom
fix/evm-fee-refunds

Conversation

@mattkii

@mattkii mattkii commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes an over-refund bug in the fee-abstraction EVM gas refund path that allowed permissionless draining of the fee_collector module account.

RefundGas (x/vm/keeper/gas.go) computes the fee-abstraction refund as amount * leftoverGas / denominator, where amount is the full upfront fee charged for the whole gas limit (recorded in ContextPaidFeesKey by the chain's EVM ante). The denominator was the gas actually consumed (gasUsed) instead of the gas limit. Whenever gasLimit > 2 × gasUsed, the refund exceeded the entire upfront fee, and the surplus was paid out of fee_collector — the validator/staker reward pool. Repeating cheap, high-gas-limit transactions drains it.

Fix: use the transaction gas limit as the denominator. Since the caller in state_transition.go passes leftoverGas = gasLimit - gasUsed, this is exactly gasUsed + leftoverGas, so refund = amount * leftoverGas / gasLimit, which equals the value of the unused gas and is provably bounded by the amount paid (leftoverGas ≤ gasLimit).

Files most critical to review

  • x/vm/keeper/gas.go — the one-line denominator fix (gasUsedmsg.GasLimit).
  • tests/integration/x/vm/test_gas.go — new regression test TestGasRefundGasNoOverRefund.

How to review / reproduce

Run the refund suite (requires the test build tag, executed from the evmd module):

cd evmd
go test -tags=test ./tests/integration/ -run 'TestKeeperTestSuite/TestGasRefundGas' -v

The new TestGasRefundGasNoOverRefund reproduces the attack at the keeper level (gasLimit=100,000, gasUsed=21,000, paid fee recorded in ContextPaidFeesKey) and asserts the refund equals only the unused-gas value and never exceeds the paid fee.

Verification results:

  • Before the fix (denominator = gasUsed): refund = 45,142,857,142 vs. the correct 9,480,000,000fee_collector over-drained. Test FAILS.
  • After the fix (denominator = msg.GasLimit): refund = 9,480,000,000, bounded by the paid fee. Test PASSES, along with all pre-existing TestGasRefundGas cases.

End-to-end verification was also performed through the consuming chain's production EVM ante (NewEVMMonoDecoratorfee_collectorRefundGas) using the issue's PoC config (gasLimit=100,000, gasPrice=1e6, paidFee=1e11):

  • Before: refund 376,190,476,190; fee_collector net drain −276,190,476,190 (matches the reported PoC exactly).
  • After: refund 79,000,000,000; fee_collector retains the 21,000,000,000 fee for gas actually used; attacker net profit = 0.

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 — N/A: this targets the active v0.6.x maintenance branch (fix/distribution-32byte-withdraw), which is the lineage the consuming chain pins via KiiChain/evm v0.6.0-fork.1. main is an older divergent branch that already uses the gas-limit denominator and is not vulnerable. A new fork tag should be cut from this branch and the consuming chain's go.mod bumped to it.

@github-actions github-actions Bot added the tests label Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Keeper.RefundGas in x/vm/keeper/gas.go updates its documentation and implementation to compute refunds by scaling the paid fee amount by leftoverGas / msg.GasLimit instead of using the previous divisor, preventing over-refunds when gasLimit exceeds actual gas consumption. The existing TestGasRefundGas call site is updated to pass an additional gasUsed argument computed from DefaultCoreMsgGasUsage - tc.leftoverGas. A new integration test, TestGasRefundGasNoOverRefund, pre-funds the fee collector, constructs a message with gasLimit >> gasUsed, calls RefundGas, and asserts the balance delta equals leftoverGas * DefaultGasPrice and does not exceed the paid fee. The fix is recorded in CHANGELOG.md.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: evm fees refund' directly describes the main change: fixing an over-refund bug in the EVM fee-abstraction refund mechanism.
Description check ✅ Passed The description is directly related to the changeset, providing detailed context about the over-refund bug fix, the solution, affected files, and verification results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/evm-fee-refunds

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/x/vm/test_gas.go`:
- Line 260: The RefundGas method call in the integration test is passing an
extra gasUsed argument that is no longer part of the method signature. Remove
the gasUsed parameter from the RefundGas call on the
unitNetwork.App.GetEVMKeeper().RefundGas line, keeping only the ctx, *coreMsg,
leftoverGas, and baseDenom arguments to match the actual method signature in
x/vm/keeper/gas.go.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9732a2d5-f9f4-4fd5-a403-54af34017e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 90dedcc and 15471c7.

📒 Files selected for processing (2)
  • tests/integration/x/vm/test_gas.go
  • x/vm/keeper/gas.go

Comment thread tests/integration/x/vm/test_gas.go
@mattkii mattkii force-pushed the fix/evm-fee-refunds branch from 15471c7 to 983ade3 Compare June 19, 2026 04:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
x/vm/keeper/gas.go (1)

69-73: ⚡ Quick win

Guard the denominator computation against overflow/zero before division.

gasUsed + leftoverGas is summed as uint64 and fed to big.Int.Div. If that sum overflows to zero, this path can panic on division-by-zero. Add an explicit checked add before constructing gasLimit.

Proposed hardening
+				totalGas, carry := bits.Add64(gasUsed, leftoverGas, 0)
+				if carry != 0 || totalGas == 0 {
+					return errorsmod.Wrap(types.ErrInvalidRefund, "invalid gas inputs for refund calculation")
+				}
-				gasLimit := new(big.Int).SetUint64(gasUsed + leftoverGas)
+				gasLimit := new(big.Int).SetUint64(totalGas)
 				remaining = new(big.Int).Div(
 					new(big.Int).Mul(amount, new(big.Int).SetUint64(leftoverGas)),
 					gasLimit,
 				)
import "math/bits"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@x/vm/keeper/gas.go` around lines 69 - 73, The denominator computation in the
division operation contains a potential overflow vulnerability where gasUsed +
leftoverGas could overflow as a uint64 and wrap to zero, causing a
division-by-zero panic. Import the math/bits package and replace the direct
addition of gasUsed and leftoverGas with a checked add operation using
math/bits.Add64 to detect overflow before constructing the gasLimit variable.
Handle the overflow case appropriately (either return early with an error or use
a safe fallback) to ensure the denominator in the Div call is never zero.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@x/vm/keeper/gas.go`:
- Around line 69-73: The denominator computation in the division operation
contains a potential overflow vulnerability where gasUsed + leftoverGas could
overflow as a uint64 and wrap to zero, causing a division-by-zero panic. Import
the math/bits package and replace the direct addition of gasUsed and leftoverGas
with a checked add operation using math/bits.Add64 to detect overflow before
constructing the gasLimit variable. Handle the overflow case appropriately
(either return early with an error or use a safe fallback) to ensure the
denominator in the Div call is never zero.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 070cb690-8956-40e1-99b7-6abc45ad0f08

📥 Commits

Reviewing files that changed from the base of the PR and between 15471c7 and 983ade3.

📒 Files selected for processing (4)
  • tests/integration/x/vm/test_gas.go
  • tests/integration/x/vm/test_state_transition.go
  • x/vm/keeper/gas.go
  • x/vm/keeper/state_transition.go
✅ Files skipped from review due to trivial changes (1)
  • x/vm/keeper/state_transition.go

@mattkii mattkii changed the base branch from main-old to main June 22, 2026 02:39
@mattkii mattkii force-pushed the fix/evm-fee-refunds branch 2 times, most recently from 67daf8b to 98a6942 Compare June 22, 2026 02:56
Comment thread x/vm/keeper/gas.go Outdated
Comment thread x/vm/keeper/gas.go Outdated
@mattkii mattkii force-pushed the fix/evm-fee-refunds branch 3 times, most recently from 4310666 to e203e4a Compare June 22, 2026 13:40
@mattkii mattkii force-pushed the fix/evm-fee-refunds branch from e203e4a to 9029908 Compare June 22, 2026 13:41
@mattkii mattkii requested a review from jhelison June 22, 2026 13:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@x/vm/keeper/gas.go`:
- Around line 69-71: The early return condition at line 42 that checks `if
gasUsed == 0 { return nil }` prevents the refund calculation from executing when
no gas is used, which blocks valid full refunds. Since the denominator now uses
`msg.GasLimit` (line 71), when `gasUsed == 0`, the calculation would correctly
compute the full refund as the remaining amount equals the original amount.
Remove or modify this early return condition to allow the refund calculation to
proceed and handle the full-refund case properly, such as the TestGasRefundGas
test case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12ac2967-ba5a-4c0c-916a-ee737a21c3d0

📥 Commits

Reviewing files that changed from the base of the PR and between 6391dd9 and 7006495.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • tests/integration/x/vm/test_gas.go
  • x/vm/keeper/gas.go
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/integration/x/vm/test_gas.go

Comment thread x/vm/keeper/gas.go
@mattkii mattkii merged commit 375d5dd into main Jun 23, 2026
14 of 21 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