Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,16 @@ serde = { version = "1", default-features = false, features = ["derive"] }
thiserror = { version = "2.0.0", default-features = false }
serde_json = "1"
test-case = "3"
[patch.crates-io]
revm = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" }
16 changes: 16 additions & 0 deletions crates/evm/src/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ pub enum BlockValidationError {
/// The error message.
message: String,
},
/// EVM error during builder deposit requests contract call [EIP-8282]
///
/// [EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282
#[error("failed to apply builder deposit requests contract call: {message}")]
BuilderDepositRequestsContractCall {
/// The error message.
message: String,
},
/// EVM error during builder exit requests contract call [EIP-8282]
///
/// [EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282
#[error("failed to apply builder exit requests contract call: {message}")]
BuilderExitRequestsContractCall {
/// The error message.
message: String,
},
/// Error when decoding deposit requests from receipts [EIP-6110]
///
/// [EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110
Expand Down
204 changes: 204 additions & 0 deletions crates/evm/src/block/system_calls/eip7997.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! [EIP-7997](https://eips.ethereum.org/EIPS/eip-7997) deterministic factory predeploy.
//!
//! EIP-7997 inserts a minimal `CREATE2` factory as a contract at a fixed address upon activation
//! of the Amsterdam hardfork, enabling deterministic deployments at identical addresses across
//! EVM chains.

use crate::{block::BlockExecutionError, Evm};
use alloy_hardforks::EthereumHardforks;
use alloy_primitives::{address, b256, bytes, Address, Bytes, B256};
use revm::{
context::Block,
state::{Account, AccountStatus, Bytecode, EvmState},
Database,
};

/// Address of the EIP-7997 deterministic `CREATE2` factory predeploy.
pub const FACTORY_ADDRESS: Address = address!("0x0000000000000000000000000000000000000012");

/// Runtime bytecode deployed at [`FACTORY_ADDRESS`] on activation of EIP-7997.
///
/// When called, it invokes `CREATE2` using the first 32 bytes of the input as the salt and the
/// remaining bytes as the init code, forwarding the call value.
pub const FACTORY_CODE: Bytes = bytes!(
"0x60203610602f5760003560203603806020600037600034f5806026573d600060003e3d6000fd5b60005260206000f35b60006000fd"
);

/// keccak256 hash of [`FACTORY_CODE`].
///
/// Used to detect whether the predeploy is already present, keeping the insertion idempotent.
pub const FACTORY_CODE_HASH: B256 =
b256!("0x2b2a8b8ac51cd14e2371a09cc5fa3737e74cabd8dafad0573ebca41c7cd5b51f");

/// Builds the pre-block state change that inserts the EIP-7997 factory bytecode at
/// [`FACTORY_ADDRESS`].
///
/// Returns `None` (a no-op) when Amsterdam is not active, or when the factory code is already
/// present at the address. Comparing the existing code hash makes the insertion idempotent, so it
/// only mutates state once: on the block that activates Amsterdam.
///
/// Only the code (and its hash) is set; any existing account fields, such as balance and nonce,
/// are preserved, as EIP-7997 does not specify them.
///
/// Note: this does not commit the state change to the database, it only constructs it.
#[inline]
pub(crate) fn build_factory_predeploy_state(
spec: impl EthereumHardforks,
evm: &mut impl Evm,
) -> Result<Option<EvmState>, BlockExecutionError> {
if !spec.is_amsterdam_active_at_timestamp(evm.block().timestamp().saturating_to()) {
return Ok(None);
}

let mut info = evm
.db_mut()
.basic(FACTORY_ADDRESS)
.map_err(|_| BlockExecutionError::msg("could not load EIP-7997 factory account"))?
.unwrap_or_default();

// Idempotent: only insert the factory on the block that activates Amsterdam. On later blocks
// the code is already present, so this is a no-op.
if info.code_hash == FACTORY_CODE_HASH {
return Ok(None);
}

info.code_hash = FACTORY_CODE_HASH;
info.code = Some(Bytecode::new_legacy(FACTORY_CODE));

let mut account = Account::from(info);
account.status = AccountStatus::Touched;

Ok(Some(EvmState::from_iter([(FACTORY_ADDRESS, account)])))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{EthEvmFactory, EvmEnv, EvmFactory};
use alloy_hardforks::{EthereumHardfork, ForkCondition};
use alloy_primitives::{keccak256, U256};
use revm::{
context::{BlockEnv, CfgEnv},
database::{CacheDB, EmptyDB, State},
primitives::hardfork::SpecId,
state::AccountInfo,
};

/// Minimal spec that reports Amsterdam as active from genesis when `amsterdam` is set.
struct TestSpec {
amsterdam: bool,
}

impl EthereumHardforks for TestSpec {
fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
if self.amsterdam && fork == EthereumHardfork::Amsterdam {
ForkCondition::Timestamp(0)
} else {
ForkCondition::Never
}
}
}

fn evm_with(db: CacheDB<EmptyDB>) -> impl Evm {
let mut cfg = CfgEnv::default();
cfg.spec = SpecId::AMSTERDAM;
let env = EvmEnv {
block_env: BlockEnv { timestamp: U256::from(1), ..Default::default() },
cfg_env: cfg,
};
let db = State::builder().with_database(db).with_bundle_update().build();
EthEvmFactory.create_evm(db, env)
}

#[test]
fn factory_code_hash_matches_code() {
assert_eq!(keccak256(&FACTORY_CODE), FACTORY_CODE_HASH);
assert_eq!(Bytecode::new_legacy(FACTORY_CODE).hash_slow(), FACTORY_CODE_HASH);
}

#[test]
fn inserts_factory_when_amsterdam_active() {
let mut evm = evm_with(CacheDB::new(EmptyDB::new()));

let state = build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm)
.unwrap()
.expect("factory predeploy state");

let account = state.get(&FACTORY_ADDRESS).expect("factory account");
assert_eq!(account.info.code_hash, FACTORY_CODE_HASH);
assert_eq!(account.info.code.as_ref().unwrap().original_bytes(), FACTORY_CODE);
assert!(account.is_touched());
assert_eq!(state.len(), 1);
}

#[test]
fn idempotent_when_already_deployed() {
let mut db = CacheDB::new(EmptyDB::new());
// Pretend the factory is already deployed at the target address.
db.insert_account_info(
FACTORY_ADDRESS,
AccountInfo {
code_hash: FACTORY_CODE_HASH,
code: Some(Bytecode::new_legacy(FACTORY_CODE)),
..Default::default()
},
);
let mut evm = evm_with(db);

assert!(build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm)
.unwrap()
.is_none());
}

#[test]
fn commit_records_change_and_preserves_balance() {
use revm::{database::states::bundle_state::BundleRetention, DatabaseCommit};

// Account pre-exists with a balance but no code.
let balance = U256::from(7u64);
let mut db = CacheDB::new(EmptyDB::new());
db.insert_account_info(FACTORY_ADDRESS, AccountInfo::from_balance(balance));

// Built inline (not via `evm_with`) so the concrete `State` DB type is preserved and its
// `merge_transitions`/`take_bundle` methods remain callable.
let mut cfg = CfgEnv::default();
cfg.spec = SpecId::AMSTERDAM;
let env = EvmEnv {
block_env: BlockEnv { timestamp: U256::from(1), ..Default::default() },
cfg_env: cfg,
};
let inner = State::builder().with_database(db).with_bundle_update().build();
let mut evm = EthEvmFactory.create_evm(inner, env);

let state = build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm)
.unwrap()
.unwrap();
// Balance is preserved on the pre-existing account.
assert_eq!(state.get(&FACTORY_ADDRESS).unwrap().info.balance, balance);
evm.db_mut().commit(state);

// Post-state reads back the code and the preserved balance.
let info = evm.db_mut().basic(FACTORY_ADDRESS).unwrap().unwrap();
assert_eq!(info.code_hash, FACTORY_CODE_HASH);
assert_eq!(info.balance, balance);

// The bundle records the transition from (balance, no code) to (balance, factory code),
// i.e. the original is tracked correctly rather than the post-insertion state.
evm.db_mut().merge_transitions(BundleRetention::Reverts);
let bundle = evm.db_mut().take_bundle();
let acct = bundle.account(&FACTORY_ADDRESS).expect("bundle account");
assert_eq!(acct.info.as_ref().unwrap().code_hash, FACTORY_CODE_HASH);
let original = acct.original_info.as_ref().expect("original info");
assert_eq!(original.balance, balance);
assert!(original.is_empty_code_hash(), "original should have no code");
}

#[test]
fn noop_before_amsterdam() {
let mut evm = evm_with(CacheDB::new(EmptyDB::new()));

assert!(build_factory_predeploy_state(&TestSpec { amsterdam: false }, &mut evm)
.unwrap()
.is_none());
}
}
125 changes: 125 additions & 0 deletions crates/evm/src/block/system_calls/eip8282.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! [EIP-8282](https://eips.ethereum.org/EIPS/eip-8282) builder execution requests system calls.
//!
//! EIP-8282 introduces two new EIP-7685 request types, processed by post-block system calls to
//! two predeploys, starting at the Amsterdam hardfork:
//!
//! * **Builder deposit requests** (request type `0x03`)
//! * **Builder exit requests** (request type `0x04`)
//!
//! Each predeploy maintains a FIFO request queue and an "excess" counter in storage slot 0 (the
//! same design as EIP-7002/7251). At the end of every block from Amsterdam onward the contract is
//! invoked as `SYSTEM_ADDRESS` with empty calldata, which drains the queue (returning the encoded
//! requests) and, on first invocation, initializes the excess counter from the
//! `EXCESS_INHIBITOR` sentinel (`2^256 - 1`) to `0`.

use crate::{
block::{BlockExecutionError, BlockValidationError},
Evm,
};
use alloc::format;
use alloy_eips::eip7002::SYSTEM_ADDRESS;
use alloy_primitives::{address, Address, Bytes};
use core::fmt::Debug;
use revm::context_interface::result::{ExecutionResult, ResultAndState};

/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for EIP-8282 builder
/// deposit requests.
pub const BUILDER_DEPOSIT_REQUEST_TYPE: u8 = 0x03;

/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for EIP-8282 builder exit
/// requests.
pub const BUILDER_EXIT_REQUEST_TYPE: u8 = 0x04;

/// The address of the EIP-8282 builder deposit requests predeploy.
pub const BUILDER_DEPOSIT_REQUEST_PREDEPLOY_ADDRESS: Address =
address!("0x0000BFF46984E3725691FA540A8C7589300D8282");

/// The address of the EIP-8282 builder exit requests predeploy.
pub const BUILDER_EXIT_REQUEST_PREDEPLOY_ADDRESS: Address =
address!("0x000064D678505AD48F8CCB093BC65613800E8282");

/// Applies the post-block call to the EIP-8282 builder deposit requests contract.
///
/// Note: this does not commit the state changes to the database, it only transacts the call.
#[inline]
pub(crate) fn transact_builder_deposit_requests_contract_call<Halt>(
evm: &mut impl Evm<HaltReason = Halt>,
) -> Result<ResultAndState<Halt>, BlockExecutionError> {
// At the end of processing any execution block where Amsterdam is active, call the builder
// deposit requests contract as `SYSTEM_ADDRESS` with empty calldata.
match evm.transact_system_call(
SYSTEM_ADDRESS,
BUILDER_DEPOSIT_REQUEST_PREDEPLOY_ADDRESS,
Bytes::new(),
) {
Ok(res) => Ok(res),
Err(e) => Err(BlockValidationError::BuilderDepositRequestsContractCall {
message: format!("execution failed: {e}"),
}
.into()),
}
}

/// Applies the post-block call to the EIP-8282 builder exit requests contract.
///
/// Note: this does not commit the state changes to the database, it only transacts the call.
#[inline]
pub(crate) fn transact_builder_exit_requests_contract_call<Halt>(
evm: &mut impl Evm<HaltReason = Halt>,
) -> Result<ResultAndState<Halt>, BlockExecutionError> {
match evm.transact_system_call(
SYSTEM_ADDRESS,
BUILDER_EXIT_REQUEST_PREDEPLOY_ADDRESS,
Bytes::new(),
) {
Ok(res) => Ok(res),
Err(e) => Err(BlockValidationError::BuilderExitRequestsContractCall {
message: format!("execution failed: {e}"),
}
.into()),
}
}

/// Extracts the builder deposit requests from the execution output.
#[inline]
pub(crate) fn deposit_post_commit<Halt: Debug>(
result: ExecutionResult<Halt>,
) -> Result<Bytes, BlockExecutionError> {
match result {
ExecutionResult::Success { output, .. } => Ok(output.into_data()),
ExecutionResult::Revert { output, .. } => {
Err(BlockValidationError::BuilderDepositRequestsContractCall {
message: format!("execution reverted: {output}"),
}
.into())
}
ExecutionResult::Halt { reason, .. } => {
Err(BlockValidationError::BuilderDepositRequestsContractCall {
message: format!("execution halted: {reason:?}"),
}
.into())
}
}
}

/// Extracts the builder exit requests from the execution output.
#[inline]
pub(crate) fn exit_post_commit<Halt: Debug>(
result: ExecutionResult<Halt>,
) -> Result<Bytes, BlockExecutionError> {
match result {
ExecutionResult::Success { output, .. } => Ok(output.into_data()),
ExecutionResult::Revert { output, .. } => {
Err(BlockValidationError::BuilderExitRequestsContractCall {
message: format!("execution reverted: {output}"),
}
.into())
}
ExecutionResult::Halt { reason, .. } => {
Err(BlockValidationError::BuilderExitRequestsContractCall {
message: format!("execution halted: {reason:?}"),
}
.into())
}
}
}
Loading