Skip to content
Merged
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
227 changes: 227 additions & 0 deletions crates/fbal/tests/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
64 changes: 64 additions & 0 deletions crates/test-utils/contracts/src/ContractFactory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}