diff --git a/crates/fbal/tests/builder.rs b/crates/fbal/tests/builder.rs index 7fb94a4..a0aebad 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,212 @@ 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; + } +}