Skip to main content

revm_handler/
post_execution.rs

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