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    /// Runs the precompile for the given call inputs.
21    ///
22    /// Return values distinguish whether the provider handled the call:
23    /// - `Ok(Some(output))` means the call was executed by this provider.
24    /// - `Ok(None)` means this provider does not contain a precompile for the
25    ///   requested address, so the caller should continue with regular contract
26    ///   execution.
27    /// - `Err(error)` means execution failed with a provider error that should
28    ///   abort EVM execution. Non-fatal precompile failures, such as reverts,
29    ///   out-of-gas, or invalid input, should be encoded in `output` instead.
30    fn run(
31        &mut self,
32        context: &mut CTX,
33        inputs: &CallInputs,
34    ) -> Result<Option<Self::Output>, String>;
35
36    /// Get the warm addresses.
37    fn warm_addresses(&self) -> &AddressSet;
38
39    /// Check if the address is a precompile.
40    fn contains(&self, address: &Address) -> bool {
41        self.warm_addresses().contains(address)
42    }
43}
44
45/// The [`PrecompileProvider`] for ethereum precompiles.
46#[derive(Debug)]
47pub struct EthPrecompiles {
48    /// Contains precompiles for the current spec.
49    pub precompiles: &'static Precompiles,
50    /// Current spec. None means that spec was not set yet.
51    pub spec: SpecId,
52}
53
54impl EthPrecompiles {
55    /// Create a new precompile provider with the given spec.
56    pub fn new(spec: SpecId) -> Self {
57        Self {
58            precompiles: Precompiles::new(PrecompileSpecId::from_spec_id(spec)),
59            spec,
60        }
61    }
62
63    /// Returns addresses of the precompiles.
64    pub const fn warm_addresses(&self) -> &AddressSet {
65        self.precompiles.addresses_set()
66    }
67
68    /// Returns whether the address is a precompile.
69    pub fn contains(&self, address: &Address) -> bool {
70        self.precompiles.contains(address)
71    }
72}
73
74impl Clone for EthPrecompiles {
75    fn clone(&self) -> Self {
76        Self {
77            precompiles: self.precompiles,
78            spec: self.spec,
79        }
80    }
81}
82
83/// Converts a [`PrecompileOutput`] into an [`InterpreterResult`] for a call frame
84/// with `gas_limit` regular gas.
85///
86/// Maps precompile status to the corresponding instruction result:
87/// - `Success` -> [`InstructionResult::Return`]
88/// - `Revert` -> [`InstructionResult::Revert`]
89/// - `Halt(OOG)` -> [`InstructionResult::PrecompileOOG`]
90/// - `Halt(other)` -> [`InstructionResult::PrecompileError`]
91///
92/// A precompile that reports more gas than it was given is downgraded to
93/// [`InstructionResult::PrecompileOOG`]. Anything but a success or revert consumes
94/// all regular gas and returns no output bytes.
95pub fn precompile_output_to_interpreter_result(
96    output: PrecompileOutput,
97    gas_limit: u64,
98) -> InterpreterResult {
99    // A precompile lying about its usage must not leave the frame with gas it
100    // never had: charging more regular gas than the limit is an OOG halt.
101    let result = if output.gas_used > gas_limit {
102        InstructionResult::PrecompileOOG
103    } else {
104        match &output.status {
105            PrecompileStatus::Success => InstructionResult::Return,
106            PrecompileStatus::Revert => InstructionResult::Revert,
107            PrecompileStatus::Halt(reason) if reason.is_oog() => InstructionResult::PrecompileOOG,
108            PrecompileStatus::Halt(_) => InstructionResult::PrecompileError,
109        }
110    };
111
112    // Gas used, refund, state gas (with its spilled portion, so a later rollback
113    // credits it back to regular gas per EIP-8037) and the reservoir all come from
114    // the precompile's own accounting.
115    let mut gas = Gas::new(gas_limit);
116    *gas.tracker_mut() = output.to_gas_tracker(gas_limit);
117
118    // Only a success or revert returns output bytes and keeps its unspent gas.
119    if result.is_halt() {
120        gas.spend_all();
121        return InterpreterResult::new(result, Bytes::new(), gas);
122    }
123
124    InterpreterResult::new(result, output.bytes, gas)
125}
126
127impl<CTX: ContextTr> PrecompileProvider<CTX> for EthPrecompiles {
128    type Output = InterpreterResult;
129
130    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
131        let spec = spec.into();
132        // generate new precompiles only on new spec
133        if spec == self.spec {
134            return false;
135        }
136        self.precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec));
137        self.spec = spec;
138        true
139    }
140
141    fn run(
142        &mut self,
143        context: &mut CTX,
144        inputs: &CallInputs,
145    ) -> Result<Option<InterpreterResult>, String> {
146        let Some(precompile) = self.precompiles.get(&inputs.bytecode_address) else {
147            return Ok(None);
148        };
149
150        let output = precompile
151            .execute(
152                &inputs.input.as_bytes(context),
153                inputs.gas_limit,
154                inputs.reservoir,
155            )
156            .map_err(|e| e.to_string())?;
157
158        // If this is a top-level precompile call (depth == 1), persist the error message
159        // into the local context so it can be returned as output in the final result.
160        // Only do this for non-OOG halt errors.
161        if let Some(halt_reason) = output.halt_reason() {
162            if !halt_reason.is_oog() && context.journal().depth() == 1 {
163                context
164                    .local_mut()
165                    .set_precompile_error_context(halt_reason.to_string());
166            }
167        }
168
169        let result = precompile_output_to_interpreter_result(output, inputs.gas_limit);
170        Ok(Some(result))
171    }
172
173    fn warm_addresses(&self) -> &AddressSet {
174        Self::warm_addresses(self)
175    }
176
177    fn contains(&self, address: &Address) -> bool {
178        Self::contains(self, address)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::{instructions::EthInstructions, ExecuteEvm, MainContext};
186    use context::{Context, Evm, FrameStack, TxEnv};
187    use context_interface::result::{ExecutionResult, HaltReason, OutOfGasError};
188    use database::InMemoryDB;
189    use interpreter::interpreter::EthInterpreter;
190    use primitives::{address, hardfork::SpecId, TxKind, U256};
191    use state::AccountInfo;
192
193    /// Test-only address that hosts an over-spending precompile.
194    const OVERSPEND_PRECOMPILE: Address = address!("0000000000000000000000000000000000000100");
195
196    /// Custom precompile provider that drives the bug path: it returns a
197    /// `PrecompileOutput` with `status = Success` and `gas_used = u64::MAX` while
198    /// `gas_limit` is finite. Without the fix, `record_regular_cost`'s `false` return
199    /// is discarded so the call lands as `Return` with the gas tracker untouched —
200    /// the transaction succeeds and refunds the precompile's "free" gas. With the fix,
201    /// the helper converts the over-spend into `PrecompileOOG`, halting the tx.
202    #[derive(Debug)]
203    struct OverspendingPrecompiles {
204        inner: EthPrecompiles,
205        warm: AddressSet,
206    }
207
208    impl OverspendingPrecompiles {
209        fn new(spec: SpecId) -> Self {
210            let inner = EthPrecompiles::new(spec);
211            let mut warm = AddressSet::default();
212            warm.clone_from(inner.warm_addresses());
213            warm.insert(OVERSPEND_PRECOMPILE);
214            Self { inner, warm }
215        }
216    }
217
218    impl<CTX> PrecompileProvider<CTX> for OverspendingPrecompiles
219    where
220        CTX: ContextTr<Cfg: Cfg<Spec = SpecId>>,
221    {
222        type Output = InterpreterResult;
223
224        fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
225            let changed =
226                <EthPrecompiles as PrecompileProvider<CTX>>::set_spec(&mut self.inner, spec);
227            self.warm.clone_from(self.inner.warm_addresses());
228            self.warm.insert(OVERSPEND_PRECOMPILE);
229            changed
230        }
231
232        fn run(
233            &mut self,
234            context: &mut CTX,
235            inputs: &CallInputs,
236        ) -> Result<Option<Self::Output>, String> {
237            if inputs.bytecode_address == OVERSPEND_PRECOMPILE {
238                let output = PrecompileOutput {
239                    status: PrecompileStatus::Success,
240                    gas_used: u64::MAX,
241                    gas_refunded: 0,
242                    state_gas_used: 0,
243                    state_gas_spilled: 0,
244                    reservoir: inputs.reservoir,
245                    bytes: Bytes::from_static(b"unreliable"),
246                };
247                return Ok(Some(precompile_output_to_interpreter_result(
248                    output,
249                    inputs.gas_limit,
250                )));
251            }
252            <EthPrecompiles as PrecompileProvider<CTX>>::run(&mut self.inner, context, inputs)
253        }
254
255        fn warm_addresses(&self) -> &AddressSet {
256            &self.warm
257        }
258    }
259
260    /// The spilled portion of a precompile's state gas must reach the frame's gas
261    /// tracker, otherwise a rollback credits it to the reservoir instead of regular
262    /// gas (EIP-8037).
263    #[test]
264    fn precompile_output_propagates_spilled_state_gas() {
265        let output = PrecompileOutput {
266            status: PrecompileStatus::Success,
267            // 10 regular + 30 state gas, of which 20 spilled out of the 10 gas reservoir
268            gas_used: 40,
269            gas_refunded: 0,
270            state_gas_used: 30,
271            state_gas_spilled: 20,
272            reservoir: 0,
273            bytes: Bytes::new(),
274        };
275        let mut result = precompile_output_to_interpreter_result(output, 100);
276
277        assert_eq!(result.result, InstructionResult::Return);
278        assert_eq!(result.gas.state_gas_spent(), 30);
279        assert_eq!(result.gas.state_gas_spilled(), 20);
280        assert_eq!(result.gas.remaining(), 60);
281
282        // rollback returns the spilled part to regular gas and the rest to the reservoir
283        result.gas.rollback_state_gas();
284        assert_eq!(result.gas.remaining(), 80);
285        assert_eq!(result.gas.reservoir(), 10);
286        assert_eq!(result.gas.state_gas_spent(), 0);
287        assert_eq!(result.gas.state_gas_spilled(), 0);
288    }
289
290    /// A precompile that reports more gas than its limit is turned into an OOG halt
291    /// with all gas consumed and no output bytes.
292    #[test]
293    fn precompile_output_overspend_is_oog() {
294        let output = PrecompileOutput::new(u64::MAX, Bytes::from_static(b"out"), 0);
295        let result = precompile_output_to_interpreter_result(output, 100);
296        assert_eq!(result.result, InstructionResult::PrecompileOOG);
297        assert_eq!(result.gas.remaining(), 0);
298        assert!(result.output.is_empty());
299    }
300
301    /// End-to-end regression test for Bug 3. A transaction targets a custom precompile
302    /// that lies about its gas usage. The fix turns this into an `OutOfGas(Precompile)`
303    /// halt; without the fix it is silently treated as a successful call.
304    #[test]
305    fn overspending_precompile_halts_tx_with_precompile_oog() {
306        let caller = address!("0000000000000000000000000000000000000001");
307        let mut db = InMemoryDB::default();
308        db.insert_account_info(
309            caller,
310            AccountInfo {
311                balance: U256::from(10).pow(U256::from(18)),
312                ..Default::default()
313            },
314        );
315
316        let spec = SpecId::default();
317        let ctx = Context::mainnet().with_db(db);
318        let mut evm = Evm {
319            ctx,
320            inspector: (),
321            instruction: EthInstructions::<EthInterpreter, _>::new_mainnet_with_spec(spec),
322            precompiles: OverspendingPrecompiles::new(spec),
323            frame_stack: FrameStack::new_prealloc(8),
324            #[cfg(feature = "asyncdb")]
325            async_stack: database_interface::async_db::FiberStack::default(),
326        };
327
328        let tx = TxEnv::builder()
329            .caller(caller)
330            .kind(TxKind::Call(OVERSPEND_PRECOMPILE))
331            .gas_limit(100_000)
332            .build()
333            .unwrap();
334
335        let exec = evm.transact_one(tx).expect("handler returned an error");
336
337        match exec {
338            ExecutionResult::Halt { reason, .. } => {
339                assert_eq!(
340                    reason,
341                    HaltReason::OutOfGas(OutOfGasError::Precompile),
342                    "expected precompile OOG halt for over-spending precompile",
343                );
344            }
345            ExecutionResult::Success { .. } => panic!(
346                "before-fix behavior leaked: over-spending precompile reported Success \
347                 instead of halting with PrecompileOOG"
348            ),
349            ExecutionResult::Revert { .. } => panic!("expected Halt(PrecompileOOG), got Revert"),
350        }
351    }
352}