Skip to content

fix(evm): don't pay the beneficiary for the unused EIP-8037 reservoir - #395

Open
Xowiek wants to merge 1 commit into
circlefin:mainfrom
Xowiek:fix/beneficiary-reservoir
Open

fix(evm): don't pay the beneficiary for the unused EIP-8037 reservoir#395
Xowiek wants to merge 1 commit into
circlefin:mainfrom
Xowiek:fix/beneficiary-reservoir

Conversation

@Xowiek

@Xowiek Xowiek commented Sep 12, 2026

Copy link
Copy Markdown

ArcEvmHandler::reward_beneficiary pays the beneficiary for:

let gas_used = exec_result.gas().used();

Under EIP-8037 that also counts gas the caller gets back. last_frame_result rebuilds the top-level gas over the whole tx gas limit, returns only the regular remaining, and keeps the unused state gas reservoir separately:

*gas = Gas::new_spent(evm.ctx().tx().gas_limit());
gas.erase_cost(remaining);
gas.set_reservoir(reservoir);

so used()tx.gas_limit - remaining - refunded — still includes the reservoir. Arc doesn't override reimburse_caller, and revm's version hands remaining + reservoir + refunded back to the caller. The reservoir ends up both refunded and paid to the beneficiary.

revm's own post_execution::reward_beneficiary leaves it out for exactly this reason:

// Exclude reservoir gas (EIP-8037) from the used gas — reservoir is unused and reimbursed.
let effective_used = gas.used().saturating_sub(gas.reservoir());

EIP-8037 support landed in revm-handler 18.0.0. The Arc override was written before that and didn't pick the subtraction up when revm was bumped.

With a 20M gas limit, 1M regular gas left, a 3M reservoir and a price of 10, the caller is charged 200,000,000. On main, reimbursement plus reward come to:

  left: 230000000
 right: 200000000

The extra 30,000,000 is reservoir × price of native coin that was never debited from anyone.

This isn't reachable on any network today. The reservoir is only non-zero with EIP-8037 enabled, revm enables it at SpecId::AMSTERDAM, and no Arc chain spec schedules Amsterdam — mainnet and localdev stop at Osaka, testnet and devnet at Prague. So the change is a no-op for every existing block. create_evm already turns off revm's Amsterdam EIP-7708 logs ahead of the spec being enabled; this is the same kind of change for the fee path.

The added test runs revm's reimburse_caller and Arc's reward_beneficiary on the gas last_frame_result produces, and checks the two credits equal what the caller was charged. It fails on main with the output above. cargo test -p arc-evm passes (150 tests), as do fmt and clippy.

ArcEvmHandler overrides reward_beneficiary to pay the full effective gas price
to the beneficiary, and takes the gas it pays for from exec_result.gas().used().

Under EIP-8037 the tx-level Gas built by last_frame_result spans the whole
tx.gas_limit and keeps the unused state gas reservoir outside `remaining`, so
used() still counts it. revm's reimburse_caller, which Arc does not override,
returns remaining + reservoir + refunded to the caller. Paying the beneficiary
for used() as well credits the reservoir twice, and the two credits add up to
more than the caller was charged.

revm's own post_execution::reward_beneficiary subtracts gas.reservoir() for
this reason. The Arc override predates EIP-8037 support in revm-handler 18.0.0
and did not pick the subtraction up when revm was bumped.

No current network is affected: the reservoir is only non-zero once EIP-8037
is enabled, which revm does at Amsterdam, and no Arc chain spec schedules
Amsterdam. The change is a no-op until then.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant