diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43abf4cd8e..cad97618d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -343,6 +343,8 @@ jobs: - name: Install foundry uses: foundry-rs/foundry-toolchain@v1 + with: + version: 'v1.2.3' - name: Show Forge version working-directory: ./tee-worker/omni-executor/contracts/aa diff --git a/tee-worker/omni-executor/contracts/aa/DEPLOYMENT.md b/tee-worker/omni-executor/contracts/aa/DEPLOYMENT.md index e77c3924b2..62f8064f1a 100644 --- a/tee-worker/omni-executor/contracts/aa/DEPLOYMENT.md +++ b/tee-worker/omni-executor/contracts/aa/DEPLOYMENT.md @@ -118,6 +118,237 @@ 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 +✅ **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 `Create2FactoryV1` 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, you can deploy AA contracts using two methods: + +**Option A: Deploy All Contracts at Once** + +```bash +# Set factory address +export CREATE2_FACTORY_ADDRESS=0x... # From step 1 + +# Deploy all AA contracts via CREATE2 +forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -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. 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 + +# Addresses will be identical across all chains! +forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ + -vvv +``` + +### CREATE2 Configuration + +**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 Create2FactoryV1 (required) + +# 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 +``` + +**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 deployment scripts use **purely deterministic salts** based only on contract name: + +```solidity +salt = keccak256(abi.encode(contractName)) +``` + +This means: +- **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 + +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 +Create2FactoryV1 factory = Create2FactoryV1(factoryAddress); +bytes32 salt = factory.generateSalt("EntryPointV1"); +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 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 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 + ## 📁 Deployment Artifacts After successful deployment, you'll find: diff --git a/tee-worker/omni-executor/contracts/aa/deployments/create2-factories.json b/tee-worker/omni-executor/contracts/aa/deployments/create2-factories.json new file mode 100644 index 0000000000..8d3475e74e --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/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" + ] +} diff --git a/tee-worker/omni-executor/contracts/aa/foundry.toml b/tee-worker/omni-executor/contracts/aa/foundry.toml index 1207c6938b..e795f37bf7 100644 --- a/tee-worker/omni-executor/contracts/aa/foundry.toml +++ b/tee-worker/omni-executor/contracts/aa/foundry.toml @@ -3,9 +3,13 @@ src = "src" out = "out" libs = ["lib", "../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" }, diff --git a/tee-worker/omni-executor/contracts/aa/script/DeployContract.s.sol b/tee-worker/omni-executor/contracts/aa/script/DeployContract.s.sol new file mode 100644 index 0000000000..28273bda7d --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/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 + ); + } +} diff --git a/tee-worker/omni-executor/contracts/aa/script/DeployCreate2Factory.s.sol b/tee-worker/omni-executor/contracts/aa/script/DeployCreate2Factory.s.sol new file mode 100644 index 0000000000..3743512ecc --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/script/DeployCreate2Factory.s.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../src/core/Create2FactoryV1.sol"; + +/** + * @title DeployCreate2Factory + * @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 + * + * 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("=== Create2FactoryV1 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 Create2FactoryV1..."); + vm.startBroadcast(deployerPrivateKey); + + Create2FactoryV1 factory = new Create2FactoryV1(); + + vm.stopBroadcast(); + + // Verify deployment + require(address(factory) == predictedFactory, "Factory address mismatch!"); + console.log("Create2FactoryV1 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); + } +} diff --git a/tee-worker/omni-executor/contracts/aa/script/DeployWithCreate2.s.sol b/tee-worker/omni-executor/contracts/aa/script/DeployWithCreate2.s.sol new file mode 100644 index 0000000000..ffcfc369ad --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/script/DeployWithCreate2.s.sol @@ -0,0 +1,495 @@ +// 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 DeployWithCreate2 + * @notice Deployment script for AA contracts using CREATE2 for deterministic addresses + * @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. Create2FactoryV1 must be deployed on the target network + * 2. Factory address must be provided via CREATE2_FACTORY_ADDRESS environment variable + * + * Usage: + * forge script script/DeployWithCreate2.s.sol:DeployWithCreate2 --rpc-url --broadcast --verify + * + * Environment Variables: + * 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 + Create2FactoryV1 public factory; + + // 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 = Create2FactoryV1(factoryAddr); + + // 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("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 all addresses before deployment + console.log("=== Predicted Addresses ==="); + if (shouldDeployEntryPoint) { + 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"); + 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"); + 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"); + 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(); + vm.stopBroadcast(); + console.log("Waiting for EntryPoint deployment confirmation..."); + } + + if (shouldDeployFactory) { + vm.startBroadcast(deployerPrivateKey); + deployAccountFactory(); + vm.stopBroadcast(); + console.log("Waiting for Factory deployment confirmation..."); + } + + // Deploy Simple paymaster if configured + if (shouldDeploySimplePaymaster) { + vm.startBroadcast(deployerPrivateKey); + deployPaymaster(); + 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(); + 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() internal { + console.log("Deploying EntryPointV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("EntryPointV1"); + 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() internal { + console.log("Deploying OmniAccountFactoryV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("OmniAccountFactoryV1"); + 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() internal { + console.log("Deploying SimplePaymaster via CREATE2..."); + + bytes32 salt = factory.generateSalt("SimplePaymaster"); + 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() internal { + console.log("Deploying ERC20PaymasterV1 via CREATE2..."); + + bytes32 salt = factory.generateSalt("ERC20PaymasterV1"); + 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("Create2FactoryV1:", address(factory)); + console.log(""); + console.log("IMPORTANT: These addresses are deterministic across all chains!"); + console.log("The same contract names will always 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, '{"deploymentMethod": "CREATE2"}' + ); + currentIndex++; + } + + if (shouldDeployFactory) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "OmniAccountFactoryV1", + factoryAddress, + string( + abi.encodePacked( + '{"entryPoint": "', vm.toString(entryPointAddress), '", "deploymentMethod": "CREATE2"}' + ) + ) + ); + currentIndex++; + } + + if (shouldDeploySimplePaymaster) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "SimplePaymaster", + paymasterAddress, + string( + abi.encodePacked( + '{"initialBundler": "', + vm.toString(initialBundler), + '", "entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2"}' + ) + ) + ); + currentIndex++; + } + + if (shouldDeployERC20Paymaster) { + deployments[currentIndex] = DeploymentHelper.createContractDeployment( + vm, + "ERC20PaymasterV1", + erc20PaymasterAddress, + string( + abi.encodePacked( + '{"initialBundler": "', + vm.toString(initialBundler), + '", "entryPoint": "', + vm.toString(entryPointAddress), + '", "deploymentMethod": "CREATE2"}' + ) + ) + ); + } + + DeploymentHelper.saveDeploymentArtifacts( + vm, "deployments", environment, networkConfig.name, networkConfig.chainId, deployments + ); + } +} diff --git a/tee-worker/omni-executor/contracts/aa/src/core/Create2FactoryV1.sol b/tee-worker/omni-executor/contracts/aa/src/core/Create2FactoryV1.sol new file mode 100644 index 0000000000..3c3cd8150e --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/src/core/Create2FactoryV1.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +/** + * @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 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 + /// @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 deterministic salt for deployment + * @param contractName The name/identifier of the contract + * @return salt The generated salt + * @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) external pure returns (bytes32 salt) { + salt = keccak256(abi.encode(contractName)); + } +} diff --git a/tee-worker/omni-executor/contracts/aa/test/Create2Factory.t.sol b/tee-worker/omni-executor/contracts/aa/test/Create2Factory.t.sol new file mode 100644 index 0000000000..624107d3f0 --- /dev/null +++ b/tee-worker/omni-executor/contracts/aa/test/Create2Factory.t.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.28; + +import {Test, console} from "forge-std/Test.sol"; +import {Create2FactoryV1} from "../src/core/Create2FactoryV1.sol"; +import {Counter} from "../src/Counter.sol"; + +contract Create2FactoryTest is Test { + Create2FactoryV1 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 Create2FactoryV1(); + } + + // ============ 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( + Create2FactoryV1.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(Create2FactoryV1.AddressAlreadyDeployed.selector, deployed)); + factory.deploy(salt, bytecode); + } + + // ============ Salt Generation Tests ============ + + function test_GenerateSalt() public view { + string memory contractName = "TestContract"; + + bytes32 salt = factory.generateSalt(contractName); + + // Salt should be non-zero + assertTrue(salt != bytes32(0)); + + // Salt should be deterministic + bytes32 salt2 = factory.generateSalt(contractName); + assertEq(salt, salt2); + + // 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"); + bytes32 salt2 = factory.generateSalt("Contract2"); + + // Different names should produce different salts + assertTrue(salt1 != salt2); + } + + // ============ 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_DeterministicDeployment() public { + // Scenario: Deploy same contract to same address regardless of deployer + // using deterministic salt based only on contract name + + string memory contractName = "EntryPointV1"; + + // Generate deterministic salt + bytes32 salt = factory.generateSalt(contractName); + + // Predict address + bytes memory bytecode = type(Counter).creationCode; + address predicted = factory.computeAddress(salt, bytecode); + + // Deploy (can be from any deployer) + vm.prank(deployer1); + address deployed = factory.deploy(salt, bytecode); + + // Verify + assertEq(deployed, predicted); + assertTrue(factory.isDeployed(deployed)); + + // Verify same salt is generated regardless of who calls it + bytes32 salt2 = factory.generateSalt(contractName); + assertEq(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"; + bytes memory bytecode = type(Counter).creationCode; + + // "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); + + assertEq(deployed1, predicted1); + + // In a real multi-chain scenario, deploying with the same salt and bytecode + // 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 + } + + // ============ 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) public view { + bytes32 salt = factory.generateSalt(name); + + // Salt should be deterministic + bytes32 salt2 = factory.generateSalt(name); + assertEq(salt, salt2); + + // Salt should match manual calculation + assertEq(salt, keccak256(abi.encode(name))); + } + + // ============ 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); + } +}