From 7cb9ea4d0f5f9e52ae267475fa5c0fcabee3b474 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Fri, 12 Dec 2025 14:21:14 -0500 Subject: [PATCH 1/2] feat(fbal): contract factory tests --- crates/fbal/tests/builder.rs | 270 ++++++++++++++++++ .../contracts/src/ContractFactory.sol | 64 +++++ 2 files changed, 334 insertions(+) create mode 100644 crates/test-utils/contracts/src/ContractFactory.sol diff --git a/crates/fbal/tests/builder.rs b/crates/fbal/tests/builder.rs index 7fb94a4..91b3db1 100644 --- a/crates/fbal/tests/builder.rs +++ b/crates/fbal/tests/builder.rs @@ -33,6 +33,24 @@ sol!( ) ); +sol!( + #[sol(rpc)] + ContractFactory, + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-utils/contracts/out/ContractFactory.sol/ContractFactory.json" + ) +); + +sol!( + #[sol(rpc)] + SimpleStorage, + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-utils/contracts/out/ContractFactory.sol/SimpleStorage.json" + ) +); + const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; fn execute_txns_build_access_list( @@ -394,3 +412,255 @@ pub fn test_multi_sstore() { let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); dbg!(access_list); } + +#[test] +/// Tests that contract deployment via CREATE is tracked in the access list +/// Verifies: +/// - Factory contract address is in touched accounts +/// - Newly deployed contract address is in touched accounts +/// - Code change is recorded for the new contract +/// - Nonce change is recorded for the factory (CREATE increments nonce) +pub fn test_create_deployment_tracked() { + let sender = U256::from(0xDEAD).into_address(); + let factory = U256::from(0xFAC0).into_address(); + let mut overrides = HashMap::new(); + overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER))); + overrides.insert( + factory, + AccountInfo::default() + .with_code(Bytecode::new_raw(ContractFactory::DEPLOYED_BYTECODE.clone())), + ); + + // Deploy SimpleStorage via CREATE + let tx = OpTransaction::builder() + .base( + TxEnv::builder() + .caller(sender) + .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .kind(TxKind::Call(factory)) + .data( + ContractFactory::deployWithCreateCall { + bytecode: SimpleStorage::BYTECODE.to_vec().into(), + } + .abi_encode() + .into(), + ) + .gas_price(0) + .gas_priority_fee(None) + .max_fee_per_gas(0) + .gas_limit(500_000), + ) + .build_fill(); + + let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); + + // Verify factory is in the access list + let factory_entry = access_list + .account_changes + .iter() + .find(|ac| ac.address() == factory); + assert!(factory_entry.is_some(), "Factory should be in access list"); + + // The factory's nonce should change (CREATE increments deployer nonce) + let factory_changes = factory_entry.unwrap(); + assert!( + !factory_changes.nonce_changes.is_empty(), + "Factory nonce should change due to CREATE" + ); + + // Find the deployed contract - it should have a code change + let deployed_entry = access_list + .account_changes + .iter() + .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); + assert!( + deployed_entry.is_some(), + "Deployed contract should have code change" + ); + + let deployed_changes = deployed_entry.unwrap(); + assert_eq!( + deployed_changes.code_changes.len(), + 1, + "Should have exactly one code change" + ); + + // Verify the deployed bytecode matches SimpleStorage's deployed bytecode + let code_change = &deployed_changes.code_changes[0]; + assert!( + !code_change.new_code.is_empty(), + "Deployed code should not be empty" + ); + + dbg!(&access_list); +} + +#[test] +/// Tests that contract deployment via CREATE2 is tracked in the access list +/// Verifies: +/// - Factory contract address is in touched accounts +/// - Deployed address (deterministic) is in touched accounts +/// - Code change is recorded with correct bytecode +pub fn test_create2_deployment_tracked() { + let sender = U256::from(0xDEAD).into_address(); + let factory = U256::from(0xFAC0).into_address(); + let salt = B256::from(U256::from(12345)); + + let mut overrides = HashMap::new(); + overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER))); + overrides.insert( + factory, + AccountInfo::default() + .with_code(Bytecode::new_raw(ContractFactory::DEPLOYED_BYTECODE.clone())), + ); + + // Deploy SimpleStorage via CREATE2 + let tx = OpTransaction::builder() + .base( + TxEnv::builder() + .caller(sender) + .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .kind(TxKind::Call(factory)) + .data( + ContractFactory::deployWithCreate2Call { + bytecode: SimpleStorage::BYTECODE.to_vec().into(), + salt, + } + .abi_encode() + .into(), + ) + .gas_price(0) + .gas_priority_fee(None) + .max_fee_per_gas(0) + .gas_limit(500_000), + ) + .build_fill(); + + let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); + + // Verify factory is in the access list + let factory_entry = access_list + .account_changes + .iter() + .find(|ac| ac.address() == factory); + assert!(factory_entry.is_some(), "Factory should be in access list"); + + // Find the deployed contract - it should have a code change + let deployed_entry = access_list + .account_changes + .iter() + .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); + assert!( + deployed_entry.is_some(), + "Deployed contract should have code change" + ); + + let deployed_changes = deployed_entry.unwrap(); + assert_eq!( + deployed_changes.code_changes.len(), + 1, + "Should have exactly one code change" + ); + + // Verify the deployed bytecode is present + let code_change = &deployed_changes.code_changes[0]; + assert!( + !code_change.new_code.is_empty(), + "Deployed code should not be empty" + ); + + dbg!(&access_list); +} + +#[test] +/// Tests that deploying a contract and immediately calling it tracks both operations +/// Verifies: +/// - Both the factory and deployed contract are tracked +/// - Code change for deployment is recorded +/// - Storage change from the call is recorded on the new contract's address +pub fn test_create_and_immediate_call() { + let sender = U256::from(0xDEAD).into_address(); + let factory = U256::from(0xFAC0).into_address(); + + let mut overrides = HashMap::new(); + overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER))); + overrides.insert( + factory, + AccountInfo::default() + .with_code(Bytecode::new_raw(ContractFactory::DEPLOYED_BYTECODE.clone())), + ); + + // Deploy SimpleStorage and immediately call setValue(42) + let set_value_calldata = SimpleStorage::setValueCall { v: U256::from(42) }.abi_encode(); + + let tx = OpTransaction::builder() + .base( + TxEnv::builder() + .caller(sender) + .chain_id(Some(BASE_SEPOLIA_CHAIN_ID)) + .kind(TxKind::Call(factory)) + .data( + ContractFactory::deployAndCallCall { + bytecode: SimpleStorage::BYTECODE.to_vec().into(), + callData: set_value_calldata.into(), + } + .abi_encode() + .into(), + ) + .gas_price(0) + .gas_priority_fee(None) + .max_fee_per_gas(0) + .gas_limit(500_000), + ) + .build_fill(); + + let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); + + // Verify factory is in the access list + let factory_entry = access_list + .account_changes + .iter() + .find(|ac| ac.address() == factory); + assert!(factory_entry.is_some(), "Factory should be in access list"); + + // Find the deployed contract - it should have both code change AND storage change + let deployed_entry = access_list + .account_changes + .iter() + .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); + assert!( + deployed_entry.is_some(), + "Deployed contract should have code change" + ); + + let deployed_changes = deployed_entry.unwrap(); + + // Verify code change exists + assert_eq!( + deployed_changes.code_changes.len(), + 1, + "Should have exactly one code change" + ); + + // Verify storage change exists (from setValue(42)) + // SimpleStorage stores `value` at slot 0 + assert!( + !deployed_changes.storage_changes.is_empty(), + "Should have storage change from setValue call" + ); + + // Verify the storage slot is 0 and value is 42 + let storage_change = &deployed_changes.storage_changes[0]; + assert_eq!( + storage_change.slot, + B256::ZERO, + "Storage slot should be 0" + ); + assert_eq!( + storage_change.changes[0].new_value, + B256::from(U256::from(42)), + "Storage value should be 42" + ); + + dbg!(&access_list); +} diff --git a/crates/test-utils/contracts/src/ContractFactory.sol b/crates/test-utils/contracts/src/ContractFactory.sol new file mode 100644 index 0000000..5acec2e --- /dev/null +++ b/crates/test-utils/contracts/src/ContractFactory.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ContractFactory +/// @notice Factory contract for testing CREATE and CREATE2 deployment tracking +contract ContractFactory { + event Created(address indexed deployed); + + /// @notice Deploy a contract using CREATE opcode + /// @param bytecode The bytecode to deploy + /// @return addr The deployed contract address + function deployWithCreate(bytes memory bytecode) public returns (address addr) { + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + } + require(addr != address(0), "CREATE failed"); + emit Created(addr); + } + + /// @notice Deploy a contract using CREATE2 opcode + /// @param bytecode The bytecode to deploy + /// @param salt The salt for deterministic address calculation + /// @return addr The deployed contract address + function deployWithCreate2(bytes memory bytecode, bytes32 salt) public returns (address addr) { + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + require(addr != address(0), "CREATE2 failed"); + emit Created(addr); + } + + /// @notice Deploy a contract and immediately call it + /// @param bytecode The bytecode to deploy + /// @param callData The call data to execute on the deployed contract + /// @return addr The deployed contract address + /// @return result The result of the call + function deployAndCall( + bytes memory bytecode, + bytes memory callData + ) public returns (address addr, bytes memory result) { + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + } + require(addr != address(0), "CREATE failed"); + (bool success, bytes memory data) = addr.call(callData); + require(success, "Call failed"); + result = data; + emit Created(addr); + } +} + +/// @title SimpleStorage +/// @notice Simple contract for testing deployment and storage operations +contract SimpleStorage { + uint256 public value; + + function setValue(uint256 v) public { + value = v; + } + + function getValue() public view returns (uint256) { + return value; + } +} From d7858853eeff2be832c02e789137386bde8cc14f Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Fri, 12 Dec 2025 14:24:55 -0500 Subject: [PATCH 2/2] fix(ci): bad code formatting --- crates/fbal/tests/builder.rs | 69 +++++++----------------------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/crates/fbal/tests/builder.rs b/crates/fbal/tests/builder.rs index 91b3db1..a0aebad 100644 --- a/crates/fbal/tests/builder.rs +++ b/crates/fbal/tests/builder.rs @@ -455,42 +455,26 @@ pub fn test_create_deployment_tracked() { let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); // Verify factory is in the access list - let factory_entry = access_list - .account_changes - .iter() - .find(|ac| ac.address() == factory); + let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory); assert!(factory_entry.is_some(), "Factory should be in access list"); // The factory's nonce should change (CREATE increments deployer nonce) let factory_changes = factory_entry.unwrap(); - assert!( - !factory_changes.nonce_changes.is_empty(), - "Factory nonce should change due to CREATE" - ); + assert!(!factory_changes.nonce_changes.is_empty(), "Factory nonce should change due to CREATE"); // Find the deployed contract - it should have a code change let deployed_entry = access_list .account_changes .iter() .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); - assert!( - deployed_entry.is_some(), - "Deployed contract should have code change" - ); + assert!(deployed_entry.is_some(), "Deployed contract should have code change"); let deployed_changes = deployed_entry.unwrap(); - assert_eq!( - deployed_changes.code_changes.len(), - 1, - "Should have exactly one code change" - ); + assert_eq!(deployed_changes.code_changes.len(), 1, "Should have exactly one code change"); // Verify the deployed bytecode matches SimpleStorage's deployed bytecode let code_change = &deployed_changes.code_changes[0]; - assert!( - !code_change.new_code.is_empty(), - "Deployed code should not be empty" - ); + assert!(!code_change.new_code.is_empty(), "Deployed code should not be empty"); dbg!(&access_list); } @@ -539,10 +523,7 @@ pub fn test_create2_deployment_tracked() { let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); // Verify factory is in the access list - let factory_entry = access_list - .account_changes - .iter() - .find(|ac| ac.address() == factory); + let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory); assert!(factory_entry.is_some(), "Factory should be in access list"); // Find the deployed contract - it should have a code change @@ -550,24 +531,14 @@ pub fn test_create2_deployment_tracked() { .account_changes .iter() .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); - assert!( - deployed_entry.is_some(), - "Deployed contract should have code change" - ); + assert!(deployed_entry.is_some(), "Deployed contract should have code change"); let deployed_changes = deployed_entry.unwrap(); - assert_eq!( - deployed_changes.code_changes.len(), - 1, - "Should have exactly one code change" - ); + assert_eq!(deployed_changes.code_changes.len(), 1, "Should have exactly one code change"); // Verify the deployed bytecode is present let code_change = &deployed_changes.code_changes[0]; - assert!( - !code_change.new_code.is_empty(), - "Deployed code should not be empty" - ); + assert!(!code_change.new_code.is_empty(), "Deployed code should not be empty"); dbg!(&access_list); } @@ -617,10 +588,7 @@ pub fn test_create_and_immediate_call() { let access_list = execute_txns_build_access_list(vec![tx], Some(overrides)); // Verify factory is in the access list - let factory_entry = access_list - .account_changes - .iter() - .find(|ac| ac.address() == factory); + let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory); assert!(factory_entry.is_some(), "Factory should be in access list"); // Find the deployed contract - it should have both code change AND storage change @@ -628,19 +596,12 @@ pub fn test_create_and_immediate_call() { .account_changes .iter() .find(|ac| !ac.code_changes.is_empty() && ac.address() != factory); - assert!( - deployed_entry.is_some(), - "Deployed contract should have code change" - ); + assert!(deployed_entry.is_some(), "Deployed contract should have code change"); let deployed_changes = deployed_entry.unwrap(); // Verify code change exists - assert_eq!( - deployed_changes.code_changes.len(), - 1, - "Should have exactly one code change" - ); + assert_eq!(deployed_changes.code_changes.len(), 1, "Should have exactly one code change"); // Verify storage change exists (from setValue(42)) // SimpleStorage stores `value` at slot 0 @@ -651,11 +612,7 @@ pub fn test_create_and_immediate_call() { // Verify the storage slot is 0 and value is 42 let storage_change = &deployed_changes.storage_changes[0]; - assert_eq!( - storage_change.slot, - B256::ZERO, - "Storage slot should be 0" - ); + assert_eq!(storage_change.slot, B256::ZERO, "Storage slot should be 0"); assert_eq!( storage_change.changes[0].new_value, B256::from(U256::from(42)),