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);
100    result.gas.record_refund(output.gas_refunded);
101
102    // spend used gas.
103    if output.status.is_success_or_revert() {
104        if !result.gas.record_regular_cost(output.gas_used) {
105            result.gas.spend_all();
106            result.output = Bytes::new();
107            result.result = InstructionResult::PrecompileOOG;
108            return result;
109        }
110    } else {
111        result.gas.spend_all();
112    }
113
114    // set result
115    result.result = match output.status {
116        PrecompileStatus::Success => InstructionResult::Return,
117        PrecompileStatus::Revert => InstructionResult::Revert,
118        PrecompileStatus::Halt(halt_reason) => {
119            if halt_reason.is_oog() {
120                InstructionResult::PrecompileOOG
121            } else {
122                InstructionResult::PrecompileError
123            }
124        }
125    };
126
127    result
128}
129
130impl<CTX: ContextTr> PrecompileProvider<CTX> for EthPrecompiles {
131    type Output = InterpreterResult;
132
133    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
134        let spec = spec.into();
135        // generate new precompiles only on new spec
136        if spec == self.spec {
137            return false;
138        }
139        self.precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec));
140        self.spec = spec;
141        true
142    }
143
144    fn run(
145        &mut self,
146        context: &mut CTX,
147        inputs: &CallInputs,
148    ) -> Result<Option<InterpreterResult>, String> {
149        let Some(precompile) = self.precompiles.get(&inputs.bytecode_address) else {
150            return Ok(None);
151        };
152
153        let output = precompile
154            .execute(
155                &inputs.input.as_bytes(context),
156                inputs.gas_limit,
157                inputs.reservoir,
158            )
159            .map_err(|e| e.to_string())?;
160
161        // If this is a top-level precompile call (depth == 1), persist the error message
162        // into the local context so it can be returned as output in the final result.
163        // Only do this for non-OOG halt errors.
164        if let Some(halt_reason) = output.halt_reason() {
165            if !halt_reason.is_oog() && context.journal().depth() == 1 {
166                context
167                    .local_mut()
168                    .set_precompile_error_context(halt_reason.to_string());
169            }
170        }
171
172        let result = precompile_output_to_interpreter_result(output, inputs.gas_limit);
173        Ok(Some(result))
174    }
175
176    fn warm_addresses(&self) -> &AddressSet {
177        Self::warm_addresses(self)
178    }
179
180    fn contains(&self, address: &Address) -> bool {
181        Self::contains(self, address)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::{instructions::EthInstructions, ExecuteEvm, MainContext};
189    use context::{Context, Evm, FrameStack, TxEnv};
190    use context_interface::result::{ExecutionResult, HaltReason, OutOfGasError};
191    use database::InMemoryDB;
192    use interpreter::interpreter::EthInterpreter;
193    use primitives::{address, hardfork::SpecId, TxKind, U256};
194    use state::AccountInfo;
195
196    /// Test-only address that hosts an over-spending precompile.
197    const OVERSPEND_PRECOMPILE: Address = address!("0000000000000000000000000000000000000100");
198
199    /// Custom precompile provider that drives the bug path: it returns a
200    /// `PrecompileOutput` with `status = Success` and `gas_used = u64::MAX` while
201    /// `gas_limit` is finite. Without the fix, `record_regular_cost`'s `false` return
202    /// is discarded so the call lands as `Return` with the gas tracker untouched —
203    /// the transaction succeeds and refunds the precompile's "free" gas. With the fix,
204    /// the helper converts the over-spend into `PrecompileOOG`, halting the tx.
205    #[derive(Debug)]
206    struct OverspendingPrecompiles {
207        inner: EthPrecompiles,
208        warm: AddressSet,
209    }
210
211    impl OverspendingPrecompiles {
212        fn new(spec: SpecId) -> Self {
213            let inner = EthPrecompiles::new(spec);
214            let mut warm = AddressSet::default();
215            warm.clone_from(inner.warm_addresses());
216            warm.insert(OVERSPEND_PRECOMPILE);
217            Self { inner, warm }
218        }
219    }
220
221    impl<CTX> PrecompileProvider<CTX> for OverspendingPrecompiles
222    where
223        CTX: ContextTr<Cfg: Cfg<Spec = SpecId>>,
224    {
225        type Output = InterpreterResult;
226
227        fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
228            let changed =
229                <EthPrecompiles as PrecompileProvider<CTX>>::set_spec(&mut self.inner, spec);
230            self.warm.clone_from(self.inner.warm_addresses());
231            self.warm.insert(OVERSPEND_PRECOMPILE);
232            changed
233        }
234
235        fn run(
236            &mut self,
237            context: &mut CTX,
238            inputs: &CallInputs,
239        ) -> Result<Option<Self::Output>, String> {
240            if inputs.bytecode_address == OVERSPEND_PRECOMPILE {
241                let output = PrecompileOutput {
242                    status: PrecompileStatus::Success,
243                    gas_used: u64::MAX,
244                    gas_refunded: 0,
245                    state_gas_used: 0,
246                    reservoir: inputs.reservoir,
247                    bytes: Bytes::from_static(b"unreliable"),
248                };
249                return Ok(Some(precompile_output_to_interpreter_result(
250                    output,
251                    inputs.gas_limit,
252                )));
253            }
254            <EthPrecompiles as PrecompileProvider<CTX>>::run(&mut self.inner, context, inputs)
255        }
256
257        fn warm_addresses(&self) -> &AddressSet {
258            &self.warm
259        }
260    }
261
262    /// End-to-end regression test for Bug 3. A transaction targets a custom precompile
263    /// that lies about its gas usage. The fix turns this into an `OutOfGas(Precompile)`
264    /// halt; without the fix it is silently treated as a successful call.
265    #[test]
266    fn overspending_precompile_halts_tx_with_precompile_oog() {
267        let caller = address!("0000000000000000000000000000000000000001");
268        let mut db = InMemoryDB::default();
269        db.insert_account_info(
270            caller,
271            AccountInfo {
272                balance: U256::from(10).pow(U256::from(18)),
273                ..Default::default()
274            },
275        );
276
277        let spec = SpecId::default();
278        let ctx = Context::mainnet().with_db(db);
279        let mut evm = Evm {
280            ctx,
281            inspector: (),
282            instruction: EthInstructions::<EthInterpreter, _>::new_mainnet_with_spec(spec),
283            precompiles: OverspendingPrecompiles::new(spec),
284            frame_stack: FrameStack::new_prealloc(8),
285        };
286
287        let tx = TxEnv::builder()
288            .caller(caller)
289            .kind(TxKind::Call(OVERSPEND_PRECOMPILE))
290            .gas_limit(100_000)
291            .build()
292            .unwrap();
293
294        let exec = evm.transact_one(tx).expect("handler returned an error");
295
296        match exec {
297            ExecutionResult::Halt { reason, .. } => {
298                assert_eq!(
299                    reason,
300                    HaltReason::OutOfGas(OutOfGasError::Precompile),
301                    "expected precompile OOG halt for over-spending precompile",
302                );
303            }
304            ExecutionResult::Success { .. } => panic!(
305                "before-fix behavior leaked: over-spending precompile reported Success \
306                 instead of halting with PrecompileOOG"
307            ),
308            ExecutionResult::Revert { .. } => panic!("expected Halt(PrecompileOOG), got Revert"),
309        }
310    }
311}