use std::path::Path;
use alloy::primitives::{keccak256, Address, Bytes, Log, U256};
use revm::context::TxEnv;
use revm::context_interface::result::ExecutionResult;
pub use revm::context_interface::result::Output;
use revm::context_interface::ContextTr;
use revm::database::CacheDB;
use revm::database_interface::EmptyDB;
use revm::primitives::TxKind;
use revm::{ExecuteCommitEvm, ExecuteEvm, MainBuilder, MainContext};
use serde_json::Value;
pub type FixtureEvm = revm::MainnetEvm<revm::handler::MainnetContext<CacheDB<EmptyDB>>>;
#[derive(Debug)]
pub enum Verdict {
Accepted { output: Output, logs: Vec<Log> },
Reverted(Bytes),
Halted(String),
}
#[must_use]
pub fn new_fixture_evm() -> FixtureEvm {
let db = CacheDB::new(EmptyDB::default());
revm::context::Context::mainnet()
.with_db(db)
.build_mainnet()
}
pub fn set_disable_nonce_check(evm: &mut FixtureEvm, disable: bool) {
evm.ctx.cfg.disable_nonce_check = disable;
}
pub fn set_code_size_limits(evm: &mut FixtureEvm, max: Option<usize>) {
evm.ctx.cfg.limit_contract_code_size = max;
evm.ctx.cfg.limit_contract_initcode_size = max;
}
pub fn set_block_gas_limit(evm: &mut FixtureEvm, gas: u64) {
evm.ctx.modify_block(|block| {
block.gas_limit = gas;
});
}
pub fn set_tx_gas_limit_cap(evm: &mut FixtureEvm, cap: u64) {
evm.ctx.cfg.tx_gas_limit_cap = Some(cap);
}
pub fn transact(evm: &mut FixtureEvm, spec: TxSpec) -> Verdict {
#[expect(clippy::expect_used)] let tx = match spec {
TxSpec::Deploy { init_code, gas } => TxEnv::builder()
.kind(TxKind::Create)
.gas_limit(gas)
.data(init_code)
.build()
.expect("valid deploy tx env"),
TxSpec::Call { to, data, gas } => TxEnv::builder()
.kind(TxKind::Call(to))
.gas_limit(gas)
.data(data)
.build()
.expect("valid call tx env"),
};
match evm.transact(tx) {
Ok(res) => {
let out = match res.result {
ExecutionResult::Success { output, logs, .. } => {
evm.commit(res.state);
Verdict::Accepted { output, logs }
}
ExecutionResult::Revert { output, .. } => {
evm.commit(res.state);
Verdict::Reverted(output)
}
ExecutionResult::Halt { reason, .. } => {
evm.commit(res.state);
Verdict::Halted(format!("halted: {reason:?}"))
}
};
out
}
Err(e) => Verdict::Halted(format!("transact error: {e:?}")),
}
}
pub fn transact_with_gas(evm: &mut FixtureEvm, spec: TxSpec) -> (Verdict, u64) {
#[expect(clippy::expect_used)] let tx = match spec {
TxSpec::Deploy { init_code, gas } => TxEnv::builder()
.kind(TxKind::Create)
.gas_limit(gas)
.data(init_code)
.build()
.expect("valid deploy tx env"),
TxSpec::Call { to, data, gas } => TxEnv::builder()
.kind(TxKind::Call(to))
.gas_limit(gas)
.data(data)
.build()
.expect("valid call tx env"),
};
match evm.transact(tx) {
Ok(res) => {
let gas_used = res.result.tx_gas_used();
let out = match res.result {
ExecutionResult::Success { output, logs, .. } => {
evm.commit(res.state);
Verdict::Accepted { output, logs }
}
ExecutionResult::Revert { output, .. } => {
evm.commit(res.state);
Verdict::Reverted(output)
}
ExecutionResult::Halt { reason, .. } => {
evm.commit(res.state);
Verdict::Halted(format!("halted: {reason:?}"))
}
};
(out, gas_used)
}
Err(e) => (Verdict::Halted(format!("transact error: {e:?}")), 0),
}
}
pub enum TxSpec {
Deploy { init_code: Bytes, gas: u64 },
Call { to: Address, data: Bytes, gas: u64 },
}
pub fn deploy(evm: &mut FixtureEvm, init_code: Bytes, gas: u64) -> Result<Address, String> {
match transact(evm, TxSpec::Deploy { init_code, gas }) {
Verdict::Accepted {
output: Output::Create(_, Some(addr)),
..
} => Ok(addr),
Verdict::Accepted {
output: Output::Create(_, None),
..
} => Err("deploy succeeded without an address (Create output missing addr)".to_string()),
Verdict::Accepted { output, .. } => Err(format!(
"deploy succeeded with non-Create output {output:?}"
)),
Verdict::Reverted(r) => Err(format!("deploy reverted: {r:?}")),
Verdict::Halted(h) => Err(format!("deploy halted: {h}")),
}
}
pub fn read_address(
evm: &mut FixtureEvm,
to: Address,
data: Bytes,
gas: u64,
) -> Result<Address, String> {
let out = call_bytes(evm, to, data, gas)?;
if out.len() < 32 {
return Err(format!("address getter returned {} bytes", out.len()));
}
Ok(Address::from_slice(&out.as_ref()[12..32]))
}
pub fn call_bytes(
evm: &mut FixtureEvm,
to: Address,
data: Bytes,
gas: u64,
) -> Result<Bytes, String> {
match transact(evm, TxSpec::Call { to, data, gas }) {
Verdict::Accepted {
output: Output::Call(b),
..
} => Ok(b),
Verdict::Accepted {
output: Output::Create(..),
..
} => Err("call unexpectedly returned a Create output".to_string()),
Verdict::Reverted(r) => Err(format!("call reverted: {r:?}")),
Verdict::Halted(h) => Err(format!("call halted: {h}")),
}
}
pub fn seed_slots(evm: &mut FixtureEvm, account: Address, slots: &[(U256, U256)]) {
let db = evm.ctx.db_mut();
for &(slot, value) in slots {
db.insert_account_storage(account, slot, value).ok();
}
}
pub fn set_native_balance(evm: &mut FixtureEvm, account: Address, balance: U256) {
use revm::DatabaseRef as _;
let db = evm.ctx.db_mut();
let existing = db.basic_ref(account).ok().flatten().unwrap_or_default();
db.insert_account_info(
account,
revm::state::AccountInfo {
balance,
..existing
},
);
}
pub fn native_balance_of(evm: &mut FixtureEvm, account: Address) -> U256 {
use revm::DatabaseRef as _;
let db = evm.ctx.db_mut();
db.basic_ref(account)
.ok()
.flatten()
.map(|a| a.balance)
.unwrap_or_default()
}
#[must_use]
pub fn selector(sig: &str) -> [u8; 4] {
let h = keccak256(sig.as_bytes());
let mut out = [0u8; 4];
out.copy_from_slice(&h.0[0..4]);
out
}
#[must_use]
pub fn decode_error_string(reason: &[u8]) -> Option<String> {
if reason.len() < 4 || reason[..4] != [0x08, 0xc3, 0x79, 0xa0] {
return None;
}
let data = &reason[4..];
if data.len() < 64 {
return None;
}
let len = U256::from_be_bytes::<32>(data[32..64].try_into().ok()?).to::<usize>();
if 64 + len > data.len() {
return None;
}
Some(String::from_utf8_lossy(&data[64..64 + len]).into_owned())
}
pub fn parse_foundry_creation_bytecode(artifact_json: &str) -> Result<Vec<u8>, String> {
let v: Value =
serde_json::from_str(artifact_json).map_err(|e| format!("invalid artifact JSON: {e}"))?;
let hex_str = v["bytecode"]["object"]
.as_str()
.ok_or_else(|| "artifact has no bytecode.object".to_string())?;
alloy::hex::decode(hex_str.trim_start_matches("0x")).map_err(|e| format!("hex decode: {e}"))
}
pub fn load_foundry_creation_bytecode(
dir: &Path,
file: &str,
contract: &str,
) -> Result<Vec<u8>, String> {
let path = dir.join(file).join(format!("{contract}.json"));
let raw = std::fs::read_to_string(&path)
.map_err(|e| format!("read artifact {}: {e}", path.display()))?;
parse_foundry_creation_bytecode(&raw)
}