Skip to main content

revm_handler/
precompile_provider.rs

1use auto_impl::auto_impl;
2use context::{Cfg, LocalContextTr};
3use context_interface::{ContextTr, JournalTr};
4use interpreter::{CallInputs, Gas, InstructionResult, InterpreterResult};
5use precompile::{PrecompileOutput, PrecompileSpecId, PrecompileStatus, Precompiles};
6use primitives::{hardfork::SpecId, Address, AddressSet, Bytes};
7use std::string::{String, ToString};
8
9/// Provider for precompiled contracts in the EVM.
10#[auto_impl(&mut, Box)]
11pub trait PrecompileProvider<CTX: ContextTr> {
12    /// The output type returned by precompile execution.
13    type Output;
14
15    /// Sets the spec id and returns true if the spec id was changed. Initial call to set_spec will always return true.
16    ///
17    /// Returns `true` if precompile addresses should be injected into the journal.
18    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool;
19
20    /// Run the precompile.
21    fn run(
22        &mut self,
23        context: &mut CTX,
24        inputs: &CallInputs,
25    ) -> Result<Option<Self::Output>, String>;
26
27    /// Get the warm addresses.
28    fn warm_addresses(&self) -> &AddressSet;
29
30    /// Check if the address is a precompile.
31    fn contains(&self, address: &Address) -> bool {
32        self.warm_addresses().contains(address)
33    }
34}
35
36/// The [`PrecompileProvider`] for ethereum precompiles.
37#[derive(Debug)]
38pub struct EthPrecompiles {
39    /// Contains precompiles for the current spec.
40    pub precompiles: &'static Precompiles,
41    /// Current spec. None means that spec was not set yet.
42    pub spec: SpecId,
43}
44
45impl EthPrecompiles {
46    /// Create a new precompile provider with the given spec.
47    pub fn new(spec: SpecId) -> Self {
48        Self {
49            precompiles: Precompiles::new(PrecompileSpecId::from_spec_id(spec)),
50            spec,
51        }
52    }
53
54    /// Returns addresses of the precompiles.
55    pub const fn warm_addresses(&self) -> &AddressSet {
56        self.precompiles.addresses_set()
57    }
58
59    /// Returns whether the address is a precompile.
60    pub fn contains(&self, address: &Address) -> bool {
61        self.precompiles.contains(address)
62    }
63}
64
65impl Clone for EthPrecompiles {
66    fn clone(&self) -> Self {
67        Self {
68            precompiles: self.precompiles,
69            spec: self.spec,
70        }
71    }
72}
73
74/// Converts a [`PrecompileOutput`] into an [`InterpreterResult`].
75///
76/// Maps precompile status to the corresponding instruction result:
77/// - `Success` → `InstructionResult::Return`
78/// - `Revert` → `InstructionResult::Revert`
79/// - `Halt(OOG)` → `InstructionResult::PrecompileOOG`
80/// - `Halt(other)` → `InstructionResult::PrecompileError`
81pub fn precompile_output_to_interpreter_result(
82    output: PrecompileOutput,
83    gas_limit: u64,
84) -> InterpreterResult {
85    // set output bytes
86    let bytes = if output.status.is_success_or_revert() {
87        output.bytes
88    } else {
89        Bytes::new()
90    };
91
92    let mut result = InterpreterResult {
93        result: InstructionResult::Return,
94        gas: Gas::new_with_regular_gas_and_reservoir(gas_limit, output.reservoir),
95        output: bytes,
96    };
97
98    // set state gas, reservoir is already set in the Gas constructor
99    result.gas.set_state_gas_spent(output.state_gas_used as i64);
100    result.gas.set_refill_amount(output.refill_amount);
101    result.gas.record_refund(output.gas_refunded);
102
103    // spend used gas.
104    if output.status.is_success_or_revert() {
105        if !result.gas.record_regular_cost(output.gas_used) {
106            result.gas.spend_all();
107            result.output = Bytes::new();
108            result.result = InstructionResult::PrecompileOOG;
109            return result;
110        }
111    } else {
112        result.gas.spend_all();
113    }
114
115    // set result
116    result.result = match output.status {
117        PrecompileStatus::Success => InstructionResult::Return,
118        PrecompileStatus::Revert => InstructionResult::Revert,
119        PrecompileStatus::Halt(halt_reason) => {
120            if halt_reason.is_oog() {
121                InstructionResult::PrecompileOOG
122            } else {
123                InstructionResult::PrecompileError
124            }
125        }
126    };
127
128    result
129}
130
131impl<CTX: ContextTr> PrecompileProvider<CTX> for EthPrecompiles {
132    type Output = InterpreterResult;
133
134    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
135        let spec = spec.into();
136        // generate new precompiles only on new spec
137        if spec == self.spec {
138            return false;
139        }
140        self.precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec));
141        self.spec = spec;
142        true
143    }
144
145    fn run(
146        &mut self,
147        context: &mut CTX,
148        inputs: &CallInputs,
149    ) -> Result<Option<InterpreterResult>, String> {
150        let Some(precompile) = self.precompiles.get(&inputs.bytecode_address) else {
151            return Ok(None);
152        };
153
154        let output = precompile
155            .execute(
156                &inputs.input.as_bytes(context),
157                inputs.gas_limit,
158                inputs.reservoir,
159            )
160            .map_err(|e| e.to_string())?;
161
162        // If this is a top-level precompile call (depth == 1), persist the error message
163        // into the local context so it can be returned as output in the final result.
164        // Only do this for non-OOG halt errors.
165        if let Some(halt_reason) = output.halt_reason() {
166            if !halt_reason.is_oog() && context.journal().depth() == 1 {
167                context
168                    .local_mut()
169                    .set_precompile_error_context(halt_reason.to_string());
170            }
171        }
172
173        let result = precompile_output_to_interpreter_result(output, inputs.gas_limit);
174        Ok(Some(result))
175    }
176
177    fn warm_addresses(&self) -> &AddressSet {
178        Self::warm_addresses(self)
179    }
180
181    fn contains(&self, address: &Address) -> bool {
182        Self::contains(self, address)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::{instructions::EthInstructions, ExecuteEvm, MainContext};
190    use context::{Context, Evm, FrameStack, TxEnv};
191    use context_interface::result::{ExecutionResult, HaltReason, OutOfGasError};
192    use database::InMemoryDB;
193    use interpreter::interpreter::EthInterpreter;
194    use primitives::{address, hardfork::SpecId, TxKind, U256};
195    use state::AccountInfo;
196
197    /// Test-only address that hosts an over-spending precompile.
198    const OVERSPEND_PRECOMPILE: Address = address!("0000000000000000000000000000000000000100");
199
200    /// Custom precompile provider that drives the bug path: it returns a
201    /// `PrecompileOutput` with `status = Success` and `gas_used = u64::MAX` while
202    /// `gas_limit` is finite. Without the fix, `record_regular_cost`'s `false` return
203    /// is discarded so the call lands as `Return` with the gas tracker untouched —
204    /// the transaction succeeds and refunds the precompile's "free" gas. With the fix,
205    /// the helper converts the over-spend into `PrecompileOOG`, halting the tx.
206    #[derive(Debug)]
207    struct OverspendingPrecompiles {
208        inner: EthPrecompiles,
209        warm: AddressSet,
210    }
211
212    impl OverspendingPrecompiles {
213        fn new(spec: SpecId) -> Self {
214            let inner = EthPrecompiles::new(spec);
215            let mut warm = AddressSet::default();
216            warm.clone_from(inner.warm_addresses());
217            warm.insert(OVERSPEND_PRECOMPILE);
218            Self { inner, warm }
219        }
220    }
221
222    impl<CTX> PrecompileProvider<CTX> for OverspendingPrecompiles
223    where
224        CTX: ContextTr<Cfg: Cfg<Spec = SpecId>>,
225    {
226        type Output = InterpreterResult;
227
228        fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
229            let changed =
230                <EthPrecompiles as PrecompileProvider<CTX>>::set_spec(&mut self.inner, spec);
231            self.warm.clone_from(self.inner.warm_addresses());
232            self.warm.insert(OVERSPEND_PRECOMPILE);
233            changed
234        }
235
236        fn run(
237            &mut self,
238            context: &mut CTX,
239            inputs: &CallInputs,
240        ) -> Result<Option<Self::Output>, String> {
241            if inputs.bytecode_address == OVERSPEND_PRECOMPILE {
242                let output = PrecompileOutput {
243                    status: PrecompileStatus::Success,
244                    gas_used: u64::MAX,
245                    gas_refunded: 0,
246                    state_gas_used: 0,
247                    reservoir: inputs.reservoir,
248                    refill_amount: 0,
249                    bytes: Bytes::from_static(b"unreliable"),
250                };
251                return Ok(Some(precompile_output_to_interpreter_result(
252                    output,
253                    inputs.gas_limit,
254                )));
255            }
256            <EthPrecompiles as PrecompileProvider<CTX>>::run(&mut self.inner, context, inputs)
257        }
258
259        fn warm_addresses(&self) -> &AddressSet {
260            &self.warm
261        }
262    }
263
264    /// End-to-end regression test for Bug 3. A transaction targets a custom precompile
265    /// that lies about its gas usage. The fix turns this into an `OutOfGas(Precompile)`
266    /// halt; without the fix it is silently treated as a successful call.
267    #[test]
268    fn overspending_precompile_halts_tx_with_precompile_oog() {
269        let caller = address!("0000000000000000000000000000000000000001");
270        let mut db = InMemoryDB::default();
271        db.insert_account_info(
272            caller,
273            AccountInfo {
274                balance: U256::from(10).pow(U256::from(18)),
275                ..Default::default()
276            },
277        );
278
279        let spec = SpecId::default();
280        let ctx = Context::mainnet().with_db(db);
281        let mut evm = Evm {
282            ctx,
283            inspector: (),
284            instruction: EthInstructions::<EthInterpreter, _>::new_mainnet_with_spec(spec),
285            precompiles: OverspendingPrecompiles::new(spec),
286            frame_stack: FrameStack::new_prealloc(8),
287        };
288
289        let tx = TxEnv::builder()
290            .caller(caller)
291            .kind(TxKind::Call(OVERSPEND_PRECOMPILE))
292            .gas_limit(100_000)
293            .build()
294            .unwrap();
295
296        let exec = evm.transact_one(tx).expect("handler returned an error");
297
298        match exec {
299            ExecutionResult::Halt { reason, .. } => {
300                assert_eq!(
301                    reason,
302                    HaltReason::OutOfGas(OutOfGasError::Precompile),
303                    "expected precompile OOG halt for over-spending precompile",
304                );
305            }
306            ExecutionResult::Success { .. } => panic!(
307                "before-fix behavior leaked: over-spending precompile reported Success \
308                 instead of halting with PrecompileOOG"
309            ),
310            ExecutionResult::Revert { .. } => panic!("expected Halt(PrecompileOOG), got Revert"),
311        }
312    }
313}