From c2b1a49ac6a88c3898f1c905a95ed72e20020798 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:51:06 +0100 Subject: [PATCH 01/14] Configure foundry for deterministic bytecode - Set bytecode_hash = none - Set cbor_metadata = false - Pin evm_version = cancun These settings ensure identical bytecode across builds, which is essential for CREATE2 address determinism. --- tee-worker/omni-executor/aa-contracts/foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tee-worker/omni-executor/aa-contracts/foundry.toml b/tee-worker/omni-executor/aa-contracts/foundry.toml index 64f9987406..017290fc1d 100644 --- a/tee-worker/omni-executor/aa-contracts/foundry.toml +++ b/tee-worker/omni-executor/aa-contracts/foundry.toml @@ -3,9 +3,13 @@ src = "src" out = "out" libs = ["lib", "../forge-libs"] solc_version = "0.8.28" +evm_version = "cancun" optimizer = true optimizer_runs = 1000000 via_ir = true +# Deterministic bytecode settings for CREATE2 deployments +bytecode_hash = "none" +cbor_metadata = false fs_permissions = [ { access = "read-write", path = "./deployments" }, { access = "read-write", path = "./test_deployments" }, From 127da67003e449aa80dba9321b3e2b25b25e0e27 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:51:22 +0100 Subject: [PATCH 02/14] Add Create2Factory for deterministic deployments Implements a CREATE2 factory contract with: - Deterministic address computation - Deployment tracking and redeployment prevention - Sender-protected and simple salt generation - Address prediction helpers --- .../aa-contracts/src/core/Create2Factory.sol | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol diff --git a/tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol b/tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol new file mode 100644 index 0000000000..314c558603 --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +/** + * @title Create2Factory + * @notice Factory contract for deterministic contract deployments using CREATE2 + * @dev Provides deterministic address generation across multiple EVM chains + * This contract should be deployed using a fresh EOA on each network + */ +contract Create2Factory { + /// @notice Emitted when a contract is successfully deployed + /// @param deployed The address of the newly deployed contract + /// @param salt The salt used for deployment + /// @param deployer The address that initiated the deployment + event ContractDeployed(address indexed deployed, bytes32 indexed salt, address indexed deployer); + + /// @notice Thrown when attempting to deploy to an address that already has code + error AddressAlreadyDeployed(address target); + + /// @notice Thrown when the deployment fails + error DeploymentFailed(); + + /// @notice Mapping to track deployed addresses to prevent redeployment + mapping(address => bool) public deployments; + + /** + * @notice Deploys a contract using CREATE2 + * @param salt The salt for deterministic address generation + * @param bytecode The creation bytecode of the contract to deploy + * @return deployed The address of the deployed contract + */ + function deploy(bytes32 salt, bytes memory bytecode) external payable returns (address deployed) { + // Predict the deployment address + deployed = computeAddress(salt, bytecode); + + // Prevent redeployment to the same address + if (deployments[deployed]) { + revert AddressAlreadyDeployed(deployed); + } + + // Check if address already has code (additional safety check) + if (deployed.code.length > 0) { + revert AddressAlreadyDeployed(deployed); + } + + // Deploy the contract using CREATE2 + assembly { + deployed := create2(callvalue(), add(bytecode, 0x20), mload(bytecode), salt) + } + + // Check if deployment was successful + if (deployed == address(0)) { + revert DeploymentFailed(); + } + + // Mark address as deployed + deployments[deployed] = true; + + emit ContractDeployed(deployed, salt, msg.sender); + + return deployed; + } + + /** + * @notice Computes the address where a contract will be deployed + * @param salt The salt for deterministic address generation + * @param bytecode The creation bytecode of the contract + * @return predicted The predicted address of the contract + */ + function computeAddress(bytes32 salt, bytes memory bytecode) public view returns (address predicted) { + bytes32 bytecodeHash = keccak256(bytecode); + predicted = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)))) + ); + } + + /** + * @notice Computes the address where a contract will be deployed (using bytecode hash) + * @param salt The salt for deterministic address generation + * @param bytecodeHash The keccak256 hash of the creation bytecode + * @return predicted The predicted address of the contract + */ + function computeAddressWithHash(bytes32 salt, bytes32 bytecodeHash) public view returns (address predicted) { + predicted = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)))) + ); + } + + /** + * @notice Checks if a contract has been deployed at the given address via this factory + * @param target The address to check + * @return deployed True if the address was deployed via this factory + */ + function isDeployed(address target) external view returns (bool) { + return deployments[target]; + } + + /** + * @notice Generates a sender-protected salt for deployment + * @param contractName The name/identifier of the contract + * @param version The version string + * @param sender The address of the deployer (typically msg.sender) + * @return salt The generated salt + * @dev Sender-protected salts prevent frontrunning by including the deployer address + * This ensures only the specified sender can deploy to the computed address + */ + function generateSalt(string memory contractName, string memory version, address sender) + external + pure + returns (bytes32 salt) + { + salt = keccak256(abi.encode(contractName, version, sender)); + } + + /** + * @notice Generates a simple salt for deployment (less secure, vulnerable to frontrunning) + * @param contractName The name/identifier of the contract + * @param version The version string + * @return salt The generated salt + * @dev Simple salts allow anyone to deploy to the computed address (frontrunning risk) + * Only use this if you don't care about frontrunning protection + */ + function generateSimpleSalt(string memory contractName, string memory version) + external + pure + returns (bytes32 salt) + { + salt = keccak256(abi.encode(contractName, version)); + } +} From d5e3a1eb7e90e63a67a15cf228725dbc70e7eed1 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:52:59 +0100 Subject: [PATCH 03/14] Add comprehensive tests for Create2Factory --- .../aa-contracts/test/Create2Factory.t.sol | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol diff --git a/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol b/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol new file mode 100644 index 0000000000..8fc6cac8ef --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import {Test, console} from "forge-std/Test.sol"; +import {Create2Factory} from "../src/core/Create2Factory.sol"; +import {Counter} from "../src/Counter.sol"; + +contract Create2FactoryTest is Test { + Create2Factory public factory; + + address deployer1 = makeAddr("deployer1"); + address deployer2 = makeAddr("deployer2"); + + event ContractDeployed(address indexed deployed, bytes32 indexed salt, address indexed deployer); + + function setUp() public { + factory = new Create2Factory(); + } + + // ============ Constructor Tests ============ + + function test_Constructor() public view { + // Factory should be deployed successfully + assertTrue(address(factory).code.length > 0); + } + + // ============ Address Computation Tests ============ + + function test_ComputeAddress() public view { + bytes32 salt = keccak256("test-salt"); + bytes memory bytecode = type(Counter).creationCode; + + address predicted = factory.computeAddress(salt, bytecode); + + // Predicted address should be non-zero + assertTrue(predicted != address(0)); + + // Manual calculation should match + bytes32 bytecodeHash = keccak256(bytecode); + address expectedAddress = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, bytecodeHash)))) + ); + + assertEq(predicted, expectedAddress); + } + + function test_ComputeAddressWithHash() public view { + bytes32 salt = keccak256("test-salt"); + bytes memory bytecode = type(Counter).creationCode; + bytes32 bytecodeHash = keccak256(bytecode); + + address predicted = factory.computeAddressWithHash(salt, bytecodeHash); + + // Should match computeAddress result + address predictedDirect = factory.computeAddress(salt, bytecode); + assertEq(predicted, predictedDirect); + } + + function test_ComputeAddress_DifferentSalts() public view { + bytes32 salt1 = keccak256("salt1"); + bytes32 salt2 = keccak256("salt2"); + bytes memory bytecode = type(Counter).creationCode; + + address predicted1 = factory.computeAddress(salt1, bytecode); + address predicted2 = factory.computeAddress(salt2, bytecode); + + // Different salts should produce different addresses + assertTrue(predicted1 != predicted2); + } + + function test_ComputeAddress_DifferentBytecode() public view { + bytes32 salt = keccak256("same-salt"); + bytes memory bytecode1 = type(Counter).creationCode; + bytes memory bytecode2 = abi.encodePacked(type(Counter).creationCode, bytes32(0)); + + address predicted1 = factory.computeAddress(salt, bytecode1); + address predicted2 = factory.computeAddress(salt, bytecode2); + + // Different bytecode should produce different addresses + assertTrue(predicted1 != predicted2); + } + + // ============ Deployment Tests ============ + + function test_Deploy() public { + bytes32 salt = keccak256("test-deployment"); + bytes memory bytecode = type(Counter).creationCode; + address predicted = factory.computeAddress(salt, bytecode); + + // Deploy should emit event + vm.expectEmit(true, true, true, true); + emit ContractDeployed(predicted, salt, address(this)); + + address deployed = factory.deploy(salt, bytecode); + + // Deployed address should match prediction + assertEq(deployed, predicted); + + // Contract should have code + assertTrue(deployed.code.length > 0); + + // Should be marked as deployed + assertTrue(factory.isDeployed(deployed)); + + // Deployed contract should be functional + Counter counter = Counter(deployed); + counter.increment(); + assertEq(counter.number(), 1); + } + + function test_Deploy_WithValue() public { + // Deploy a simple contract that can receive ETH + // Using minimal bytecode that accepts value and stores it + bytes32 salt = keccak256("test-deployment-with-value"); + + // Bytecode that just returns empty runtime (but can receive ETH) + // PUSH1 0x00 DUP1 RETURN + bytes memory bytecode = hex"60008060006000f3"; + + uint256 valueToSend = 1 ether; + vm.deal(address(this), valueToSend); + + address deployed = factory.deploy{value: valueToSend}(salt, bytecode); + + // Contract should have received ETH + assertEq(deployed.balance, valueToSend); + } + + function test_Deploy_MultipleDifferentContracts() public { + // Deploy first contract + bytes32 salt1 = keccak256("contract1"); + bytes memory bytecode = type(Counter).creationCode; + address deployed1 = factory.deploy(salt1, bytecode); + + // Deploy second contract with different salt + bytes32 salt2 = keccak256("contract2"); + address deployed2 = factory.deploy(salt2, bytecode); + + // Addresses should be different + assertTrue(deployed1 != deployed2); + + // Both should be functional + Counter(deployed1).increment(); + Counter(deployed2).increment(); + assertEq(Counter(deployed1).number(), 1); + assertEq(Counter(deployed2).number(), 1); + } + + function test_Deploy_FromDifferentSenders() public { + bytes32 salt = keccak256("same-salt"); + bytes memory bytecode = type(Counter).creationCode; + + // First deployment from deployer1 + vm.prank(deployer1); + address deployed1 = factory.deploy(salt, bytecode); + + // Second deployment from deployer2 with same salt should work + // (different from predicted address though since it's already used) + bytes32 salt2 = keccak256("different-salt"); + vm.prank(deployer2); + address deployed2 = factory.deploy(salt2, bytecode); + + // Addresses should be different + assertTrue(deployed1 != deployed2); + } + + // ============ Redeployment Prevention Tests ============ + + function test_Deploy_RevertRedeployment() public { + bytes32 salt = keccak256("test-redeployment"); + bytes memory bytecode = type(Counter).creationCode; + + // First deployment + factory.deploy(salt, bytecode); + + // Second deployment should revert + vm.expectRevert( + abi.encodeWithSelector( + Create2Factory.AddressAlreadyDeployed.selector, factory.computeAddress(salt, bytecode) + ) + ); + factory.deploy(salt, bytecode); + } + + function test_Deploy_RevertIfAddressHasCode() public { + // This test simulates the scenario where an address already has code + // We'll deploy a contract first, then try to deploy again + bytes32 salt = keccak256("existing-code"); + bytes memory bytecode = type(Counter).creationCode; + + // First deployment + address deployed = factory.deploy(salt, bytecode); + + // Try to deploy again - should revert + vm.expectRevert(abi.encodeWithSelector(Create2Factory.AddressAlreadyDeployed.selector, deployed)); + factory.deploy(salt, bytecode); + } + + // ============ Salt Generation Tests ============ + + function test_GenerateSalt() public view { + string memory contractName = "TestContract"; + string memory version = "v1.0.0"; + address sender = deployer1; + + bytes32 salt = factory.generateSalt(contractName, version, sender); + + // Salt should be non-zero + assertTrue(salt != bytes32(0)); + + // Salt should be deterministic + bytes32 salt2 = factory.generateSalt(contractName, version, sender); + assertEq(salt, salt2); + + // Salt should include sender (different sender = different salt) + bytes32 salt3 = factory.generateSalt(contractName, version, deployer2); + assertTrue(salt != salt3); + } + + function test_GenerateSalt_DifferentNames() public view { + bytes32 salt1 = factory.generateSalt("Contract1", "v1.0.0", deployer1); + bytes32 salt2 = factory.generateSalt("Contract2", "v1.0.0", deployer1); + + // Different names should produce different salts + assertTrue(salt1 != salt2); + } + + function test_GenerateSalt_DifferentVersions() public view { + bytes32 salt1 = factory.generateSalt("TestContract", "v1.0.0", deployer1); + bytes32 salt2 = factory.generateSalt("TestContract", "v2.0.0", deployer1); + + // Different versions should produce different salts + assertTrue(salt1 != salt2); + } + + function test_GenerateSalt_DifferentSenders() public view { + bytes32 salt1 = factory.generateSalt("TestContract", "v1.0.0", deployer1); + bytes32 salt2 = factory.generateSalt("TestContract", "v1.0.0", deployer2); + + // Different senders should produce different salts (frontrunning protection) + assertTrue(salt1 != salt2); + } + + function test_GenerateSimpleSalt() public view { + string memory contractName = "TestContract"; + string memory version = "v1.0.0"; + + bytes32 salt = factory.generateSimpleSalt(contractName, version); + + // Salt should be non-zero + assertTrue(salt != bytes32(0)); + + // Salt should be deterministic + bytes32 salt2 = factory.generateSimpleSalt(contractName, version); + assertEq(salt, salt2); + + // Simple salt should be same for all senders (no frontrunning protection) + // This is implicit - the function doesn't take a sender parameter + } + + function test_GenerateSimpleSalt_VsGenerateSalt() public view { + string memory contractName = "TestContract"; + string memory version = "v1.0.0"; + + bytes32 simpleSalt = factory.generateSimpleSalt(contractName, version); + bytes32 protectedSalt = factory.generateSalt(contractName, version, deployer1); + + // Simple salt and protected salt should be different + assertTrue(simpleSalt != protectedSalt); + } + + // ============ Deployment Tracking Tests ============ + + function test_IsDeployed() public { + bytes32 salt = keccak256("tracking-test"); + bytes memory bytecode = type(Counter).creationCode; + + address predicted = factory.computeAddress(salt, bytecode); + + // Should not be deployed initially + assertFalse(factory.isDeployed(predicted)); + + // Deploy contract + factory.deploy(salt, bytecode); + + // Should be marked as deployed + assertTrue(factory.isDeployed(predicted)); + } + + function test_IsDeployed_RandomAddress() public { + address randomAddr = makeAddr("random"); + + // Random address should not be marked as deployed + assertFalse(factory.isDeployed(randomAddr)); + } + + // ============ Integration Tests ============ + + function test_EndToEnd_SenderProtectedDeployment() public { + // Scenario: Deploy same contract to same address on multiple "chains" (test runs) + // using sender-protected salt + + string memory contractName = "EntryPointV1"; + string memory version = "v1.0.0"; + + // Generate sender-protected salt + vm.prank(deployer1); + bytes32 salt = factory.generateSalt(contractName, version, deployer1); + + // Predict address + bytes memory bytecode = type(Counter).creationCode; + address predicted = factory.computeAddress(salt, bytecode); + + // Deploy + vm.prank(deployer1); + address deployed = factory.deploy(salt, bytecode); + + // Verify + assertEq(deployed, predicted); + assertTrue(factory.isDeployed(deployed)); + + // Verify only deployer1 can deploy to this address + // (deployer2 would generate a different salt and thus different address) + bytes32 salt2 = factory.generateSalt(contractName, version, deployer2); + assertTrue(salt != salt2); + } + + function test_EndToEnd_MultiChainDeployment() public { + // Simulate deploying the same contract with same salt on multiple chains + // In reality this would be different factory instances, but we can test the logic + + string memory contractName = "TestContract"; + string memory version = "v1.0.0"; + bytes memory bytecode = type(Counter).creationCode; + + // "Chain 1" deployment + bytes32 salt1 = factory.generateSalt(contractName, version, deployer1); + address predicted1 = factory.computeAddress(salt1, bytecode); + vm.prank(deployer1); + address deployed1 = factory.deploy(salt1, bytecode); + + assertEq(deployed1, predicted1); + + // In a real multi-chain scenario, deploying with the same salt and bytecode + // from the same deployer on a different chain (different factory instance) + // would yield the same address - but the factory address would need to be the same + // That's why we deploy the factory with a fresh EOA on each chain + } + + // ============ Fuzz Tests ============ + + function testFuzz_ComputeAddress(bytes32 salt, bytes memory bytecode) public view { + // Skip if bytecode is empty (invalid contract) + vm.assume(bytecode.length > 0); + + address predicted = factory.computeAddress(salt, bytecode); + + // Should always return a non-zero address + assertTrue(predicted != address(0)); + } + + function testFuzz_Deploy(bytes32 salt) public { + // Use a simple valid bytecode + bytes memory bytecode = type(Counter).creationCode; + + address predicted = factory.computeAddress(salt, bytecode); + address deployed = factory.deploy(salt, bytecode); + + assertEq(deployed, predicted); + assertTrue(factory.isDeployed(deployed)); + } + + function testFuzz_GenerateSalt(string memory name, string memory version, address sender) public view { + vm.assume(sender != address(0)); + + bytes32 salt = factory.generateSalt(name, version, sender); + + // Salt should be deterministic + bytes32 salt2 = factory.generateSalt(name, version, sender); + assertEq(salt, salt2); + } + + // ============ Edge Cases ============ + + function test_Deploy_MinimalBytecode() public { + // Test with minimal valid bytecode that returns empty runtime code + bytes32 salt = keccak256("minimal"); + // This bytecode: PUSH1 0x00 DUP1 RETURN (returns 0 bytes of runtime code) + bytes memory bytecode = hex"60008060006000f3"; + + address deployed = factory.deploy(salt, bytecode); + + assertTrue(deployed != address(0)); + // Empty runtime code is valid (contract gets deployed with no code) + assertTrue(deployed.code.length == 0); + } + + function test_Deploy_EmptyBytecode() public { + // Empty creation bytecode actually succeeds in CREATE2 (creates empty contract) + bytes32 salt = keccak256("empty"); + bytes memory bytecode = hex""; + + // This should succeed and create a contract with no code + address deployed = factory.deploy(salt, bytecode); + + assertTrue(deployed != address(0)); + assertEq(deployed.code.length, 0); + } +} From 5d1995dc1cfc529465a3836bbaee6c9c4cb5c1ac Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:53:40 +0100 Subject: [PATCH 04/14] Add deployment script for Create2Factory --- .../script/DeployCreate2Factory.s.sol | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol new file mode 100644 index 0000000000..29fb90e893 --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../src/core/Create2Factory.sol"; + +/** + * @title DeployCreate2Factory + * @notice Deployment script for the Create2Factory contract + * @dev This script should be run with a FRESH EOA that has not deployed contracts before + * The factory address will be deterministic based on the deployer's address and nonce + * + * Usage: + * forge script script/DeployCreate2Factory.s.sol:DeployCreate2Factory --rpc-url --broadcast --verify + * + * Environment Variables: + * PRIVATE_KEY - Private key of the deployer (should be a fresh EOA) + * RPC_URL - RPC endpoint for the target network + * ETHERSCAN_API_KEY - API key for contract verification (optional) + */ +contract DeployCreate2Factory is Script { + // Network configuration + struct NetworkConfig { + string name; + uint256 chainId; + } + + function run() external { + // Get deployment parameters + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + // Get network info + NetworkConfig memory networkConfig = getNetworkConfig(); + + // Log deployment info + console.log("=== Create2Factory Deployment ==="); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Deployer address:", deployer); + console.log("Deployer balance:", deployer.balance / 1e18, "ETH"); + console.log("Deployer nonce:", vm.getNonce(deployer)); + console.log(""); + + // Warning about fresh EOA requirement + if (vm.getNonce(deployer) > 0) { + console.log("WARNING: Deployer has non-zero nonce!"); + console.log( + "For consistent factory addresses across chains, it's recommended to use a fresh EOA (nonce 0)" + ); + console.log("Current nonce:", vm.getNonce(deployer)); + console.log(""); + } + + // Predict factory address + address predictedFactory = vm.computeCreateAddress(deployer, vm.getNonce(deployer)); + console.log("Predicted factory address:", predictedFactory); + console.log(""); + + // Deploy factory + console.log("Deploying Create2Factory..."); + vm.startBroadcast(deployerPrivateKey); + + Create2Factory factory = new Create2Factory(); + + vm.stopBroadcast(); + + // Verify deployment + require(address(factory) == predictedFactory, "Factory address mismatch!"); + console.log("Create2Factory deployed at:", address(factory)); + console.log(""); + + // Print summary + console.log("=== Deployment Summary ==="); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Factory Address:", address(factory)); + console.log("Deployer:", deployer); + console.log("Final Nonce:", vm.getNonce(deployer)); + console.log(""); + console.log("IMPORTANT: Save this factory address for future deployments!"); + console.log("Add it to: deployments/create2-factories.json"); + console.log(""); + + // Save deployment info if requested + saveDeploymentInfo(networkConfig, address(factory), deployer); + } + + function getNetworkConfig() internal view returns (NetworkConfig memory) { + uint256 chainId = block.chainid; + string memory name = getNetworkName(chainId); + return NetworkConfig({name: name, chainId: chainId}); + } + + function getNetworkName(uint256 chainId) internal pure returns (string memory) { + if (chainId == 1) return "mainnet"; + if (chainId == 11155111) return "sepolia"; + if (chainId == 42161) return "arbitrum"; + if (chainId == 421614) return "arbitrum-sepolia"; + if (chainId == 10) return "optimism"; + if (chainId == 11155420) return "optimism-sepolia"; + if (chainId == 8453) return "base"; + if (chainId == 84532) return "base-sepolia"; + if (chainId == 137) return "polygon"; + if (chainId == 80002) return "polygon-amoy"; + if (chainId == 56) return "bsc"; + if (chainId == 97) return "bsc-testnet"; + if (chainId == 43114) return "avalanche"; + if (chainId == 43113) return "avalanche-fuji"; + if (chainId == 250) return "fantom"; + if (chainId == 100) return "gnosis"; + if (chainId == 1284) return "moonbeam"; + if (chainId == 1285) return "moonriver"; + if (chainId == 42220) return "celo"; + if (chainId == 1313161554) return "aurora"; + if (chainId == 25) return "cronos"; + if (chainId == 2001) return "hyperspace"; + if (chainId == 1337) return "localhost"; + if (chainId == 31337) return "anvil"; + return string(abi.encodePacked("unknown-", vm.toString(chainId))); + } + + function saveDeploymentInfo(NetworkConfig memory config, address factory, address deployer) internal { + bool shouldSave = vm.envOr("SAVE_DEPLOYMENT_FILE", true); + if (!shouldSave) { + console.log("Skipping deployment file save (SAVE_DEPLOYMENT_FILE=false)"); + return; + } + + string memory deploymentEnv = vm.envOr("DEPLOYMENT_ENV", string("")); + string memory basePath = "deployments"; + + if (bytes(deploymentEnv).length > 0) { + basePath = string(abi.encodePacked(basePath, "/", deploymentEnv)); + } + + string memory filePath = string(abi.encodePacked(basePath, "/", config.name, "-create2-factory.json")); + + // Create JSON object + string memory json = "deployment"; + vm.serializeString(json, "network", config.name); + vm.serializeUint(json, "chainId", config.chainId); + vm.serializeAddress(json, "factoryAddress", factory); + vm.serializeAddress(json, "deployer", deployer); + vm.serializeUint(json, "blockNumber", block.number); + string memory finalJson = vm.serializeUint(json, "timestamp", block.timestamp); + + // Write to file + vm.writeJson(finalJson, filePath); + console.log("Deployment info saved to:", filePath); + } +} From f8c4f297a4471a3ea4449e8dcb412443cf17f859 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:53:51 +0100 Subject: [PATCH 05/14] Add CREATE2-based deployment script for AA contracts --- .../script/DeployWithCreate2.s.sol | 519 ++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol new file mode 100644 index 0000000000..50c4e38293 --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../src/core/EntryPointV1.sol"; +import "../src/accounts/OmniAccountFactoryV1.sol"; +import "../src/core/SimplePaymaster.sol"; +import "../src/core/ERC20PaymasterV1.sol"; +import "../src/core/Create2Factory.sol"; +import "./DeploymentHelper.sol"; + +/** + * @title DeployWithCreate2 + * @notice Deployment script for AA contracts using CREATE2 for deterministic addresses + * @dev This script uses a pre-deployed Create2Factory to deploy contracts with deterministic addresses + * across multiple EVM chains. All deployments maintain the same addresses when using the same + * deployer EOA and contract versions. + * + * Prerequisites: + * 1. Create2Factory must be deployed on the target network + * 2. Factory address must be provided via CREATE2_FACTORY_ADDRESS environment variable + * 3. Same deployer EOA should be used across all chains for address consistency + * + * Usage: + * forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 --rpc-url --broadcast --verify + * + * Environment Variables: + * CREATE2_FACTORY_ADDRESS - Address of the deployed Create2Factory (required) + * CONTRACT_VERSION - Version string for salt generation (default: "v1.0.0") + * All other environment variables from Deploy.s.sol are supported + */ +contract DeployWithCreate2 is Script { + // CREATE2 configuration + Create2Factory public factory; + string public contractVersion; + + // Configuration - can be overridden via environment variables + uint256 public paymasterInitialDeposit; + bool public shouldDeployEntryPoint; + bool public shouldDeployFactory; + bool public shouldDeploySimplePaymaster; + bool public shouldDeployERC20Paymaster; + address public existingEntryPointAddress; + address public initialBundler; + bool public saveDeploymentFile; + + // Contract addresses will be stored here after deployment + address public entryPointAddress; + address public factoryAddress; + address public paymasterAddress; + address public erc20PaymasterAddress; + + // Network configuration + struct NetworkConfig { + string name; + uint256 chainId; + uint256 minDeploymentBalance; + } + + function run() external { + // Get deployment parameters + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + // Load CREATE2 factory + address factoryAddr = vm.envAddress("CREATE2_FACTORY_ADDRESS"); + require(factoryAddr != address(0), "CREATE2_FACTORY_ADDRESS not set"); + require(factoryAddr.code.length > 0, "CREATE2_FACTORY_ADDRESS is not a contract"); + factory = Create2Factory(factoryAddr); + + // Load contract version + contractVersion = vm.envOr("CONTRACT_VERSION", string("v1.0.0")); + + // Load configuration + loadConfiguration(); + + // Get network info + NetworkConfig memory networkConfig = getNetworkConfig(); + + // Log deployment info + console.log("=== AA Contracts Deployment (CREATE2) ==="); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Deployer address:", deployer); + console.log("Deployer balance:", deployer.balance / 1e18, "ETH"); + console.log("Block number:", block.number); + console.log("Create2Factory:", address(factory)); + console.log("Contract version:", contractVersion); + console.log(""); + + // Check minimum deployment balance + require( + deployer.balance >= networkConfig.minDeploymentBalance, + string( + abi.encodePacked( + "Insufficient balance for deployment (need at least ", + vm.toString(networkConfig.minDeploymentBalance / 1e18), + " ETH)" + ) + ) + ); + + // Validate dependencies + validateDependencies(); + + // Predict all addresses before deployment + console.log("=== Predicted Addresses ==="); + if (shouldDeployEntryPoint) { + bytes32 entryPointSalt = factory.generateSalt("EntryPointV1", contractVersion, deployer); + address predictedEntryPoint = + factory.computeAddress(entryPointSalt, abi.encodePacked(type(EntryPointV1).creationCode)); + console.log("EntryPointV1 (predicted):", predictedEntryPoint); + } + if (shouldDeployFactory) { + bytes32 factorySalt = factory.generateSalt("OmniAccountFactoryV1", contractVersion, deployer); + address predictedFactory = factory.computeAddress( + factorySalt, + abi.encodePacked( + type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress)) + ) + ); + console.log("OmniAccountFactoryV1 (predicted):", predictedFactory); + } + if (shouldDeploySimplePaymaster) { + bytes32 paymasterSalt = factory.generateSalt("SimplePaymaster", contractVersion, deployer); + address predictedPaymaster = factory.computeAddress( + paymasterSalt, + abi.encodePacked( + type(SimplePaymaster).creationCode, + abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ) + ); + console.log("SimplePaymaster (predicted):", predictedPaymaster); + } + if (shouldDeployERC20Paymaster) { + bytes32 erc20PaymasterSalt = factory.generateSalt("ERC20PaymasterV1", contractVersion, deployer); + address predictedERC20Paymaster = factory.computeAddress( + erc20PaymasterSalt, + abi.encodePacked( + type(ERC20PaymasterV1).creationCode, + abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ) + ); + console.log("ERC20PaymasterV1 (predicted):", predictedERC20Paymaster); + } + console.log(""); + + // Deploy contracts in dependency order with confirmation between each + if (shouldDeployEntryPoint) { + vm.startBroadcast(deployerPrivateKey); + deployEntryPoint(deployer); + vm.stopBroadcast(); + console.log("Waiting for EntryPoint deployment confirmation..."); + } + + if (shouldDeployFactory) { + vm.startBroadcast(deployerPrivateKey); + deployAccountFactory(deployer); + vm.stopBroadcast(); + console.log("Waiting for Factory deployment confirmation..."); + } + + // Deploy Simple paymaster if configured + if (shouldDeploySimplePaymaster) { + vm.startBroadcast(deployerPrivateKey); + deployPaymaster(deployer); + vm.stopBroadcast(); + console.log("Waiting for SimplePaymaster deployment confirmation..."); + + // Initialize simple paymaster if configured + if (paymasterInitialDeposit > 0) { + vm.startBroadcast(deployerPrivateKey); + initializePaymaster(); + vm.stopBroadcast(); + console.log("Waiting for SimplePaymaster initialization confirmation..."); + } + } + + // Deploy ERC20 paymaster if configured + if (shouldDeployERC20Paymaster) { + vm.startBroadcast(deployerPrivateKey); + deployERC20Paymaster(deployer); + vm.stopBroadcast(); + console.log("Waiting for ERC20Paymaster deployment confirmation..."); + + // Initialize ERC20 paymaster if configured + if (paymasterInitialDeposit > 0) { + vm.startBroadcast(deployerPrivateKey); + initializeERC20Paymaster(); + vm.stopBroadcast(); + console.log("Waiting for ERC20Paymaster initialization confirmation..."); + } + } + + // Log final deployment results + logDeploymentResults(networkConfig); + + // Save deployment addresses to file (if enabled) + if (saveDeploymentFile) { + saveDeploymentAddresses(networkConfig); + } else { + console.log("Deployment file saving disabled (set SAVE_DEPLOYMENT_FILE=true to enable)"); + } + } + + function loadConfiguration() internal { + // Load deployment flags (defaults for backward compatibility) + shouldDeployEntryPoint = vm.envOr("DEPLOY_ENTRYPOINT", true); + shouldDeployFactory = vm.envOr("DEPLOY_FACTORY", true); + shouldDeploySimplePaymaster = vm.envOr("DEPLOY_SIMPLE_PAYMASTER", true); + shouldDeployERC20Paymaster = vm.envOr("DEPLOY_ERC20_PAYMASTER", false); + + // Load existing EntryPoint address if not deploying new one + if (!shouldDeployEntryPoint) { + existingEntryPointAddress = vm.envAddress("ENTRYPOINT_ADDRESS"); + require(existingEntryPointAddress != address(0), "ENTRYPOINT_ADDRESS required when DEPLOY_ENTRYPOINT=false"); + require(existingEntryPointAddress.code.length > 0, "ENTRYPOINT_ADDRESS must be a deployed contract"); + entryPointAddress = existingEntryPointAddress; + } + + // Load paymaster deposit amount (default: 1 ETH, can be 0 to skip initialization) + paymasterInitialDeposit = vm.envOr("PAYMASTER_INITIAL_DEPOSIT", uint256(1 ether)); + + // Load initial bundler (default: deployer address) + address deployer = msg.sender; + initialBundler = vm.envOr("INITIAL_BUNDLER", deployer); + + // Load save deployment file flag (default: false for testing, true for production) + saveDeploymentFile = vm.envOr("SAVE_DEPLOYMENT_FILE", false); + + console.log("Configuration:"); + console.log("- Deploy EntryPoint:", shouldDeployEntryPoint ? "Yes" : "No"); + if (!shouldDeployEntryPoint) { + console.log("- Existing EntryPoint:", entryPointAddress); + } + console.log("- Deploy Factory:", shouldDeployFactory ? "Yes" : "No"); + console.log("- Deploy Simple paymaster:", shouldDeploySimplePaymaster ? "Yes" : "No"); + console.log("- Deploy ERC20 paymaster:", shouldDeployERC20Paymaster ? "Yes" : "No"); + console.log("- Paymaster initial deposit:", paymasterInitialDeposit / 1e18, "ETH"); + console.log("- Initial bundler:", initialBundler); + console.log("- Save deployment file:", saveDeploymentFile ? "Yes" : "No"); + console.log(""); + } + + function validateDependencies() internal view { + // Validate that EntryPoint is available for contracts that depend on it + if ( + (shouldDeployFactory || shouldDeploySimplePaymaster || shouldDeployERC20Paymaster) + && !shouldDeployEntryPoint + ) { + require(entryPointAddress != address(0), "EntryPoint address required for Factory/Paymaster deployment"); + } + } + + function getNetworkConfig() internal view returns (NetworkConfig memory) { + uint256 chainId = block.chainid; + + if (chainId == 1) { + return NetworkConfig("Ethereum Mainnet", 1, 0.01 ether); + } else if (chainId == 11155111) { + return NetworkConfig("Ethereum Sepolia", 11155111, 0.01 ether); + } else if (chainId == 56) { + return NetworkConfig("BSC Mainnet", 56, 0.01 ether); + } else if (chainId == 97) { + return NetworkConfig("BSC Testnet", 97, 0.05 ether); + } else if (chainId == 8453) { + return NetworkConfig("Base", 8453, 0.01 ether); + } else if (chainId == 84532) { + return NetworkConfig("Base Sepolia", 84532, 0.05 ether); + } else if (chainId == 137) { + return NetworkConfig("Polygon Mainnet", 137, 1 ether); + } else if (chainId == 80001) { + return NetworkConfig("Polygon Mumbai", 80001, 0.1 ether); + } else if (chainId == 42161) { + return NetworkConfig("Arbitrum Mainnet", 42161, 0.01 ether); + } else if (chainId == 421614) { + return NetworkConfig("Arbitrum Sepolia", 421614, 0.01 ether); + } else if (chainId == 999) { + return NetworkConfig("HyperEVM Mainnet", 999, 0.01 ether); + } else if (chainId == 998) { + return NetworkConfig("HyperEVM Testnet", 998, 0.01 ether); + } else if (chainId == 1337) { + return NetworkConfig("Local Anvil", 1337, 0.01 ether); + } else if (chainId == 31337) { + return NetworkConfig("Local Anvil", 31337, 0.01 ether); + } else { + return NetworkConfig( + string(abi.encodePacked("Unknown Network (", vm.toString(chainId), ")")), chainId, 0.01 ether + ); + } + } + + function deployEntryPoint(address deployer) internal { + console.log("Deploying EntryPointV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("EntryPointV1", contractVersion, deployer); + bytes memory bytecode = abi.encodePacked(type(EntryPointV1).creationCode); + + address deployed = factory.deploy(salt, bytecode); + entryPointAddress = deployed; + + console.log("EntryPointV1 deployed at:", entryPointAddress); + console.log(""); + } + + function deployAccountFactory(address deployer) internal { + console.log("Deploying OmniAccountFactoryV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("OmniAccountFactoryV1", contractVersion, deployer); + bytes memory bytecode = + abi.encodePacked(type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress))); + + address deployed = factory.deploy(salt, bytecode); + factoryAddress = deployed; + + console.log("OmniAccountFactoryV1 deployed at:", factoryAddress); + console.log("EntryPoint reference:", entryPointAddress); + console.log(""); + } + + function deployPaymaster(address deployer) internal { + console.log("Deploying SimplePaymaster via CREATE2..."); + + bytes32 salt = factory.generateSalt("SimplePaymaster", contractVersion, deployer); + bytes memory bytecode = abi.encodePacked( + type(SimplePaymaster).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ); + + address deployed = factory.deploy(salt, bytecode); + paymasterAddress = deployed; + + console.log("SimplePaymaster deployed at:", paymasterAddress); + console.log("EntryPoint reference:", entryPointAddress); + console.log("Initial bundler:", initialBundler); + console.log(""); + } + + function deployERC20Paymaster(address deployer) internal { + console.log("Deploying ERC20PaymasterV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("ERC20PaymasterV1", contractVersion, deployer); + bytes memory bytecode = abi.encodePacked( + type(ERC20PaymasterV1).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ); + + address deployed = factory.deploy(salt, bytecode); + erc20PaymasterAddress = deployed; + + console.log("ERC20PaymasterV1 deployed at:", erc20PaymasterAddress); + console.log("EntryPoint reference:", entryPointAddress); + console.log("Initial bundler:", initialBundler); + console.log(""); + } + + function initializePaymaster() internal { + console.log("Initializing Paymaster with deposit..."); + + SimplePaymaster paymaster = SimplePaymaster(payable(paymasterAddress)); + + uint256 stakeAmount = 0; + uint256 depositAmount = paymasterInitialDeposit; + + if (stakeAmount > 0) { + paymaster.addStake{value: stakeAmount}(1 days); + console.log("Added stake:", stakeAmount / 1e18, "ETH"); + } + + if (depositAmount > 0) { + paymaster.deposit{value: depositAmount}(); + console.log("Added deposit:", depositAmount / 1e18, "ETH"); + } + + console.log("Paymaster initialized"); + console.log(""); + } + + function initializeERC20Paymaster() internal { + console.log("Initializing ERC20 Paymaster with deposit..."); + + ERC20PaymasterV1 erc20Paymaster = ERC20PaymasterV1(payable(erc20PaymasterAddress)); + + uint256 stakeAmount = 0; + uint256 depositAmount = paymasterInitialDeposit; + + if (stakeAmount > 0) { + erc20Paymaster.addStake{value: stakeAmount}(1 days); + console.log("Added stake:", stakeAmount / 1e18, "ETH"); + } + + if (depositAmount > 0) { + erc20Paymaster.deposit{value: depositAmount}(); + console.log("Added deposit:", depositAmount / 1e18, "ETH"); + } + + console.log("ERC20 Paymaster initialized"); + console.log(""); + } + + function logDeploymentResults(NetworkConfig memory networkConfig) internal view { + console.log("=== DEPLOYMENT COMPLETE (CREATE2) ==="); + console.log(""); + console.log("Contract Addresses:"); + if (shouldDeployEntryPoint) { + console.log("EntryPointV1: ", entryPointAddress); + } else { + console.log("EntryPointV1 (existing):", entryPointAddress); + } + if (shouldDeployFactory) { + console.log("OmniAccountFactoryV1: ", factoryAddress); + } + if (shouldDeploySimplePaymaster) { + console.log("SimplePaymaster: ", paymasterAddress); + } + if (shouldDeployERC20Paymaster) { + console.log("ERC20PaymasterV1: ", erc20PaymasterAddress); + } + console.log(""); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Deployment Date:", block.timestamp); + console.log("Create2Factory:", address(factory)); + console.log("Contract Version:", contractVersion); + console.log(""); + console.log("IMPORTANT: These addresses are deterministic across all chains!"); + console.log("Using the same deployer EOA and version will yield the same addresses."); + console.log(""); + } + + function saveDeploymentAddresses(NetworkConfig memory networkConfig) internal { + string memory environment = ""; + try vm.envString("DEPLOYMENT_ENV") returns (string memory env) { + environment = env; + } catch {} + + uint256 deploymentCount = 0; + if (shouldDeployEntryPoint) deploymentCount++; + if (shouldDeployFactory) deploymentCount++; + if (shouldDeploySimplePaymaster) deploymentCount++; + if (shouldDeployERC20Paymaster) deploymentCount++; + + require(deploymentCount > 0, "No contracts were deployed"); + DeploymentHelper.ContractDeployment[] memory deployments = + new DeploymentHelper.ContractDeployment[](deploymentCount); + + uint256 currentIndex = 0; + + if (shouldDeployEntryPoint) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "EntryPointV1", + entryPointAddress, + string(abi.encodePacked('{"deploymentMethod": "CREATE2", "version": "', contractVersion, '"}')) + ); + currentIndex++; + } + + if (shouldDeployFactory) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "OmniAccountFactoryV1", + factoryAddress, + string( + abi.encodePacked( + '{"entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2", "version": "', + contractVersion, + '"}' + ) + ) + ); + currentIndex++; + } + + if (shouldDeploySimplePaymaster) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "SimplePaymaster", + paymasterAddress, + string( + abi.encodePacked( + '{"initialBundler": "', + vm.toString(initialBundler), + '", "entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2", "version": "', + contractVersion, + '"}' + ) + ) + ); + currentIndex++; + } + + if (shouldDeployERC20Paymaster) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "ERC20PaymasterV1", + erc20PaymasterAddress, + string( + abi.encodePacked( + '{"initialBundler": "', + vm.toString(initialBundler), + '", "entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2", "version": "', + contractVersion, + '"}' + ) + ) + ); + } + + DeploymentHelper.saveDeploymentArtifacts( + vm, "deployments", environment, networkConfig.name, networkConfig.chainId, deployments + ); + } +} From 665035a2d0e02010749ad8f9495fb8114a936879 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:54:35 +0100 Subject: [PATCH 06/14] Add Create2Factory addresses registry --- .../deployments/create2-factories.json | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/deployments/create2-factories.json diff --git a/tee-worker/omni-executor/aa-contracts/deployments/create2-factories.json b/tee-worker/omni-executor/aa-contracts/deployments/create2-factories.json new file mode 100644 index 0000000000..8d3475e74e --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/deployments/create2-factories.json @@ -0,0 +1,133 @@ +{ + "$schema": "./create2-factories.schema.json", + "description": "Registry of Create2Factory contract addresses across different networks", + "version": "1.0.0", + "factories": { + "ethereum": { + "chainId": 1, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "ethereum-sepolia": { + "chainId": 11155111, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "arbitrum": { + "chainId": 42161, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "arbitrum-sepolia": { + "chainId": 421614, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "optimism": { + "chainId": 10, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "optimism-sepolia": { + "chainId": 11155420, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "base": { + "chainId": 8453, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "base-sepolia": { + "chainId": 84532, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "polygon": { + "chainId": 137, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "polygon-amoy": { + "chainId": 80002, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "bsc": { + "chainId": 56, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "bsc-testnet": { + "chainId": 97, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "avalanche": { + "chainId": 43114, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "avalanche-fuji": { + "chainId": 43113, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "hyperevm-mainnet": { + "chainId": 999, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "hyperevm-testnet": { + "chainId": 998, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false + }, + "localhost": { + "chainId": 31337, + "address": null, + "deployer": null, + "blockNumber": null, + "deployed": false, + "note": "Local Anvil testnet - addresses will change between restarts" + } + }, + "notes": [ + "This file tracks Create2Factory deployments across all networks", + "Update this file after deploying the factory to a new network", + "Factory addresses are used by DeployWithCreate2.s.sol script", + "For deterministic deployments, use the same deployer EOA on all chains" + ] +} From 1bb11a14b4f366ce1e7428de79308aa555ea209a Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 30 Oct 2025 17:54:44 +0100 Subject: [PATCH 07/14] Document CREATE2 deterministic deployment strategy --- .../omni-executor/aa-contracts/DEPLOYMENT.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md b/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md index e77c3924b2..85bf7976ee 100644 --- a/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md +++ b/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md @@ -118,6 +118,198 @@ forge script script/Deploy.s.sol:Deploy \ -vvv ``` +## 🎯 CREATE2 Deterministic Deployments + +### Why CREATE2? + +When deploying contracts across multiple EVM chains, standard CREATE deployments (using EOA nonce) require careful nonce management to maintain consistent addresses. If you deploy contracts in different orders on different chains, they'll have different addresses, making multi-chain integrations complex. + +**CREATE2** solves this by making contract addresses deterministic based on: +- Factory address (not deployer EOA) +- Salt value +- Contract bytecode + +This allows **identical addresses across all chains** when using the same salt and factory address. + +### Benefits + +✅ **Deterministic Addresses**: Same contract address on all chains +✅ **Order Independent**: Deploy contracts in any order +✅ **Predictable**: Know contract addresses before deployment +✅ **Frontrun Protected**: Using sender-specific salts prevents address squatting +✅ **Multi-Chain Ready**: Deploy to new networks without nonce coordination + +### CREATE2 Deployment Strategy + +#### Step 1: Deploy the CREATE2 Factory + +The `Create2Factory` contract must be deployed **once per network** using a **fresh EOA** (recommended for consistency, though not strictly required). + +```bash +# Set up environment +source .env + +# Deploy the factory +forge script script/DeployCreate2Factory.s.sol:DeployCreate2Factory \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv +``` + +**Important**: +- The script will warn if your EOA has a non-zero nonce +- For maximum consistency, use a fresh EOA (nonce 0) to deploy the factory on all chains +- Save the factory address - you'll need it for all future deployments + +After deployment, add the factory address to `deployments/create2-factories.json`: + +```json +{ + "ethereum": "0x...", + "arbitrum": "0x...", + "bsc": "0x...", + "hyperevm": "0x..." +} +``` + +#### Step 2: Deploy AA Contracts via CREATE2 + +Once the factory is deployed, use it to deploy AA contracts: + +```bash +# Set factory address and version +export CREATE2_FACTORY_ADDRESS=0x... # From step 1 +export CONTRACT_VERSION=v1.0.0 # Version for salt generation + +# Deploy AA contracts via CREATE2 +forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv +``` + +#### Step 3: Deploy to Additional Networks + +To deploy to a new network with the **same addresses**: + +1. Deploy the CREATE2 factory on the new network (step 1) +2. Use the **same deployer EOA** and **same version** from step 2 +3. Contracts will deploy to **identical addresses**! + +```bash +# Example: Deploy to new network +export RPC_URL=https://new-network-rpc.example.com +export CREATE2_FACTORY_ADDRESS=0x... # Factory on new network + +# Same version = same addresses! +export CONTRACT_VERSION=v1.0.0 + +forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv +``` + +### CREATE2 Configuration + +All standard environment variables from `Deploy.s.sol` are supported, plus: + +```bash +# CREATE2-specific variables +CREATE2_FACTORY_ADDRESS=0x... # Address of deployed Create2Factory (required) +CONTRACT_VERSION=v1.0.0 # Version string for salt generation (default: v1.0.0) + +# All standard variables still work +DEPLOY_ENTRYPOINT=true +DEPLOY_FACTORY=true +DEPLOY_SIMPLE_PAYMASTER=true +DEPLOY_ERC20_PAYMASTER=false +PAYMASTER_INITIAL_DEPOSIT=1000000000000000000 +INITIAL_BUNDLER=0x... +SAVE_DEPLOYMENT_FILE=true +DEPLOYMENT_ENV=production +``` + +### Salt Generation Strategy + +The `DeployWithCreate2` script uses **sender-protected salts** to prevent frontrunning: + +```solidity +salt = keccak256(abi.encode(contractName, version, msg.sender)) +``` + +This means: +- **Same deployer EOA** + **same version** = **same addresses** across all chains +- Different deployers will get different addresses (security feature) +- Update `CONTRACT_VERSION` when you want new addresses for updated contracts + +### Address Prediction + +Before deployment, the script shows predicted addresses: + +``` +=== Predicted Addresses === +EntryPointV1 (predicted): 0x1234... +OmniAccountFactoryV1 (predicted): 0x5678... +SimplePaymaster (predicted): 0xabcd... +``` + +You can also compute addresses manually: + +```solidity +// In Solidity +Create2Factory factory = Create2Factory(factoryAddress); +bytes32 salt = factory.generateSalt("EntryPointV1", "v1.0.0", msg.sender); +address predicted = factory.computeAddress(salt, type(EntryPointV1).creationCode); +``` + +```bash +# Using cast +cast call $FACTORY_ADDRESS "computeAddress(bytes32,bytes)(address)" \ + $SALT \ + $(cast --from-utf8 "$(cat out/EntryPointV1.sol/EntryPointV1.json | jq -r .bytecode.object)") +``` + +### Migration from Standard Deployments + +**Current deployments are preserved** - no migration needed! + +- Existing contracts on 15+ networks continue to work +- CREATE2 factory is used **only for future deployments** +- When deploying to new networks, use CREATE2 for consistency + +### Deterministic Bytecode Configuration + +The `foundry.toml` has been configured for deterministic builds: + +```toml +solc_version = "0.8.28" +evm_version = "cancun" +bytecode_hash = "none" # Critical for determinism +cbor_metadata = false # Critical for determinism +optimizer = true +optimizer_runs = 1000000 +``` + +**Important**: These settings ensure identical bytecode across builds, which is essential for CREATE2 determinism. Do not modify these settings between deployments. + +### Troubleshooting + +**Problem**: Addresses don't match across chains +**Solution**: Ensure you're using the same deployer EOA and CONTRACT_VERSION + +**Problem**: Factory deployment fails +**Solution**: Make sure you have enough ETH for deployment gas + +**Problem**: "AddressAlreadyDeployed" error +**Solution**: Contract was already deployed. Either use it or change the CONTRACT_VERSION + +**Problem**: Verification fails with "Bytecode does not match" +**Solution**: Ensure your local build uses the same compiler settings as deployment + ## 📁 Deployment Artifacts After successful deployment, you'll find: From e3216955840d93a4f2581abe48de277127e80583 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 5 Nov 2025 20:43:26 +0100 Subject: [PATCH 08/14] Rename Create2Factory to Create2FactoryV1 --- ...reate2Factory.sol => Create2FactoryV1.sol} | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) rename tee-worker/omni-executor/aa-contracts/src/core/{Create2Factory.sol => Create2FactoryV1.sol} (69%) diff --git a/tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol b/tee-worker/omni-executor/aa-contracts/src/core/Create2FactoryV1.sol similarity index 69% rename from tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol rename to tee-worker/omni-executor/aa-contracts/src/core/Create2FactoryV1.sol index 314c558603..3c3cd8150e 100644 --- a/tee-worker/omni-executor/aa-contracts/src/core/Create2Factory.sol +++ b/tee-worker/omni-executor/aa-contracts/src/core/Create2FactoryV1.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.28; /** - * @title Create2Factory + * @title Create2FactoryV1 * @notice Factory contract for deterministic contract deployments using CREATE2 * @dev Provides deterministic address generation across multiple EVM chains * This contract should be deployed using a fresh EOA on each network */ -contract Create2Factory { +contract Create2FactoryV1 { /// @notice Emitted when a contract is successfully deployed /// @param deployed The address of the newly deployed contract /// @param salt The salt used for deployment @@ -69,9 +69,8 @@ contract Create2Factory { */ function computeAddress(bytes32 salt, bytes memory bytecode) public view returns (address predicted) { bytes32 bytecodeHash = keccak256(bytecode); - predicted = address( - uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)))) - ); + predicted = + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash))))); } /** @@ -81,9 +80,8 @@ contract Create2Factory { * @return predicted The predicted address of the contract */ function computeAddressWithHash(bytes32 salt, bytes32 bytecodeHash) public view returns (address predicted) { - predicted = address( - uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)))) - ); + predicted = + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash))))); } /** @@ -96,35 +94,14 @@ contract Create2Factory { } /** - * @notice Generates a sender-protected salt for deployment + * @notice Generates a deterministic salt for deployment * @param contractName The name/identifier of the contract - * @param version The version string - * @param sender The address of the deployer (typically msg.sender) * @return salt The generated salt - * @dev Sender-protected salts prevent frontrunning by including the deployer address - * This ensures only the specified sender can deploy to the computed address + * @dev This generates a purely deterministic salt based only on contract name. + * The same contract name will always produce the same address across all chains. + * This means addresses are predictable and consistent for all deployers. */ - function generateSalt(string memory contractName, string memory version, address sender) - external - pure - returns (bytes32 salt) - { - salt = keccak256(abi.encode(contractName, version, sender)); - } - - /** - * @notice Generates a simple salt for deployment (less secure, vulnerable to frontrunning) - * @param contractName The name/identifier of the contract - * @param version The version string - * @return salt The generated salt - * @dev Simple salts allow anyone to deploy to the computed address (frontrunning risk) - * Only use this if you don't care about frontrunning protection - */ - function generateSimpleSalt(string memory contractName, string memory version) - external - pure - returns (bytes32 salt) - { - salt = keccak256(abi.encode(contractName, version)); + function generateSalt(string memory contractName) external pure returns (bytes32 salt) { + salt = keccak256(abi.encode(contractName)); } } From e520a4ed3f0f8846e334c6c95825dc51d75b3cd4 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 5 Nov 2025 20:43:38 +0100 Subject: [PATCH 09/14] Update deployment scripts for Create2FactoryV1 and new generateSalt signature --- .../script/DeployCreate2Factory.s.sol | 16 ++-- .../script/DeployWithCreate2.s.sol | 74 +++++++------------ 2 files changed, 32 insertions(+), 58 deletions(-) diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol index 29fb90e893..3743512ecc 100644 --- a/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol +++ b/tee-worker/omni-executor/aa-contracts/script/DeployCreate2Factory.s.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.28; import "forge-std/Script.sol"; import "forge-std/console.sol"; -import "../src/core/Create2Factory.sol"; +import "../src/core/Create2FactoryV1.sol"; /** * @title DeployCreate2Factory - * @notice Deployment script for the Create2Factory contract + * @notice Deployment script for the Create2FactoryV1 contract * @dev This script should be run with a FRESH EOA that has not deployed contracts before * The factory address will be deterministic based on the deployer's address and nonce * @@ -35,7 +35,7 @@ contract DeployCreate2Factory is Script { NetworkConfig memory networkConfig = getNetworkConfig(); // Log deployment info - console.log("=== Create2Factory Deployment ==="); + console.log("=== Create2FactoryV1 Deployment ==="); console.log("Network:", networkConfig.name); console.log("Chain ID:", networkConfig.chainId); console.log("Deployer address:", deployer); @@ -46,9 +46,7 @@ contract DeployCreate2Factory is Script { // Warning about fresh EOA requirement if (vm.getNonce(deployer) > 0) { console.log("WARNING: Deployer has non-zero nonce!"); - console.log( - "For consistent factory addresses across chains, it's recommended to use a fresh EOA (nonce 0)" - ); + console.log("For consistent factory addresses across chains, it's recommended to use a fresh EOA (nonce 0)"); console.log("Current nonce:", vm.getNonce(deployer)); console.log(""); } @@ -59,16 +57,16 @@ contract DeployCreate2Factory is Script { console.log(""); // Deploy factory - console.log("Deploying Create2Factory..."); + console.log("Deploying Create2FactoryV1..."); vm.startBroadcast(deployerPrivateKey); - Create2Factory factory = new Create2Factory(); + Create2FactoryV1 factory = new Create2FactoryV1(); vm.stopBroadcast(); // Verify deployment require(address(factory) == predictedFactory, "Factory address mismatch!"); - console.log("Create2Factory deployed at:", address(factory)); + console.log("Create2FactoryV1 deployed at:", address(factory)); console.log(""); // Print summary diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol index 50c4e38293..a412459fce 100644 --- a/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol +++ b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol @@ -7,33 +7,29 @@ import "../src/core/EntryPointV1.sol"; import "../src/accounts/OmniAccountFactoryV1.sol"; import "../src/core/SimplePaymaster.sol"; import "../src/core/ERC20PaymasterV1.sol"; -import "../src/core/Create2Factory.sol"; +import "../src/core/Create2FactoryV1.sol"; import "./DeploymentHelper.sol"; /** * @title DeployWithCreate2 * @notice Deployment script for AA contracts using CREATE2 for deterministic addresses - * @dev This script uses a pre-deployed Create2Factory to deploy contracts with deterministic addresses - * across multiple EVM chains. All deployments maintain the same addresses when using the same - * deployer EOA and contract versions. + * @dev This script uses a pre-deployed Create2FactoryV1 to deploy contracts with deterministic addresses + * across multiple EVM chains. All deployments maintain the same addresses regardless of deployer. * * Prerequisites: - * 1. Create2Factory must be deployed on the target network + * 1. Create2FactoryV1 must be deployed on the target network * 2. Factory address must be provided via CREATE2_FACTORY_ADDRESS environment variable - * 3. Same deployer EOA should be used across all chains for address consistency * * Usage: * forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 --rpc-url --broadcast --verify * * Environment Variables: - * CREATE2_FACTORY_ADDRESS - Address of the deployed Create2Factory (required) - * CONTRACT_VERSION - Version string for salt generation (default: "v1.0.0") + * CREATE2_FACTORY_ADDRESS - Address of the deployed Create2FactoryV1 (required) * All other environment variables from Deploy.s.sol are supported */ contract DeployWithCreate2 is Script { // CREATE2 configuration - Create2Factory public factory; - string public contractVersion; + Create2FactoryV1 public factory; // Configuration - can be overridden via environment variables uint256 public paymasterInitialDeposit; @@ -67,10 +63,7 @@ contract DeployWithCreate2 is Script { address factoryAddr = vm.envAddress("CREATE2_FACTORY_ADDRESS"); require(factoryAddr != address(0), "CREATE2_FACTORY_ADDRESS not set"); require(factoryAddr.code.length > 0, "CREATE2_FACTORY_ADDRESS is not a contract"); - factory = Create2Factory(factoryAddr); - - // Load contract version - contractVersion = vm.envOr("CONTRACT_VERSION", string("v1.0.0")); + factory = Create2FactoryV1(factoryAddr); // Load configuration loadConfiguration(); @@ -85,8 +78,7 @@ contract DeployWithCreate2 is Script { console.log("Deployer address:", deployer); console.log("Deployer balance:", deployer.balance / 1e18, "ETH"); console.log("Block number:", block.number); - console.log("Create2Factory:", address(factory)); - console.log("Contract version:", contractVersion); + console.log("Create2FactoryV1:", address(factory)); console.log(""); // Check minimum deployment balance @@ -107,39 +99,35 @@ contract DeployWithCreate2 is Script { // Predict all addresses before deployment console.log("=== Predicted Addresses ==="); if (shouldDeployEntryPoint) { - bytes32 entryPointSalt = factory.generateSalt("EntryPointV1", contractVersion, deployer); + bytes32 entryPointSalt = factory.generateSalt("EntryPointV1"); address predictedEntryPoint = factory.computeAddress(entryPointSalt, abi.encodePacked(type(EntryPointV1).creationCode)); console.log("EntryPointV1 (predicted):", predictedEntryPoint); } if (shouldDeployFactory) { - bytes32 factorySalt = factory.generateSalt("OmniAccountFactoryV1", contractVersion, deployer); + bytes32 factorySalt = factory.generateSalt("OmniAccountFactoryV1"); address predictedFactory = factory.computeAddress( factorySalt, - abi.encodePacked( - type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress)) - ) + abi.encodePacked(type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress))) ); console.log("OmniAccountFactoryV1 (predicted):", predictedFactory); } if (shouldDeploySimplePaymaster) { - bytes32 paymasterSalt = factory.generateSalt("SimplePaymaster", contractVersion, deployer); + bytes32 paymasterSalt = factory.generateSalt("SimplePaymaster"); address predictedPaymaster = factory.computeAddress( paymasterSalt, abi.encodePacked( - type(SimplePaymaster).creationCode, - abi.encode(IEntryPoint(entryPointAddress), initialBundler) + type(SimplePaymaster).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) ) ); console.log("SimplePaymaster (predicted):", predictedPaymaster); } if (shouldDeployERC20Paymaster) { - bytes32 erc20PaymasterSalt = factory.generateSalt("ERC20PaymasterV1", contractVersion, deployer); + bytes32 erc20PaymasterSalt = factory.generateSalt("ERC20PaymasterV1"); address predictedERC20Paymaster = factory.computeAddress( erc20PaymasterSalt, abi.encodePacked( - type(ERC20PaymasterV1).creationCode, - abi.encode(IEntryPoint(entryPointAddress), initialBundler) + type(ERC20PaymasterV1).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) ) ); console.log("ERC20PaymasterV1 (predicted):", predictedERC20Paymaster); @@ -294,7 +282,7 @@ contract DeployWithCreate2 is Script { function deployEntryPoint(address deployer) internal { console.log("Deploying EntryPointV1 via CREATE2..."); - bytes32 salt = factory.generateSalt("EntryPointV1", contractVersion, deployer); + bytes32 salt = factory.generateSalt("EntryPointV1"); bytes memory bytecode = abi.encodePacked(type(EntryPointV1).creationCode); address deployed = factory.deploy(salt, bytecode); @@ -307,7 +295,7 @@ contract DeployWithCreate2 is Script { function deployAccountFactory(address deployer) internal { console.log("Deploying OmniAccountFactoryV1 via CREATE2..."); - bytes32 salt = factory.generateSalt("OmniAccountFactoryV1", contractVersion, deployer); + bytes32 salt = factory.generateSalt("OmniAccountFactoryV1"); bytes memory bytecode = abi.encodePacked(type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress))); @@ -322,7 +310,7 @@ contract DeployWithCreate2 is Script { function deployPaymaster(address deployer) internal { console.log("Deploying SimplePaymaster via CREATE2..."); - bytes32 salt = factory.generateSalt("SimplePaymaster", contractVersion, deployer); + bytes32 salt = factory.generateSalt("SimplePaymaster"); bytes memory bytecode = abi.encodePacked( type(SimplePaymaster).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) ); @@ -339,7 +327,7 @@ contract DeployWithCreate2 is Script { function deployERC20Paymaster(address deployer) internal { console.log("Deploying ERC20PaymasterV1 via CREATE2..."); - bytes32 salt = factory.generateSalt("ERC20PaymasterV1", contractVersion, deployer); + bytes32 salt = factory.generateSalt("ERC20PaymasterV1"); bytes memory bytecode = abi.encodePacked( type(ERC20PaymasterV1).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) ); @@ -419,11 +407,10 @@ contract DeployWithCreate2 is Script { console.log("Network:", networkConfig.name); console.log("Chain ID:", networkConfig.chainId); console.log("Deployment Date:", block.timestamp); - console.log("Create2Factory:", address(factory)); - console.log("Contract Version:", contractVersion); + console.log("Create2FactoryV1:", address(factory)); console.log(""); console.log("IMPORTANT: These addresses are deterministic across all chains!"); - console.log("Using the same deployer EOA and version will yield the same addresses."); + console.log("The same contract names will always yield the same addresses."); console.log(""); } @@ -447,10 +434,7 @@ contract DeployWithCreate2 is Script { if (shouldDeployEntryPoint) { deployments[currentIndex] = DeploymentHelper.createContractDeployment( - vm, - "EntryPointV1", - entryPointAddress, - string(abi.encodePacked('{"deploymentMethod": "CREATE2", "version": "', contractVersion, '"}')) + vm, "EntryPointV1", entryPointAddress, '{"deploymentMethod": "CREATE2"}' ); currentIndex++; } @@ -462,11 +446,7 @@ contract DeployWithCreate2 is Script { factoryAddress, string( abi.encodePacked( - '{"entryPoint": "', - vm.toString(entryPointAddress), - '", "deploymentMethod": "CREATE2", "version": "', - contractVersion, - '"}' + '{"entryPoint": "', vm.toString(entryPointAddress), '", "deploymentMethod": "CREATE2"}' ) ) ); @@ -484,9 +464,7 @@ contract DeployWithCreate2 is Script { vm.toString(initialBundler), '", "entryPoint": "', vm.toString(entryPointAddress), - '", "deploymentMethod": "CREATE2", "version": "', - contractVersion, - '"}' + '", "deploymentMethod": "CREATE2"}' ) ) ); @@ -504,9 +482,7 @@ contract DeployWithCreate2 is Script { vm.toString(initialBundler), '", "entryPoint": "', vm.toString(entryPointAddress), - '", "deploymentMethod": "CREATE2", "version": "', - contractVersion, - '"}' + '", "deploymentMethod": "CREATE2"}' ) ) ); From bb8a28abb9f63fd7a310af5b00a78a50910921d9 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 5 Nov 2025 20:43:58 +0100 Subject: [PATCH 10/14] Update tests for Create2FactoryV1 and new generateSalt signature --- .../aa-contracts/test/Create2Factory.t.sol | 116 +++++------------- 1 file changed, 33 insertions(+), 83 deletions(-) diff --git a/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol b/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol index 8fc6cac8ef..624107d3f0 100644 --- a/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol +++ b/tee-worker/omni-executor/aa-contracts/test/Create2Factory.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.28; import {Test, console} from "forge-std/Test.sol"; -import {Create2Factory} from "../src/core/Create2Factory.sol"; +import {Create2FactoryV1} from "../src/core/Create2FactoryV1.sol"; import {Counter} from "../src/Counter.sol"; contract Create2FactoryTest is Test { - Create2Factory public factory; + Create2FactoryV1 public factory; address deployer1 = makeAddr("deployer1"); address deployer2 = makeAddr("deployer2"); @@ -14,7 +14,7 @@ contract Create2FactoryTest is Test { event ContractDeployed(address indexed deployed, bytes32 indexed salt, address indexed deployer); function setUp() public { - factory = new Create2Factory(); + factory = new Create2FactoryV1(); } // ============ Constructor Tests ============ @@ -37,9 +37,8 @@ contract Create2FactoryTest is Test { // Manual calculation should match bytes32 bytecodeHash = keccak256(bytecode); - address expectedAddress = address( - uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, bytecodeHash)))) - ); + address expectedAddress = + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, bytecodeHash))))); assertEq(predicted, expectedAddress); } @@ -176,7 +175,7 @@ contract Create2FactoryTest is Test { // Second deployment should revert vm.expectRevert( abi.encodeWithSelector( - Create2Factory.AddressAlreadyDeployed.selector, factory.computeAddress(salt, bytecode) + Create2FactoryV1.AddressAlreadyDeployed.selector, factory.computeAddress(salt, bytecode) ) ); factory.deploy(salt, bytecode); @@ -192,7 +191,7 @@ contract Create2FactoryTest is Test { address deployed = factory.deploy(salt, bytecode); // Try to deploy again - should revert - vm.expectRevert(abi.encodeWithSelector(Create2Factory.AddressAlreadyDeployed.selector, deployed)); + vm.expectRevert(abi.encodeWithSelector(Create2FactoryV1.AddressAlreadyDeployed.selector, deployed)); factory.deploy(salt, bytecode); } @@ -200,75 +199,29 @@ contract Create2FactoryTest is Test { function test_GenerateSalt() public view { string memory contractName = "TestContract"; - string memory version = "v1.0.0"; - address sender = deployer1; - bytes32 salt = factory.generateSalt(contractName, version, sender); + bytes32 salt = factory.generateSalt(contractName); // Salt should be non-zero assertTrue(salt != bytes32(0)); // Salt should be deterministic - bytes32 salt2 = factory.generateSalt(contractName, version, sender); + bytes32 salt2 = factory.generateSalt(contractName); assertEq(salt, salt2); - // Salt should include sender (different sender = different salt) - bytes32 salt3 = factory.generateSalt(contractName, version, deployer2); - assertTrue(salt != salt3); + // Salt should be consistent across all deployers (deterministic addresses) + // Same contract name always produces same salt + assertEq(keccak256(abi.encode(contractName)), salt); } function test_GenerateSalt_DifferentNames() public view { - bytes32 salt1 = factory.generateSalt("Contract1", "v1.0.0", deployer1); - bytes32 salt2 = factory.generateSalt("Contract2", "v1.0.0", deployer1); + bytes32 salt1 = factory.generateSalt("Contract1"); + bytes32 salt2 = factory.generateSalt("Contract2"); // Different names should produce different salts assertTrue(salt1 != salt2); } - function test_GenerateSalt_DifferentVersions() public view { - bytes32 salt1 = factory.generateSalt("TestContract", "v1.0.0", deployer1); - bytes32 salt2 = factory.generateSalt("TestContract", "v2.0.0", deployer1); - - // Different versions should produce different salts - assertTrue(salt1 != salt2); - } - - function test_GenerateSalt_DifferentSenders() public view { - bytes32 salt1 = factory.generateSalt("TestContract", "v1.0.0", deployer1); - bytes32 salt2 = factory.generateSalt("TestContract", "v1.0.0", deployer2); - - // Different senders should produce different salts (frontrunning protection) - assertTrue(salt1 != salt2); - } - - function test_GenerateSimpleSalt() public view { - string memory contractName = "TestContract"; - string memory version = "v1.0.0"; - - bytes32 salt = factory.generateSimpleSalt(contractName, version); - - // Salt should be non-zero - assertTrue(salt != bytes32(0)); - - // Salt should be deterministic - bytes32 salt2 = factory.generateSimpleSalt(contractName, version); - assertEq(salt, salt2); - - // Simple salt should be same for all senders (no frontrunning protection) - // This is implicit - the function doesn't take a sender parameter - } - - function test_GenerateSimpleSalt_VsGenerateSalt() public view { - string memory contractName = "TestContract"; - string memory version = "v1.0.0"; - - bytes32 simpleSalt = factory.generateSimpleSalt(contractName, version); - bytes32 protectedSalt = factory.generateSalt(contractName, version, deployer1); - - // Simple salt and protected salt should be different - assertTrue(simpleSalt != protectedSalt); - } - // ============ Deployment Tracking Tests ============ function test_IsDeployed() public { @@ -296,22 +249,20 @@ contract Create2FactoryTest is Test { // ============ Integration Tests ============ - function test_EndToEnd_SenderProtectedDeployment() public { - // Scenario: Deploy same contract to same address on multiple "chains" (test runs) - // using sender-protected salt + function test_EndToEnd_DeterministicDeployment() public { + // Scenario: Deploy same contract to same address regardless of deployer + // using deterministic salt based only on contract name string memory contractName = "EntryPointV1"; - string memory version = "v1.0.0"; - // Generate sender-protected salt - vm.prank(deployer1); - bytes32 salt = factory.generateSalt(contractName, version, deployer1); + // Generate deterministic salt + bytes32 salt = factory.generateSalt(contractName); // Predict address bytes memory bytecode = type(Counter).creationCode; address predicted = factory.computeAddress(salt, bytecode); - // Deploy + // Deploy (can be from any deployer) vm.prank(deployer1); address deployed = factory.deploy(salt, bytecode); @@ -319,10 +270,9 @@ contract Create2FactoryTest is Test { assertEq(deployed, predicted); assertTrue(factory.isDeployed(deployed)); - // Verify only deployer1 can deploy to this address - // (deployer2 would generate a different salt and thus different address) - bytes32 salt2 = factory.generateSalt(contractName, version, deployer2); - assertTrue(salt != salt2); + // Verify same salt is generated regardless of who calls it + bytes32 salt2 = factory.generateSalt(contractName); + assertEq(salt, salt2); } function test_EndToEnd_MultiChainDeployment() public { @@ -330,11 +280,10 @@ contract Create2FactoryTest is Test { // In reality this would be different factory instances, but we can test the logic string memory contractName = "TestContract"; - string memory version = "v1.0.0"; bytes memory bytecode = type(Counter).creationCode; - // "Chain 1" deployment - bytes32 salt1 = factory.generateSalt(contractName, version, deployer1); + // "Chain 1" deployment (any deployer gets same address) + bytes32 salt1 = factory.generateSalt(contractName); address predicted1 = factory.computeAddress(salt1, bytecode); vm.prank(deployer1); address deployed1 = factory.deploy(salt1, bytecode); @@ -342,8 +291,8 @@ contract Create2FactoryTest is Test { assertEq(deployed1, predicted1); // In a real multi-chain scenario, deploying with the same salt and bytecode - // from the same deployer on a different chain (different factory instance) - // would yield the same address - but the factory address would need to be the same + // on a different chain (different factory instance) will yield the same address + // as long as the factory is deployed to the same address on each chain // That's why we deploy the factory with a fresh EOA on each chain } @@ -370,14 +319,15 @@ contract Create2FactoryTest is Test { assertTrue(factory.isDeployed(deployed)); } - function testFuzz_GenerateSalt(string memory name, string memory version, address sender) public view { - vm.assume(sender != address(0)); - - bytes32 salt = factory.generateSalt(name, version, sender); + function testFuzz_GenerateSalt(string memory name) public view { + bytes32 salt = factory.generateSalt(name); // Salt should be deterministic - bytes32 salt2 = factory.generateSalt(name, version, sender); + bytes32 salt2 = factory.generateSalt(name); assertEq(salt, salt2); + + // Salt should match manual calculation + assertEq(salt, keccak256(abi.encode(name))); } // ============ Edge Cases ============ From c4d6c3bbc1408a71d5fd1943879b56a36a1d7748 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 5 Nov 2025 20:44:08 +0100 Subject: [PATCH 11/14] Add DeployContract script for individual contract deployments --- .../aa-contracts/script/DeployContract.s.sol | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 tee-worker/omni-executor/aa-contracts/script/DeployContract.s.sol diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployContract.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployContract.s.sol new file mode 100644 index 0000000000..28273bda7d --- /dev/null +++ b/tee-worker/omni-executor/aa-contracts/script/DeployContract.s.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../src/core/EntryPointV1.sol"; +import "../src/accounts/OmniAccountFactoryV1.sol"; +import "../src/core/SimplePaymaster.sol"; +import "../src/core/ERC20PaymasterV1.sol"; +import "../src/core/Create2FactoryV1.sol"; +import "./DeploymentHelper.sol"; + +/** + * @title DeployContract + * @notice Deployment script for deploying individual AA contracts using CREATE2 + * @dev This script deploys a single contract specified by the CONTRACT_NAME environment variable + * + * Prerequisites: + * 1. Create2FactoryV1 must be deployed on the target network + * 2. Factory address must be provided via CREATE2_FACTORY_ADDRESS environment variable + * + * Usage: + * CONTRACT_NAME=EntryPointV1 forge script script/DeployContract.s.sol:DeployContract --rpc-url --broadcast + * CONTRACT_NAME=OmniAccountFactoryV1 ENTRYPOINT_ADDRESS=0x... forge script script/DeployContract.s.sol:DeployContract --rpc-url --broadcast + * + * Environment Variables: + * CONTRACT_NAME - Name of the contract to deploy (required) + * Valid values: EntryPointV1, OmniAccountFactoryV1, SimplePaymaster, ERC20PaymasterV1 + * CREATE2_FACTORY_ADDRESS - Address of the deployed Create2FactoryV1 (required) + * ENTRYPOINT_ADDRESS - Address of deployed EntryPoint (required for Factory/Paymaster contracts) + * INITIAL_BUNDLER - Address of initial bundler (optional, defaults to deployer) + * PAYMASTER_INITIAL_DEPOSIT - Initial deposit for paymaster in wei (optional, default: 1 ETH) + * SAVE_DEPLOYMENT_FILE - Save deployment artifacts to file (optional, default: false) + * DEPLOYMENT_ENV - Environment subdirectory for deployment files (optional) + */ +contract DeployContract is Script { + // CREATE2 configuration + Create2FactoryV1 public factory; + string public contractName; + + // Configuration + address public entryPointAddress; + address public initialBundler; + uint256 public paymasterInitialDeposit; + bool public saveDeploymentFile; + + // Deployed contract address + address public deployedAddress; + + // Network configuration + struct NetworkConfig { + string name; + uint256 chainId; + uint256 minDeploymentBalance; + } + + function run() external { + // Get deployment parameters + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + // Load CREATE2 factory + address factoryAddr = vm.envAddress("CREATE2_FACTORY_ADDRESS"); + require(factoryAddr != address(0), "CREATE2_FACTORY_ADDRESS not set"); + require(factoryAddr.code.length > 0, "CREATE2_FACTORY_ADDRESS is not a contract"); + factory = Create2FactoryV1(factoryAddr); + + // Load contract name + contractName = vm.envString("CONTRACT_NAME"); + require(bytes(contractName).length > 0, "CONTRACT_NAME not set"); + + // Load configuration + loadConfiguration(deployer); + + // Get network info + NetworkConfig memory networkConfig = getNetworkConfig(); + + // Log deployment info + console.log("=== Single Contract Deployment (CREATE2) ==="); + console.log("Contract:", contractName); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Deployer address:", deployer); + console.log("Deployer balance:", deployer.balance / 1e18, "ETH"); + console.log("Block number:", block.number); + console.log("Create2FactoryV1:", address(factory)); + console.log(""); + + // Check minimum deployment balance + require( + deployer.balance >= networkConfig.minDeploymentBalance, + string( + abi.encodePacked( + "Insufficient balance for deployment (need at least ", + vm.toString(networkConfig.minDeploymentBalance / 1e18), + " ETH)" + ) + ) + ); + + // Validate dependencies + validateDependencies(); + + // Predict address + predictAddress(); + + // Deploy contract + vm.startBroadcast(deployerPrivateKey); + deployContract(); + vm.stopBroadcast(); + + // Log deployment results + logDeploymentResults(networkConfig); + + // Save deployment addresses to file (if enabled) + if (saveDeploymentFile) { + saveDeploymentAddress(networkConfig); + } else { + console.log("Deployment file saving disabled (set SAVE_DEPLOYMENT_FILE=true to enable)"); + } + } + + function loadConfiguration(address deployer) internal { + // Load EntryPoint address if required + if ( + keccak256(bytes(contractName)) == keccak256("OmniAccountFactoryV1") + || keccak256(bytes(contractName)) == keccak256("SimplePaymaster") + || keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1") + ) { + entryPointAddress = vm.envAddress("ENTRYPOINT_ADDRESS"); + require(entryPointAddress != address(0), "ENTRYPOINT_ADDRESS required for this contract"); + require(entryPointAddress.code.length > 0, "ENTRYPOINT_ADDRESS must be a deployed contract"); + } + + // Load paymaster configuration + if ( + keccak256(bytes(contractName)) == keccak256("SimplePaymaster") + || keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1") + ) { + initialBundler = vm.envOr("INITIAL_BUNDLER", deployer); + paymasterInitialDeposit = vm.envOr("PAYMASTER_INITIAL_DEPOSIT", uint256(1 ether)); + } + + // Load save deployment file flag + saveDeploymentFile = vm.envOr("SAVE_DEPLOYMENT_FILE", false); + + console.log("Configuration:"); + console.log("- Contract:", contractName); + if (entryPointAddress != address(0)) { + console.log("- EntryPoint:", entryPointAddress); + } + if (initialBundler != address(0)) { + console.log("- Initial bundler:", initialBundler); + console.log("- Paymaster initial deposit:", paymasterInitialDeposit / 1e18, "ETH"); + } + console.log("- Save deployment file:", saveDeploymentFile ? "Yes" : "No"); + console.log(""); + } + + function validateDependencies() internal view { + // Validate contract name + require( + keccak256(bytes(contractName)) == keccak256("EntryPointV1") + || keccak256(bytes(contractName)) == keccak256("OmniAccountFactoryV1") + || keccak256(bytes(contractName)) == keccak256("SimplePaymaster") + || keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1"), + string(abi.encodePacked("Unknown contract name: ", contractName)) + ); + } + + function predictAddress() internal view { + console.log("=== Predicted Address ==="); + bytes32 salt = factory.generateSalt(contractName); + + if (keccak256(bytes(contractName)) == keccak256("EntryPointV1")) { + address predicted = factory.computeAddress(salt, abi.encodePacked(type(EntryPointV1).creationCode)); + console.log(string(abi.encodePacked(contractName, " (predicted):")), predicted); + } else if (keccak256(bytes(contractName)) == keccak256("OmniAccountFactoryV1")) { + address predicted = factory.computeAddress( + salt, + abi.encodePacked(type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress))) + ); + console.log(string(abi.encodePacked(contractName, " (predicted):")), predicted); + } else if (keccak256(bytes(contractName)) == keccak256("SimplePaymaster")) { + address predicted = factory.computeAddress( + salt, + abi.encodePacked( + type(SimplePaymaster).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ) + ); + console.log(string(abi.encodePacked(contractName, " (predicted):")), predicted); + } else if (keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1")) { + address predicted = factory.computeAddress( + salt, + abi.encodePacked( + type(ERC20PaymasterV1).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ) + ); + console.log(string(abi.encodePacked(contractName, " (predicted):")), predicted); + } + console.log(""); + } + + function deployContract() internal { + console.log(string(abi.encodePacked("Deploying ", contractName, " via CREATE2..."))); + + bytes32 salt = factory.generateSalt(contractName); + bytes memory bytecode; + + if (keccak256(bytes(contractName)) == keccak256("EntryPointV1")) { + bytecode = abi.encodePacked(type(EntryPointV1).creationCode); + } else if (keccak256(bytes(contractName)) == keccak256("OmniAccountFactoryV1")) { + bytecode = + abi.encodePacked(type(OmniAccountFactoryV1).creationCode, abi.encode(IEntryPoint(entryPointAddress))); + } else if (keccak256(bytes(contractName)) == keccak256("SimplePaymaster")) { + bytecode = abi.encodePacked( + type(SimplePaymaster).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ); + } else if (keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1")) { + bytecode = abi.encodePacked( + type(ERC20PaymasterV1).creationCode, abi.encode(IEntryPoint(entryPointAddress), initialBundler) + ); + } + + deployedAddress = factory.deploy(salt, bytecode); + + console.log(string(abi.encodePacked(contractName, " deployed at:")), deployedAddress); + if (entryPointAddress != address(0)) { + console.log("EntryPoint reference:", entryPointAddress); + } + if (initialBundler != address(0)) { + console.log("Initial bundler:", initialBundler); + } + console.log(""); + + // Initialize paymaster if configured + if ( + ( + keccak256(bytes(contractName)) == keccak256("SimplePaymaster") + || keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1") + ) && paymasterInitialDeposit > 0 + ) { + initializePaymaster(); + } + } + + function initializePaymaster() internal { + console.log("Initializing Paymaster with deposit..."); + + if (keccak256(bytes(contractName)) == keccak256("SimplePaymaster")) { + SimplePaymaster paymaster = SimplePaymaster(payable(deployedAddress)); + paymaster.deposit{value: paymasterInitialDeposit}(); + console.log("Added deposit:", paymasterInitialDeposit / 1e18, "ETH"); + } else if (keccak256(bytes(contractName)) == keccak256("ERC20PaymasterV1")) { + ERC20PaymasterV1 paymaster = ERC20PaymasterV1(payable(deployedAddress)); + paymaster.deposit{value: paymasterInitialDeposit}(); + console.log("Added deposit:", paymasterInitialDeposit / 1e18, "ETH"); + } + + console.log("Paymaster initialized"); + console.log(""); + } + + function getNetworkConfig() internal view returns (NetworkConfig memory) { + uint256 chainId = block.chainid; + + if (chainId == 1) { + return NetworkConfig("Ethereum Mainnet", 1, 0.01 ether); + } else if (chainId == 11155111) { + return NetworkConfig("Ethereum Sepolia", 11155111, 0.01 ether); + } else if (chainId == 56) { + return NetworkConfig("BSC Mainnet", 56, 0.01 ether); + } else if (chainId == 97) { + return NetworkConfig("BSC Testnet", 97, 0.05 ether); + } else if (chainId == 8453) { + return NetworkConfig("Base", 8453, 0.01 ether); + } else if (chainId == 84532) { + return NetworkConfig("Base Sepolia", 84532, 0.05 ether); + } else if (chainId == 137) { + return NetworkConfig("Polygon Mainnet", 137, 1 ether); + } else if (chainId == 80001) { + return NetworkConfig("Polygon Mumbai", 80001, 0.1 ether); + } else if (chainId == 42161) { + return NetworkConfig("Arbitrum Mainnet", 42161, 0.01 ether); + } else if (chainId == 421614) { + return NetworkConfig("Arbitrum Sepolia", 421614, 0.01 ether); + } else if (chainId == 999) { + return NetworkConfig("HyperEVM Mainnet", 999, 0.01 ether); + } else if (chainId == 998) { + return NetworkConfig("HyperEVM Testnet", 998, 0.01 ether); + } else if (chainId == 1337) { + return NetworkConfig("Local Anvil", 1337, 0.01 ether); + } else if (chainId == 31337) { + return NetworkConfig("Local Anvil", 31337, 0.01 ether); + } else { + return NetworkConfig( + string(abi.encodePacked("Unknown Network (", vm.toString(chainId), ")")), chainId, 0.01 ether + ); + } + } + + function logDeploymentResults(NetworkConfig memory networkConfig) internal view { + console.log("=== DEPLOYMENT COMPLETE (CREATE2) ==="); + console.log(""); + console.log("Contract:", contractName); + console.log("Address:", deployedAddress); + console.log(""); + console.log("Network:", networkConfig.name); + console.log("Chain ID:", networkConfig.chainId); + console.log("Deployment Date:", block.timestamp); + console.log("Create2FactoryV1:", address(factory)); + console.log(""); + console.log("IMPORTANT: This address is deterministic across all chains!"); + console.log("The same contract name will always yield the same address."); + console.log(""); + } + + function saveDeploymentAddress(NetworkConfig memory networkConfig) internal { + string memory environment = ""; + try vm.envString("DEPLOYMENT_ENV") returns (string memory env) { + environment = env; + } catch {} + + DeploymentHelper.ContractDeployment[] memory deployments = new DeploymentHelper.ContractDeployment[](1); + + string memory metadata; + if (entryPointAddress != address(0) && initialBundler != address(0)) { + metadata = string( + abi.encodePacked( + '{"initialBundler": "', + vm.toString(initialBundler), + '", "entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2"}' + ) + ); + } else if (entryPointAddress != address(0)) { + metadata = string( + abi.encodePacked( + '{"entryPoint": "', vm.toString(entryPointAddress), '", "deploymentMethod": "CREATE2"}' + ) + ); + } else { + metadata = '{"deploymentMethod": "CREATE2"}'; + } + + deployments[0] = DeploymentHelper.createContractDeployment(vm, contractName, deployedAddress, metadata); + + DeploymentHelper.saveDeploymentArtifacts( + vm, "deployments", environment, networkConfig.name, networkConfig.chainId, deployments + ); + } +} From 41ecb12a9c00374ddf01ea158bdc3f2d80adb5a2 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 5 Nov 2025 20:44:25 +0100 Subject: [PATCH 12/14] Update DEPLOYMENT.md for simplified salt generation and new DeployContract script --- .../omni-executor/aa-contracts/DEPLOYMENT.md | 87 ++++++++++++++----- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md b/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md index 85bf7976ee..62f8064f1a 100644 --- a/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md +++ b/tee-worker/omni-executor/aa-contracts/DEPLOYMENT.md @@ -136,14 +136,14 @@ This allows **identical addresses across all chains** when using the same salt a ✅ **Deterministic Addresses**: Same contract address on all chains ✅ **Order Independent**: Deploy contracts in any order ✅ **Predictable**: Know contract addresses before deployment -✅ **Frontrun Protected**: Using sender-specific salts prevents address squatting -✅ **Multi-Chain Ready**: Deploy to new networks without nonce coordination +✅ **Truly Universal**: Same addresses for all deployers +✅ **Multi-Chain Ready**: Deploy to new networks without any coordination ### CREATE2 Deployment Strategy #### Step 1: Deploy the CREATE2 Factory -The `Create2Factory` contract must be deployed **once per network** using a **fresh EOA** (recommended for consistency, though not strictly required). +The `Create2FactoryV1` contract must be deployed **once per network** using a **fresh EOA** (recommended for consistency, though not strictly required). ```bash # Set up environment @@ -175,14 +175,15 @@ After deployment, add the factory address to `deployments/create2-factories.json #### Step 2: Deploy AA Contracts via CREATE2 -Once the factory is deployed, use it to deploy AA contracts: +Once the factory is deployed, you can deploy AA contracts using two methods: + +**Option A: Deploy All Contracts at Once** ```bash -# Set factory address and version +# Set factory address export CREATE2_FACTORY_ADDRESS=0x... # From step 1 -export CONTRACT_VERSION=v1.0.0 # Version for salt generation -# Deploy AA contracts via CREATE2 +# Deploy all AA contracts via CREATE2 forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ @@ -190,22 +191,42 @@ forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ -vvv ``` +**Option B: Deploy Individual Contract** + +```bash +# Set factory address +export CREATE2_FACTORY_ADDRESS=0x... # From step 1 + +# Deploy a single contract +CONTRACT_NAME=EntryPointV1 forge script script/DeployContract.s.sol:DeployContract \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv + +# Deploy factory (requires EntryPoint) +CONTRACT_NAME=OmniAccountFactoryV1 ENTRYPOINT_ADDRESS=0x... \ + forge script script/DeployContract.s.sol:DeployContract \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv +``` + #### Step 3: Deploy to Additional Networks To deploy to a new network with the **same addresses**: 1. Deploy the CREATE2 factory on the new network (step 1) -2. Use the **same deployer EOA** and **same version** from step 2 -3. Contracts will deploy to **identical addresses**! +2. Deploy contracts using either method from step 2 +3. Contracts will deploy to **identical addresses** automatically! ```bash # Example: Deploy to new network export RPC_URL=https://new-network-rpc.example.com export CREATE2_FACTORY_ADDRESS=0x... # Factory on new network -# Same version = same addresses! -export CONTRACT_VERSION=v1.0.0 - +# Addresses will be identical across all chains! forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ @@ -215,12 +236,11 @@ forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ ### CREATE2 Configuration -All standard environment variables from `Deploy.s.sol` are supported, plus: +**DeployWithCreate2.s.sol** - All standard environment variables from `Deploy.s.sol` are supported, plus: ```bash # CREATE2-specific variables -CREATE2_FACTORY_ADDRESS=0x... # Address of deployed Create2Factory (required) -CONTRACT_VERSION=v1.0.0 # Version string for salt generation (default: v1.0.0) +CREATE2_FACTORY_ADDRESS=0x... # Address of deployed Create2FactoryV1 (required) # All standard variables still work DEPLOY_ENTRYPOINT=true @@ -233,18 +253,37 @@ SAVE_DEPLOYMENT_FILE=true DEPLOYMENT_ENV=production ``` +**DeployContract.s.sol** - For individual contract deployment: + +```bash +# Required +CREATE2_FACTORY_ADDRESS=0x... # Address of deployed Create2FactoryV1 +CONTRACT_NAME=EntryPointV1 # Contract to deploy (EntryPointV1, OmniAccountFactoryV1, SimplePaymaster, ERC20PaymasterV1) + +# Required for Factory/Paymaster contracts +ENTRYPOINT_ADDRESS=0x... # Address of deployed EntryPoint + +# Optional for Paymaster contracts +INITIAL_BUNDLER=0x... # Default: deployer address +PAYMASTER_INITIAL_DEPOSIT=1000000000000000000 # Default: 1 ETH + +# Optional +SAVE_DEPLOYMENT_FILE=true # Default: false +DEPLOYMENT_ENV=production # Default: empty +``` + ### Salt Generation Strategy -The `DeployWithCreate2` script uses **sender-protected salts** to prevent frontrunning: +The deployment scripts use **purely deterministic salts** based only on contract name: ```solidity -salt = keccak256(abi.encode(contractName, version, msg.sender)) +salt = keccak256(abi.encode(contractName)) ``` This means: -- **Same deployer EOA** + **same version** = **same addresses** across all chains -- Different deployers will get different addresses (security feature) -- Update `CONTRACT_VERSION` when you want new addresses for updated contracts +- **Same contract name** = **same address** on all chains for all deployers +- Truly universal addresses across all EVM chains +- Anyone can deploy to the predicted address (first deployment wins) ### Address Prediction @@ -261,8 +300,8 @@ You can also compute addresses manually: ```solidity // In Solidity -Create2Factory factory = Create2Factory(factoryAddress); -bytes32 salt = factory.generateSalt("EntryPointV1", "v1.0.0", msg.sender); +Create2FactoryV1 factory = Create2FactoryV1(factoryAddress); +bytes32 salt = factory.generateSalt("EntryPointV1"); address predicted = factory.computeAddress(salt, type(EntryPointV1).creationCode); ``` @@ -299,13 +338,13 @@ optimizer_runs = 1000000 ### Troubleshooting **Problem**: Addresses don't match across chains -**Solution**: Ensure you're using the same deployer EOA and CONTRACT_VERSION +**Solution**: Ensure the Create2FactoryV1 is deployed to the same address on all chains (use same fresh EOA) **Problem**: Factory deployment fails **Solution**: Make sure you have enough ETH for deployment gas **Problem**: "AddressAlreadyDeployed" error -**Solution**: Contract was already deployed. Either use it or change the CONTRACT_VERSION +**Solution**: Contract was already deployed to this deterministic address. Check if it's functioning correctly or use a different contract name **Problem**: Verification fails with "Bytecode does not match" **Solution**: Ensure your local build uses the same compiler settings as deployment From ecf2a47121659b4fd010bf97455556ae692ff921 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 6 Nov 2025 09:08:56 +0100 Subject: [PATCH 13/14] Remove unused deployer parameter from deployment functions --- .../aa-contracts/script/DeployWithCreate2.s.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol index a412459fce..ffcfc369ad 100644 --- a/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol +++ b/tee-worker/omni-executor/aa-contracts/script/DeployWithCreate2.s.sol @@ -137,14 +137,14 @@ contract DeployWithCreate2 is Script { // Deploy contracts in dependency order with confirmation between each if (shouldDeployEntryPoint) { vm.startBroadcast(deployerPrivateKey); - deployEntryPoint(deployer); + deployEntryPoint(); vm.stopBroadcast(); console.log("Waiting for EntryPoint deployment confirmation..."); } if (shouldDeployFactory) { vm.startBroadcast(deployerPrivateKey); - deployAccountFactory(deployer); + deployAccountFactory(); vm.stopBroadcast(); console.log("Waiting for Factory deployment confirmation..."); } @@ -152,7 +152,7 @@ contract DeployWithCreate2 is Script { // Deploy Simple paymaster if configured if (shouldDeploySimplePaymaster) { vm.startBroadcast(deployerPrivateKey); - deployPaymaster(deployer); + deployPaymaster(); vm.stopBroadcast(); console.log("Waiting for SimplePaymaster deployment confirmation..."); @@ -168,7 +168,7 @@ contract DeployWithCreate2 is Script { // Deploy ERC20 paymaster if configured if (shouldDeployERC20Paymaster) { vm.startBroadcast(deployerPrivateKey); - deployERC20Paymaster(deployer); + deployERC20Paymaster(); vm.stopBroadcast(); console.log("Waiting for ERC20Paymaster deployment confirmation..."); @@ -279,7 +279,7 @@ contract DeployWithCreate2 is Script { } } - function deployEntryPoint(address deployer) internal { + function deployEntryPoint() internal { console.log("Deploying EntryPointV1 via CREATE2..."); bytes32 salt = factory.generateSalt("EntryPointV1"); @@ -292,7 +292,7 @@ contract DeployWithCreate2 is Script { console.log(""); } - function deployAccountFactory(address deployer) internal { + function deployAccountFactory() internal { console.log("Deploying OmniAccountFactoryV1 via CREATE2..."); bytes32 salt = factory.generateSalt("OmniAccountFactoryV1"); @@ -307,7 +307,7 @@ contract DeployWithCreate2 is Script { console.log(""); } - function deployPaymaster(address deployer) internal { + function deployPaymaster() internal { console.log("Deploying SimplePaymaster via CREATE2..."); bytes32 salt = factory.generateSalt("SimplePaymaster"); @@ -324,7 +324,7 @@ contract DeployWithCreate2 is Script { console.log(""); } - function deployERC20Paymaster(address deployer) internal { + function deployERC20Paymaster() internal { console.log("Deploying ERC20PaymasterV1 via CREATE2..."); bytes32 salt = factory.generateSalt("ERC20PaymasterV1"); From 8aa35ec43aafdaa8b31bbd1fe578c5a8687d96f8 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 6 Nov 2025 17:03:38 +0100 Subject: [PATCH 14/14] Pin Foundry version to v1.2.3 to fix AA contracts build --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d7642aa0e..9dbccab37c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -605,6 +605,8 @@ jobs: - name: Install foundry uses: foundry-rs/foundry-toolchain@v1 + with: + version: 'v1.2.3' - name: Install solana sdk run: |