Skip to main content

revm_handler/
post_execution.rs

1use crate::FrameResult;
2use context::journaled_state::account::JournaledAccountTr;
3use context_interface::{
4    cfg::GasParams,
5    journaled_state::JournalTr,
6    result::{ExecutionResult, HaltReason, HaltReasonTr, ResultGas},
7    Block, Cfg, ContextTr, Database, LocalContextTr, Transaction,
8};
9use interpreter::{Gas, InitialAndFloorGas, SuccessOrHalt};
10use primitives::{hardfork::SpecId, U256};
11
12/// Builds a [`ResultGas`] from the execution [`Gas`] struct and [`InitialAndFloorGas`].
13pub fn build_result_gas(
14    _is_halt: bool,
15    gas: &Gas,
16    init_and_floor_gas: InitialAndFloorGas,
17) -> ResultGas {
18    // `state_gas_spent` is tracked as i64 to allow a child frame's count to go
19    // negative on 0→x→0 restoration; at the top level, post-reconciliation it
20    // is expected to be >= 0 and is clamped defensively before combining with
21    // the state gas charged before the first frame (the EIP-2780 runtime gas
22    // phase).
23    let state_gas = gas
24        .state_gas_spent()
25        .saturating_add_unsigned(init_and_floor_gas.initial_state_gas)
26        .max(0) as u64;
27
28    ResultGas::default()
29        .with_total_gas_spent(
30            gas.limit()
31                .saturating_sub(gas.remaining())
32                .saturating_sub(gas.reservoir()),
33        )
34        .with_refunded(gas.refunded() as u64)
35        .with_floor_gas(init_and_floor_gas.floor_gas())
36        .with_state_gas_spent(state_gas)
37}
38
39/// Ensures minimum gas floor is spent according to EIP-7623.
40///
41/// Per EIP-8037, gas used before refund is `tx.gas - gas_left - state_gas_reservoir`.
42/// The floor applies to this combined total, not just regular gas.
43pub const fn eip7623_check_gas_floor(gas: &mut Gas, init_and_floor_gas: InitialAndFloorGas) {
44    // EIP-7623: Increase calldata cost
45    // EIP-8037: tx_gas_used_before_refund = tx.gas - gas_left - reservoir
46    // The floor must apply to this combined value, not just (limit - remaining).
47    let gas_used_before_refund = gas.total_gas_spent().saturating_sub(gas.reservoir());
48    let gas_used_after_refund = gas_used_before_refund.saturating_sub(gas.refunded() as u64);
49    if gas_used_after_refund < init_and_floor_gas.floor_gas() {
50        // Match execution-specs: when the floor wins, the unused state gas
51        // (reservoir) is absorbed into the floor cost rather than reimbursed
52        // separately. Zeroing it keeps `reimburse_caller`'s
53        // `remaining + reservoir + refunded` sum equal to `limit - floor`.
54        gas.set_spent(init_and_floor_gas.floor_gas());
55        gas.set_reservoir(0);
56        gas.set_refund(0);
57    }
58}
59
60/// Calculates and applies gas refunds based on the configured gas parameters.
61pub fn refund(gas_params: &GasParams, gas: &mut Gas, eip7702_refund: i64) {
62    gas.record_refund(eip7702_refund);
63    gas.set_final_refund(gas_params.max_refund_quotient());
64}
65
66/// Reimburses the caller for unused gas.
67#[inline]
68pub fn reimburse_caller<CTX: ContextTr>(
69    context: &mut CTX,
70    gas: &Gas,
71    additional_refund: U256,
72) -> Result<(), <CTX::Db as Database>::Error> {
73    // If fee charge was disabled (e.g. eth_call simulations), no gas was
74    // deducted from the caller upfront so there is nothing to reimburse.
75    if context.cfg().is_fee_charge_disabled() {
76        return Ok(());
77    }
78    let basefee = context.block().basefee() as u128;
79    let caller = context.tx().caller();
80    let effective_gas_price = context.tx().effective_gas_price(basefee);
81
82    // Return balance of not spent gas.
83    // Include reservoir gas (EIP-8037) which is also unused and must be reimbursed.
84    let reimbursable = gas.remaining() + gas.reservoir() + gas.refunded() as u64;
85    context
86        .journal_mut()
87        .load_account_mut(caller)?
88        .incr_balance(
89            U256::from(effective_gas_price.saturating_mul(reimbursable as u128))
90                + additional_refund,
91        );
92
93    Ok(())
94}
95
96/// Rewards the beneficiary with transaction fees.
97#[inline]
98pub fn reward_beneficiary<CTX: ContextTr>(
99    context: &mut CTX,
100    gas: &Gas,
101) -> Result<(), <CTX::Db as Database>::Error> {
102    // If fee charge was disabled (e.g. eth_call simulations), the caller was
103    // never charged for gas so there are no fees to transfer to the beneficiary.
104    if context.cfg().is_fee_charge_disabled() {
105        return Ok(());
106    }
107    let (block, tx, cfg, journal, _, _) = context.all_mut();
108    let basefee = block.basefee() as u128;
109    let effective_gas_price = tx.effective_gas_price(basefee);
110
111    // Transfer fee to coinbase/beneficiary.
112    // EIP-1559 discard basefee for coinbase transfer. Basefee amount of gas is discarded.
113    let coinbase_gas_price = if cfg.spec().into().is_enabled_in(SpecId::LONDON) {
114        effective_gas_price.saturating_sub(basefee)
115    } else {
116        effective_gas_price
117    };
118
119    // Reward beneficiary.
120    // Exclude reservoir gas (EIP-8037) from the used gas — reservoir is unused and reimbursed.
121    let effective_used = gas.used().saturating_sub(gas.reservoir());
122    journal
123        .load_account_mut(block.beneficiary())?
124        .incr_balance(U256::from(coinbase_gas_price * effective_used as u128));
125
126    Ok(())
127}
128
129/// Calculate last gas spent and transform internal reason to external.
130///
131/// TODO make Journal FinalOutput more generic.
132pub fn output<CTX: ContextTr<Journal: JournalTr>, HALTREASON: HaltReasonTr>(
133    context: &mut CTX,
134    // TODO, make this more generic and nice.
135    // FrameResult should be a generic that returns gas and interpreter result.
136    result: FrameResult,
137    result_gas: ResultGas,
138) -> ExecutionResult<HALTREASON> {
139    let output = result.output();
140    let instruction_result = result.into_interpreter_result();
141
142    // take logs from journal.
143    let logs = context.journal_mut().take_logs();
144
145    match SuccessOrHalt::<HALTREASON>::from(instruction_result.result) {
146        SuccessOrHalt::Success(reason) => ExecutionResult::Success {
147            reason,
148            gas: result_gas,
149            logs,
150            output,
151        },
152        SuccessOrHalt::Revert => ExecutionResult::Revert {
153            gas: result_gas,
154            logs,
155            output: output.into_data(),
156        },
157        SuccessOrHalt::Halt(reason) => {
158            // Bubble up precompile errors from context when available
159            if matches!(
160                instruction_result.result,
161                interpreter::InstructionResult::PrecompileError
162            ) {
163                if let Some(message) = context.local_mut().take_precompile_error_context() {
164                    return ExecutionResult::Halt {
165                        reason: HALTREASON::from(HaltReason::PrecompileErrorWithContext(message)),
166                        gas: result_gas,
167                        logs,
168                    };
169                }
170            }
171            ExecutionResult::Halt {
172                reason,
173                gas: result_gas,
174                logs,
175            }
176        }
177        // Only two internal return flags.
178        flag @ (SuccessOrHalt::FatalExternalError | SuccessOrHalt::Internal(_)) => {
179            panic!(
180                "Encountered unexpected internal return flag: {flag:?} with instruction result: {instruction_result:?}"
181            )
182        }
183    }
184}