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