use auto_impl::auto_impl;
use context::{Cfg, LocalContextTr};
use context_interface::{ContextTr, JournalTr};
use interpreter::{CallInputs, Gas, InstructionResult, InterpreterResult};
use precompile::{PrecompileOutput, PrecompileSpecId, PrecompileStatus, Precompiles};
use primitives::{hardfork::SpecId, Address, AddressSet, Bytes};
use std::string::{String, ToString};
#[auto_impl(&mut, Box)]
pub trait PrecompileProvider<CTX: ContextTr> {
type Output;
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool;
fn run(
&mut self,
context: &mut CTX,
inputs: &CallInputs,
) -> Result<Option<Self::Output>, String>;
fn warm_addresses(&self) -> &AddressSet;
fn contains(&self, address: &Address) -> bool {
self.warm_addresses().contains(address)
}
}
#[derive(Debug)]
pub struct EthPrecompiles {
pub precompiles: &'static Precompiles,
pub spec: SpecId,
}
impl EthPrecompiles {
pub fn new(spec: SpecId) -> Self {
Self {
precompiles: Precompiles::new(PrecompileSpecId::from_spec_id(spec)),
spec,
}
}
pub const fn warm_addresses(&self) -> &AddressSet {
self.precompiles.addresses_set()
}
pub fn contains(&self, address: &Address) -> bool {
self.precompiles.contains(address)
}
}
impl Clone for EthPrecompiles {
fn clone(&self) -> Self {
Self {
precompiles: self.precompiles,
spec: self.spec,
}
}
}
pub fn precompile_output_to_interpreter_result(
output: PrecompileOutput,
gas_limit: u64,
) -> InterpreterResult {
let bytes = if output.status.is_success_or_revert() {
output.bytes
} else {
Bytes::new()
};
let mut result = InterpreterResult {
result: InstructionResult::Return,
gas: Gas::new_with_regular_gas_and_reservoir(gas_limit, output.reservoir),
output: bytes,
};
result.gas.set_state_gas_spent(output.state_gas_used as i64);
result.gas.set_refill_amount(output.refill_amount);
result.gas.record_refund(output.gas_refunded);
if output.status.is_success_or_revert() {
if !result.gas.record_regular_cost(output.gas_used) {
result.gas.spend_all();
result.output = Bytes::new();
result.result = InstructionResult::PrecompileOOG;
return result;
}
} else {
result.gas.spend_all();
}
result.result = match output.status {
PrecompileStatus::Success => InstructionResult::Return,
PrecompileStatus::Revert => InstructionResult::Revert,
PrecompileStatus::Halt(halt_reason) => {
if halt_reason.is_oog() {
InstructionResult::PrecompileOOG
} else {
InstructionResult::PrecompileError
}
}
};
result
}
impl<CTX: ContextTr> PrecompileProvider<CTX> for EthPrecompiles {
type Output = InterpreterResult;
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
let spec = spec.into();
if spec == self.spec {
return false;
}
self.precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec));
self.spec = spec;
true
}
fn run(
&mut self,
context: &mut CTX,
inputs: &CallInputs,
) -> Result<Option<InterpreterResult>, String> {
let Some(precompile) = self.precompiles.get(&inputs.bytecode_address) else {
return Ok(None);
};
let output = precompile
.execute(
&inputs.input.as_bytes(context),
inputs.gas_limit,
inputs.reservoir,
)
.map_err(|e| e.to_string())?;
if let Some(halt_reason) = output.halt_reason() {
if !halt_reason.is_oog() && context.journal().depth() == 1 {
context
.local_mut()
.set_precompile_error_context(halt_reason.to_string());
}
}
let result = precompile_output_to_interpreter_result(output, inputs.gas_limit);
Ok(Some(result))
}
fn warm_addresses(&self) -> &AddressSet {
Self::warm_addresses(self)
}
fn contains(&self, address: &Address) -> bool {
Self::contains(self, address)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{instructions::EthInstructions, ExecuteEvm, MainContext};
use context::{Context, Evm, FrameStack, TxEnv};
use context_interface::result::{ExecutionResult, HaltReason, OutOfGasError};
use database::InMemoryDB;
use interpreter::interpreter::EthInterpreter;
use primitives::{address, hardfork::SpecId, TxKind, U256};
use state::AccountInfo;
const OVERSPEND_PRECOMPILE: Address = address!("0000000000000000000000000000000000000100");
#[derive(Debug)]
struct OverspendingPrecompiles {
inner: EthPrecompiles,
warm: AddressSet,
}
impl OverspendingPrecompiles {
fn new(spec: SpecId) -> Self {
let inner = EthPrecompiles::new(spec);
let mut warm = AddressSet::default();
warm.clone_from(inner.warm_addresses());
warm.insert(OVERSPEND_PRECOMPILE);
Self { inner, warm }
}
}
impl<CTX> PrecompileProvider<CTX> for OverspendingPrecompiles
where
CTX: ContextTr<Cfg: Cfg<Spec = SpecId>>,
{
type Output = InterpreterResult;
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
let changed =
<EthPrecompiles as PrecompileProvider<CTX>>::set_spec(&mut self.inner, spec);
self.warm.clone_from(self.inner.warm_addresses());
self.warm.insert(OVERSPEND_PRECOMPILE);
changed
}
fn run(
&mut self,
context: &mut CTX,
inputs: &CallInputs,
) -> Result<Option<Self::Output>, String> {
if inputs.bytecode_address == OVERSPEND_PRECOMPILE {
let output = PrecompileOutput {
status: PrecompileStatus::Success,
gas_used: u64::MAX,
gas_refunded: 0,
state_gas_used: 0,
reservoir: inputs.reservoir,
refill_amount: 0,
bytes: Bytes::from_static(b"unreliable"),
};
return Ok(Some(precompile_output_to_interpreter_result(
output,
inputs.gas_limit,
)));
}
<EthPrecompiles as PrecompileProvider<CTX>>::run(&mut self.inner, context, inputs)
}
fn warm_addresses(&self) -> &AddressSet {
&self.warm
}
}
#[test]
fn overspending_precompile_halts_tx_with_precompile_oog() {
let caller = address!("0000000000000000000000000000000000000001");
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(10).pow(U256::from(18)),
..Default::default()
},
);
let spec = SpecId::default();
let ctx = Context::mainnet().with_db(db);
let mut evm = Evm {
ctx,
inspector: (),
instruction: EthInstructions::<EthInterpreter, _>::new_mainnet_with_spec(spec),
precompiles: OverspendingPrecompiles::new(spec),
frame_stack: FrameStack::new_prealloc(8),
};
let tx = TxEnv::builder()
.caller(caller)
.kind(TxKind::Call(OVERSPEND_PRECOMPILE))
.gas_limit(100_000)
.build()
.unwrap();
let exec = evm.transact_one(tx).expect("handler returned an error");
match exec {
ExecutionResult::Halt { reason, .. } => {
assert_eq!(
reason,
HaltReason::OutOfGas(OutOfGasError::Precompile),
"expected precompile OOG halt for over-spending precompile",
);
}
ExecutionResult::Success { .. } => panic!(
"before-fix behavior leaked: over-spending precompile reported Success \
instead of halting with PrecompileOOG"
),
ExecutionResult::Revert { .. } => panic!("expected Halt(PrecompileOOG), got Revert"),
}
}
}