From ef5340b695e151be66805867c9445dd213fdece5 Mon Sep 17 00:00:00 2001 From: chatton Date: Thu, 26 Feb 2026 12:45:54 +0000 Subject: [PATCH 1/6] fix: fix allow list logic --- crates/ev-revm/src/deploy.rs | 21 +++++++++++ crates/ev-revm/src/handler.rs | 36 +++++++++++++++--- crates/node/src/txpool.rs | 71 +++++++++++++++++++++++------------ 3 files changed, 100 insertions(+), 28 deletions(-) diff --git a/crates/ev-revm/src/deploy.rs b/crates/ev-revm/src/deploy.rs index 620697f..6cf0202 100644 --- a/crates/ev-revm/src/deploy.rs +++ b/crates/ev-revm/src/deploy.rs @@ -80,3 +80,24 @@ pub fn check_deploy_allowed( Err(DeployCheckError::NotAllowed) } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + + #[test] + fn empty_allowlist_allows_any_caller() { + let settings = DeployAllowlistSettings::new(vec![], 0); + let caller = address!("0x00000000000000000000000000000000000000aa"); + assert!(settings.is_allowed(caller)); + } + + #[test] + fn check_deploy_allowed_with_empty_settings_allows() { + let settings = DeployAllowlistSettings::new(vec![], 0); + let caller = address!("0x00000000000000000000000000000000000000bb"); + let result = check_deploy_allowed(Some(&settings), caller, true, 0); + assert!(result.is_ok()); + } +} diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index e6f87d8..32fd52f 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -58,12 +58,13 @@ impl EvHandler { self.redirect } - const fn deploy_allowlist_for_block( - &self, - block_number: u64, - ) -> Option<&DeployAllowlistSettings> { + fn deploy_allowlist_for_block(&self, block_number: u64) -> Option<&DeployAllowlistSettings> { match self.deploy_allowlist.as_ref() { - Some(settings) if settings.is_active(block_number) => Some(settings), + Some(settings) + if settings.is_active(block_number) && !settings.allowlist().is_empty() => + { + Some(settings) + } _ => None, } } @@ -1334,6 +1335,31 @@ mod tests { assert!(matches!(result, Err(EVMError::Custom(_)))); } + #[test] + fn allow_deploy_when_allowlist_is_empty() { + let caller = address!("0x00000000000000000000000000000000000000cc"); + let allowlist = DeployAllowlistSettings::new(vec![], 0); + + let mut ctx = Context::mainnet().with_db(EmptyDB::default()); + ctx.block.number = U256::from(1); + ctx.cfg.spec = SpecId::CANCUN; + ctx.cfg.disable_nonce_check = true; + ctx.tx.caller = caller; + ctx.tx.kind = TxKind::Create; + ctx.tx.gas_limit = 1_000_000; + // gas_price=0 so no balance is required + ctx.tx.gas_price = 0; + + let mut evm = EvEvm::new(ctx, NoOpInspector, None); + let handler: TestHandler = EvHandler::new(None, Some(allowlist)); + + let result = handler.validate_against_state_and_deduct_caller(&mut evm); + assert!( + result.is_ok(), + "empty allowlist should allow any caller to deploy, got: {result:?}" + ); + } + fn setup_evm(redirect: BaseFeeRedirect, beneficiary: Address) -> (TestEvm, TestHandler) { let mut ctx = Context::mainnet().with_db(EmptyDB::default()); ctx.block.basefee = BASE_FEE; diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs index efeff56..a1d6fa2 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -404,31 +404,34 @@ where Client: StateProviderFactory, { // Unified deploy allowlist check (covers both Ethereum and EvNode txs). + // empty allowlist = permissionless, skip enforcement if let Some(settings) = &self.deploy_allowlist { - let is_top_level_create = match pooled.transaction().inner() { - EvTxEnvelope::Ethereum(tx) => alloy_consensus::Transaction::is_create(tx), - EvTxEnvelope::EvNode(ref signed) => { - let tx = signed.tx(); - tx.calls.first().map(|c| c.to.is_create()).unwrap_or(false) + if !settings.allowlist().is_empty() { + let is_top_level_create = match pooled.transaction().inner() { + EvTxEnvelope::Ethereum(tx) => alloy_consensus::Transaction::is_create(tx), + EvTxEnvelope::EvNode(ref signed) => { + let tx = signed.tx(); + tx.calls.first().map(|c| c.to.is_create()).unwrap_or(false) + } + }; + let caller = pooled.transaction().signer(); + let block_number = self.inner.client().best_block_number().map_err( + |err: reth_provider::ProviderError| { + InvalidPoolTransactionError::other(EvTxPoolError::StateProvider( + err.to_string(), + )) + }, + )?; + if let Err(_e) = ev_revm::deploy::check_deploy_allowed( + Some(settings), + caller, + is_top_level_create, + block_number, + ) { + return Err(InvalidPoolTransactionError::other( + EvTxPoolError::DeployNotAllowed, + )); } - }; - let caller = pooled.transaction().signer(); - let block_number = self.inner.client().best_block_number().map_err( - |err: reth_provider::ProviderError| { - InvalidPoolTransactionError::other(EvTxPoolError::StateProvider( - err.to_string(), - )) - }, - )?; - if let Err(_e) = ev_revm::deploy::check_deploy_allowed( - Some(settings), - caller, - is_top_level_create, - block_number, - ) { - return Err(InvalidPoolTransactionError::other( - EvTxPoolError::DeployNotAllowed, - )); } } @@ -797,6 +800,28 @@ mod tests { ); } + #[test] + fn evnode_create_allowed_when_allowlist_is_empty() { + let settings = ev_revm::deploy::DeployAllowlistSettings::new(vec![], 0); + let validator = create_test_validator(Some(settings)); + + let gas_limit = 200_000u64; + let max_fee_per_gas = 1_000_000_000u128; + let signed_tx = create_non_sponsored_evnode_create_tx(gas_limit, max_fee_per_gas); + + let signer = Address::from([0x33u8; 20]); + let pooled = create_pooled_tx(signed_tx, signer); + + let sender_balance = *pooled.cost() + U256::from(1); + let mut state: Option> = None; + + let result = validator.validate_evnode(&pooled, sender_balance, &mut state); + assert!( + result.is_ok(), + "empty allowlist should allow any caller to deploy, got: {result:?}" + ); + } + /// Tests pool-level deploy allowlist rejection for `EvNode` CREATE when caller not allowlisted. #[test] fn evnode_create_rejected_when_not_allowlisted() { From 65874969648d4286b60ebd609ded2a60284b6894 Mon Sep 17 00:00:00 2001 From: chatton Date: Thu, 26 Feb 2026 12:53:29 +0000 Subject: [PATCH 2/6] test: adding additional test coverage --- crates/ev-revm/src/deploy.rs | 36 ++++++++++++++++++ crates/ev-revm/src/handler.rs | 72 +++++++++++++++++++++++++++++++++++ crates/node/src/txpool.rs | 67 ++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/crates/ev-revm/src/deploy.rs b/crates/ev-revm/src/deploy.rs index 6cf0202..d516d8f 100644 --- a/crates/ev-revm/src/deploy.rs +++ b/crates/ev-revm/src/deploy.rs @@ -100,4 +100,40 @@ mod tests { let result = check_deploy_allowed(Some(&settings), caller, true, 0); assert!(result.is_ok()); } + + #[test] + fn check_deploy_allowed_with_none_settings_allows() { + let caller = address!("0x00000000000000000000000000000000000000cc"); + let result = check_deploy_allowed(None, caller, true, 0); + assert!(result.is_ok()); + } + + #[test] + fn allowlisted_caller_is_allowed() { + let caller = address!("0x00000000000000000000000000000000000000aa"); + let settings = DeployAllowlistSettings::new(vec![caller], 0); + assert!(settings.is_allowed(caller)); + let result = check_deploy_allowed(Some(&settings), caller, true, 0); + assert!(result.is_ok()); + } + + #[test] + fn non_allowlisted_caller_is_denied() { + let allowed = address!("0x00000000000000000000000000000000000000aa"); + let caller = address!("0x00000000000000000000000000000000000000bb"); + let settings = DeployAllowlistSettings::new(vec![allowed], 0); + assert!(!settings.is_allowed(caller)); + let result = check_deploy_allowed(Some(&settings), caller, true, 0); + assert_eq!(result, Err(DeployCheckError::NotAllowed)); + } + + #[test] + fn call_tx_always_allowed_regardless_of_allowlist() { + let allowed = address!("0x00000000000000000000000000000000000000aa"); + let caller = address!("0x00000000000000000000000000000000000000bb"); + let settings = DeployAllowlistSettings::new(vec![allowed], 0); + // caller is not in the allowlist, but is_top_level_create=false so it's allowed + let result = check_deploy_allowed(Some(&settings), caller, false, 0); + assert!(result.is_ok()); + } } diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 32fd52f..e154606 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -1360,6 +1360,78 @@ mod tests { ); } + #[test] + fn allow_deploy_when_allowlist_is_none() { + let caller = address!("0x00000000000000000000000000000000000000dd"); + + let mut ctx = Context::mainnet().with_db(EmptyDB::default()); + ctx.block.number = U256::from(1); + ctx.cfg.spec = SpecId::CANCUN; + ctx.cfg.disable_nonce_check = true; + ctx.tx.caller = caller; + ctx.tx.kind = TxKind::Create; + ctx.tx.gas_limit = 1_000_000; + ctx.tx.gas_price = 0; + + let mut evm = EvEvm::new(ctx, NoOpInspector, None); + let handler: TestHandler = EvHandler::new(None, None); + + let result = handler.validate_against_state_and_deduct_caller(&mut evm); + assert!( + result.is_ok(), + "no allowlist configured should allow any caller to deploy, got: {result:?}" + ); + } + + #[test] + fn allow_deploy_for_allowlisted_caller() { + let caller = address!("0x00000000000000000000000000000000000000ee"); + let allowlist = DeployAllowlistSettings::new(vec![caller], 0); + + let mut ctx = Context::mainnet().with_db(EmptyDB::default()); + ctx.block.number = U256::from(1); + ctx.cfg.spec = SpecId::CANCUN; + ctx.cfg.disable_nonce_check = true; + ctx.tx.caller = caller; + ctx.tx.kind = TxKind::Create; + ctx.tx.gas_limit = 1_000_000; + ctx.tx.gas_price = 0; + + let mut evm = EvEvm::new(ctx, NoOpInspector, None); + let handler: TestHandler = EvHandler::new(None, Some(allowlist)); + + let result = handler.validate_against_state_and_deduct_caller(&mut evm); + assert!( + result.is_ok(), + "allowlisted caller should be allowed to deploy, got: {result:?}" + ); + } + + #[test] + fn call_tx_allowed_for_non_allowlisted_caller() { + let allowed = address!("0x00000000000000000000000000000000000000aa"); + let caller = address!("0x00000000000000000000000000000000000000ff"); + let allowlist = DeployAllowlistSettings::new(vec![allowed], 0); + + let mut ctx = Context::mainnet().with_db(EmptyDB::default()); + ctx.block.number = U256::from(1); + ctx.cfg.spec = SpecId::CANCUN; + ctx.cfg.disable_nonce_check = true; + ctx.tx.caller = caller; + ctx.tx.kind = TxKind::Call(Address::ZERO); + ctx.tx.gas_limit = 1_000_000; + ctx.tx.gas_price = 0; + + let mut evm = EvEvm::new(ctx, NoOpInspector, None); + let handler: TestHandler = EvHandler::new(None, Some(allowlist)); + + let result = handler.validate_against_state_and_deduct_caller(&mut evm); + assert!( + result.is_ok(), + "CALL tx should be allowed regardless of allowlist, got: {result:?}" + ); + } + fn setup_evm(redirect: BaseFeeRedirect, beneficiary: Address) -> (TestEvm, TestHandler) { let mut ctx = Context::mainnet().with_db(EmptyDB::default()); ctx.block.basefee = BASE_FEE; diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs index a1d6fa2..442664b 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -846,4 +846,71 @@ mod tests { assert!(matches!(err, InvalidPoolTransactionError::Other(_))); } } + + #[test] + fn evnode_create_allowed_when_allowlist_is_none() { + let validator = create_test_validator(None); + + let gas_limit = 200_000u64; + let max_fee_per_gas = 1_000_000_000u128; + let signed_tx = create_non_sponsored_evnode_create_tx(gas_limit, max_fee_per_gas); + + let signer = Address::from([0x44u8; 20]); + let pooled = create_pooled_tx(signed_tx, signer); + + let sender_balance = *pooled.cost() + U256::from(1); + let mut state: Option> = None; + + let result = validator.validate_evnode(&pooled, sender_balance, &mut state); + assert!( + result.is_ok(), + "no allowlist configured should allow any caller to deploy, got: {result:?}" + ); + } + + #[test] + fn evnode_create_allowed_for_allowlisted_caller() { + let signer = Address::from([0x55u8; 20]); + let settings = ev_revm::deploy::DeployAllowlistSettings::new(vec![signer], 0); + let validator = create_test_validator(Some(settings)); + + let gas_limit = 200_000u64; + let max_fee_per_gas = 1_000_000_000u128; + let signed_tx = create_non_sponsored_evnode_create_tx(gas_limit, max_fee_per_gas); + + let pooled = create_pooled_tx(signed_tx, signer); + + let sender_balance = *pooled.cost() + U256::from(1); + let mut state: Option> = None; + + let result = validator.validate_evnode(&pooled, sender_balance, &mut state); + assert!( + result.is_ok(), + "allowlisted caller should be allowed to deploy, got: {result:?}" + ); + } + + #[test] + fn evnode_call_allowed_for_non_allowlisted_caller() { + let allowed = Address::from([0x11u8; 20]); + let settings = ev_revm::deploy::DeployAllowlistSettings::new(vec![allowed], 0); + let validator = create_test_validator(Some(settings)); + + let gas_limit = 21_000u64; + let max_fee_per_gas = 1_000_000_000u128; + // CALL tx, not CREATE + let signed_tx = create_non_sponsored_evnode_tx(gas_limit, max_fee_per_gas); + + let signer = Address::from([0x66u8; 20]); // not allowlisted + let pooled = create_pooled_tx(signed_tx, signer); + + let sender_balance = *pooled.cost() + U256::from(1); + let mut state: Option> = None; + + let result = validator.validate_evnode(&pooled, sender_balance, &mut state); + assert!( + result.is_ok(), + "CALL tx should be allowed regardless of allowlist, got: {result:?}" + ); + } } From 011dd2ee6b22ba3bf37e3b394a4bcfb80adf44d2 Mon Sep 17 00:00:00 2001 From: chatton Date: Thu, 26 Feb 2026 13:00:45 +0000 Subject: [PATCH 3/6] chore: revert code logic, keep tests --- crates/ev-revm/src/handler.rs | 11 ++++---- crates/node/src/txpool.rs | 49 ++++++++++++++++------------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index e154606..0751e43 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -58,13 +58,12 @@ impl EvHandler { self.redirect } - fn deploy_allowlist_for_block(&self, block_number: u64) -> Option<&DeployAllowlistSettings> { + const fn deploy_allowlist_for_block( + &self, + block_number: u64, + ) -> Option<&DeployAllowlistSettings> { match self.deploy_allowlist.as_ref() { - Some(settings) - if settings.is_active(block_number) && !settings.allowlist().is_empty() => - { - Some(settings) - } + Some(settings) if settings.is_active(block_number) => Some(settings), _ => None, } } diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs index 442664b..8eeb831 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -404,34 +404,31 @@ where Client: StateProviderFactory, { // Unified deploy allowlist check (covers both Ethereum and EvNode txs). - // empty allowlist = permissionless, skip enforcement if let Some(settings) = &self.deploy_allowlist { - if !settings.allowlist().is_empty() { - let is_top_level_create = match pooled.transaction().inner() { - EvTxEnvelope::Ethereum(tx) => alloy_consensus::Transaction::is_create(tx), - EvTxEnvelope::EvNode(ref signed) => { - let tx = signed.tx(); - tx.calls.first().map(|c| c.to.is_create()).unwrap_or(false) - } - }; - let caller = pooled.transaction().signer(); - let block_number = self.inner.client().best_block_number().map_err( - |err: reth_provider::ProviderError| { - InvalidPoolTransactionError::other(EvTxPoolError::StateProvider( - err.to_string(), - )) - }, - )?; - if let Err(_e) = ev_revm::deploy::check_deploy_allowed( - Some(settings), - caller, - is_top_level_create, - block_number, - ) { - return Err(InvalidPoolTransactionError::other( - EvTxPoolError::DeployNotAllowed, - )); + let is_top_level_create = match pooled.transaction().inner() { + EvTxEnvelope::Ethereum(tx) => alloy_consensus::Transaction::is_create(tx), + EvTxEnvelope::EvNode(ref signed) => { + let tx = signed.tx(); + tx.calls.first().map(|c| c.to.is_create()).unwrap_or(false) } + }; + let caller = pooled.transaction().signer(); + let block_number = self.inner.client().best_block_number().map_err( + |err: reth_provider::ProviderError| { + InvalidPoolTransactionError::other(EvTxPoolError::StateProvider( + err.to_string(), + )) + }, + )?; + if let Err(_e) = ev_revm::deploy::check_deploy_allowed( + Some(settings), + caller, + is_top_level_create, + block_number, + ) { + return Err(InvalidPoolTransactionError::other( + EvTxPoolError::DeployNotAllowed, + )); } } From 3dced5026cd8be10d04ab4e0149425d12a114bf9 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 3 Mar 2026 09:29:49 +0000 Subject: [PATCH 4/6] chore: adding E2E to test factory creation --- crates/tests/src/e2e_tests.rs | 2 +- crates/tests/src/lib.rs | 2 + crates/tests/src/test_deploy_allowlist.rs | 205 ++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 crates/tests/src/test_deploy_allowlist.rs diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index f197768..18ed5b5 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -80,7 +80,7 @@ const REVERT_INITCODE: [u8; 17] = [ /// /// # Returns /// The deterministic contract address that will be created -fn contract_address_from_nonce(deployer: Address, nonce: u64) -> Address { +pub(crate) fn contract_address_from_nonce(deployer: Address, nonce: u64) -> Address { deployer.create(nonce) } diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 0db0a58..1022d18 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -8,6 +8,8 @@ pub mod common; #[cfg(test)] pub(crate) mod e2e_tests; #[cfg(test)] +mod test_deploy_allowlist; +#[cfg(test)] mod test_evolve_engine_api; // Re-export common test utilities diff --git a/crates/tests/src/test_deploy_allowlist.rs b/crates/tests/src/test_deploy_allowlist.rs new file mode 100644 index 0000000..335f836 --- /dev/null +++ b/crates/tests/src/test_deploy_allowlist.rs @@ -0,0 +1,205 @@ +use alloy_consensus::TxReceipt; +use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag}; +use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; +use alloy_rpc_types::{ + eth::{Block, Header, Receipt, Transaction, TransactionInput, TransactionRequest}, + BlockId, +}; +use eyre::Result; +use reth_e2e_test_utils::{ + testsuite::{ + setup::{NetworkSetup, Setup}, + Environment, + }, + transaction::TransactionTestContext, + wallet::Wallet, +}; +use reth_rpc_api::clients::EthApiClient; + +use crate::{ + common::{create_test_chain_spec_with_deploy_allowlist, e2e_test_tree_config, TEST_CHAIN_ID}, + e2e_tests::{build_block_with_transactions, contract_address_from_nonce}, +}; +use ev_node::{EvolveEngineTypes, EvolveNode}; + +/// Initcode for a minimal CREATE2 factory contract. +/// +/// Runtime behavior: reads salt from calldata[0:32] and child initcode from +/// calldata[32:], then deploys the child using CREATE2 with value=0. +/// Returns the deployed child address as a 32-byte value. +const CREATE2_FACTORY_INITCODE: [u8; 40] = alloy_primitives::hex!( + "601c600c600039601c6000f3" // initcode: copies 28-byte runtime to memory and returns it + "36602090038060206000376000359060006000f560005260206000f3" // runtime +); + +/// Initcode for a minimal child contract deployed via the CREATE2 factory. +/// +/// The deployed contract stores 0x42 in memory and returns it (32 bytes) when called. +const CREATE2_CHILD_INITCODE: [u8; 22] = alloy_primitives::hex!( + "600a600c600039600a6000f3" // initcode: copies 10-byte runtime to memory and returns it + "604260005260206000f3" // runtime: returns 0x42 +); + +/// Tests that a non-allowlisted account can deploy contracts indirectly through a +/// factory contract using CREATE2, even when the deploy allowlist is active. +/// +/// # Test Flow +/// 1. Create a deploy allowlist containing only `allowed_deployer` +/// 2. `allowed_deployer` deploys a CREATE2 factory contract (top-level CREATE, allowed) +/// 3. `non_allowlisted` account calls the factory, which internally uses CREATE2 +/// 4. Verify the child contract is deployed at the expected deterministic address +/// +/// # What It Tests +/// - deploy allowlist only blocks top-level CREATE transactions +/// - contract-to-contract CREATE2 bypasses the allowlist (by design) +/// - the factory pattern works as an indirect deployment mechanism +/// +/// # Success Criteria +/// - factory deploys successfully from allowlisted account +/// - non-allowlisted account's call to the factory succeeds +/// - child contract appears at the predicted CREATE2 address +#[tokio::test(flavor = "multi_thread")] +async fn test_e2e_deploy_allowlist_permits_create2_via_factory() -> Result<()> { + reth_tracing::init_test_tracing(); + + let mut wallets = Wallet::new(2).with_chain_id(TEST_CHAIN_ID).wallet_gen(); + let allowed_deployer = wallets.remove(0); + let non_allowlisted = wallets.remove(0); + + let chain_spec = create_test_chain_spec_with_deploy_allowlist(vec![allowed_deployer.address()]); + let chain_id = chain_spec.chain().id(); + + let mut setup = Setup::::default() + .with_chain_spec(chain_spec) + .with_network(NetworkSetup::single_node()) + .with_dev_mode(true) + .with_tree_config(e2e_test_tree_config()); + + let mut env = Environment::::default(); + setup.apply::(&mut env).await?; + + let parent_block = env.node_clients[0] + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("parent block should exist"); + let mut parent_hash = parent_block.header.hash; + let mut parent_timestamp = parent_block.header.inner.timestamp; + let mut parent_number = parent_block.header.inner.number; + let gas_limit = parent_block.header.inner.gas_limit; + + // allowlisted account deploys the factory via top-level CREATE + let deploy_factory_tx = TransactionRequest { + nonce: Some(0), + gas: Some(1_000_000), + max_fee_per_gas: Some(20_000_000_000), + max_priority_fee_per_gas: Some(2_000_000_000), + chain_id: Some(chain_id), + value: Some(U256::ZERO), + to: Some(TxKind::Create), + input: TransactionInput { + input: None, + data: Some(Bytes::from_static(&CREATE2_FACTORY_INITCODE)), + }, + ..Default::default() + }; + + let factory_envelope = + TransactionTestContext::sign_tx(allowed_deployer.clone(), deploy_factory_tx).await; + let factory_raw: Bytes = factory_envelope.encoded_2718().into(); + + build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![factory_raw], + Address::ZERO, + ) + .await?; + + let factory_address = contract_address_from_nonce(allowed_deployer.address(), 0); + let factory_code = + EthApiClient::::get_code( + &env.node_clients[0].rpc, + factory_address, + Some(BlockId::latest()), + ) + .await?; + assert!( + !factory_code.is_empty(), + "factory contract should be deployed by allowlisted account" + ); + + // non-allowlisted account calls the factory to deploy a child via CREATE2 + let salt = B256::left_padding_from(&[42u8]); + let mut calldata = Vec::new(); + calldata.extend_from_slice(salt.as_slice()); + calldata.extend_from_slice(&CREATE2_CHILD_INITCODE); + + let deploy_child_tx = TransactionRequest { + nonce: Some(0), + gas: Some(1_000_000), + max_fee_per_gas: Some(20_000_000_000), + max_priority_fee_per_gas: Some(2_000_000_000), + chain_id: Some(chain_id), + value: Some(U256::ZERO), + to: Some(TxKind::Call(factory_address)), + input: TransactionInput { + input: None, + data: Some(Bytes::from(calldata)), + }, + ..Default::default() + }; + + let child_envelope = + TransactionTestContext::sign_tx(non_allowlisted.clone(), deploy_child_tx).await; + let child_raw: Bytes = child_envelope.encoded_2718().into(); + let child_tx_hash = *child_envelope.tx_hash(); + + build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![child_raw], + Address::ZERO, + ) + .await?; + + // verify the transaction was included and succeeded + let child_receipt = EthApiClient::< + TransactionRequest, + Transaction, + Block, + Receipt, + Header, + Bytes, + >::transaction_receipt(&env.node_clients[0].rpc, child_tx_hash) + .await? + .expect("factory call receipt should be available"); + assert!( + child_receipt.status(), + "non-allowlisted account calling factory should succeed" + ); + + // verify child contract exists at the expected CREATE2 address + let init_code_hash = alloy_primitives::keccak256(&CREATE2_CHILD_INITCODE); + let expected_child_address = factory_address.create2(salt, init_code_hash); + let child_code = + EthApiClient::::get_code( + &env.node_clients[0].rpc, + expected_child_address, + Some(BlockId::latest()), + ) + .await?; + assert!( + !child_code.is_empty(), + "child contract should be deployed via CREATE2 despite caller not being on the allowlist" + ); + + drop(setup); + + Ok(()) +} From d15ef31ffbb1295a93a9ccefc426253ef1ef6e58 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 3 Mar 2026 09:40:58 +0000 Subject: [PATCH 5/6] chore: update test to ensure non allow list address cannot deplopy factor contract --- crates/tests/src/test_deploy_allowlist.rs | 74 +++++++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/crates/tests/src/test_deploy_allowlist.rs b/crates/tests/src/test_deploy_allowlist.rs index 335f836..e2e5f46 100644 --- a/crates/tests/src/test_deploy_allowlist.rs +++ b/crates/tests/src/test_deploy_allowlist.rs @@ -2,7 +2,10 @@ use alloy_consensus::TxReceipt; use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag}; use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::{ - eth::{Block, Header, Receipt, Transaction, TransactionInput, TransactionRequest}, + eth::{ + Block, BlockTransactions, Header, Receipt, Transaction, TransactionInput, + TransactionRequest, + }, BlockId, }; use eyre::Result; @@ -45,16 +48,19 @@ const CREATE2_CHILD_INITCODE: [u8; 22] = alloy_primitives::hex!( /// /// # Test Flow /// 1. Create a deploy allowlist containing only `allowed_deployer` -/// 2. `allowed_deployer` deploys a CREATE2 factory contract (top-level CREATE, allowed) -/// 3. `non_allowlisted` account calls the factory, which internally uses CREATE2 -/// 4. Verify the child contract is deployed at the expected deterministic address +/// 2. `non_allowlisted` attempts a direct top-level CREATE — rejected by the allowlist +/// 3. `allowed_deployer` deploys a CREATE2 factory contract (top-level CREATE, allowed) +/// 4. `non_allowlisted` account calls the factory, which internally uses CREATE2 +/// 5. Verify the child contract is deployed at the expected deterministic address /// /// # What It Tests -/// - deploy allowlist only blocks top-level CREATE transactions +/// - deploy allowlist blocks top-level CREATE from non-allowlisted accounts +/// - deploy allowlist permits top-level CREATE from allowlisted accounts /// - contract-to-contract CREATE2 bypasses the allowlist (by design) /// - the factory pattern works as an indirect deployment mechanism /// /// # Success Criteria +/// - non-allowlisted top-level CREATE is excluded from the block /// - factory deploys successfully from allowlisted account /// - non-allowlisted account's call to the factory succeeds /// - child contract appears at the predicted CREATE2 address @@ -87,6 +93,64 @@ async fn test_e2e_deploy_allowlist_permits_create2_via_factory() -> Result<()> { let mut parent_number = parent_block.header.inner.number; let gas_limit = parent_block.header.inner.gas_limit; + // non-allowlisted account attempts a direct top-level CREATE — should be rejected + let denied_deploy_tx = TransactionRequest { + nonce: Some(0), + gas: Some(1_000_000), + max_fee_per_gas: Some(20_000_000_000), + max_priority_fee_per_gas: Some(2_000_000_000), + chain_id: Some(chain_id), + value: Some(U256::ZERO), + to: Some(TxKind::Create), + input: TransactionInput { + input: None, + data: Some(Bytes::from_static(&CREATE2_FACTORY_INITCODE)), + }, + ..Default::default() + }; + + let denied_envelope = + TransactionTestContext::sign_tx(non_allowlisted.clone(), denied_deploy_tx).await; + let denied_raw: Bytes = denied_envelope.encoded_2718().into(); + + build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![denied_raw], + Address::ZERO, + ) + .await?; + + let latest_block = env.node_clients[0] + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("latest block available after denied deploy"); + let denied_tx_count = match latest_block.transactions { + BlockTransactions::Full(ref txs) => txs.len(), + BlockTransactions::Hashes(ref hashes) => hashes.len(), + BlockTransactions::Uncle => 0, + }; + assert_eq!( + denied_tx_count, 0, + "non-allowlisted top-level CREATE should be excluded from the block" + ); + + let denied_address = contract_address_from_nonce(non_allowlisted.address(), 0); + let denied_code = + EthApiClient::::get_code( + &env.node_clients[0].rpc, + denied_address, + Some(BlockId::latest()), + ) + .await?; + assert!( + denied_code.is_empty(), + "non-allowlisted deploy should not create contract code" + ); + // allowlisted account deploys the factory via top-level CREATE let deploy_factory_tx = TransactionRequest { nonce: Some(0), From 70eb3041804d027c513b29d59e96beb9b6fec995 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 3 Mar 2026 10:22:28 +0000 Subject: [PATCH 6/6] chore: fix clippy --- crates/tests/src/test_deploy_allowlist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tests/src/test_deploy_allowlist.rs b/crates/tests/src/test_deploy_allowlist.rs index e2e5f46..8a7b026 100644 --- a/crates/tests/src/test_deploy_allowlist.rs +++ b/crates/tests/src/test_deploy_allowlist.rs @@ -249,7 +249,7 @@ async fn test_e2e_deploy_allowlist_permits_create2_via_factory() -> Result<()> { ); // verify child contract exists at the expected CREATE2 address - let init_code_hash = alloy_primitives::keccak256(&CREATE2_CHILD_INITCODE); + let init_code_hash = alloy_primitives::keccak256(CREATE2_CHILD_INITCODE); let expected_child_address = factory_address.create2(salt, init_code_hash); let child_code = EthApiClient::::get_code(