diff --git a/src/Interfaces/IUEAFactory.sol b/src/Interfaces/IUEAFactory.sol index e04c383..a5d0f93 100644 --- a/src/Interfaces/IUEAFactory.sol +++ b/src/Interfaces/IUEAFactory.sol @@ -3,6 +3,12 @@ pragma solidity 0.8.26; import {UniversalAccountId} from "../libraries/Types.sol"; +/// @notice Configuration for predicting CEA addresses on an external chain. +struct CEAConfig { + address ceaFactory; + address ceaProxyImplementation; +} + /// @title IUEAFactory /// @notice Interface for the Universal Executor Account Factory. /// @dev Deploys deterministic UEA instances via CREATE2 and maintains @@ -36,6 +42,12 @@ interface IUEAFactory { /// @param newUEA New UEA implementation address event UEAImplementationUpdated(bytes32 indexed vmHash, address previousUEA, address newUEA); + /// @notice Emitted when CEA config is set for an external chain. + /// @param chainHash Hash of the chain identifier + /// @param ceaFactory CEAFactory address on the external chain + /// @param ceaProxyImpl CEAProxy implementation address on the external chain + event CEAConfigSet(bytes32 indexed chainHash, address ceaFactory, address ceaProxyImpl); + // ========================= // UF_1: VIEW FUNCTIONS // ========================= @@ -70,6 +82,12 @@ interface IUEAFactory { /// @return Predicted UEA proxy address function computeUEA(UniversalAccountId memory id) external view returns (address); + /// @notice Returns the predicted CEA address on an external chain. + /// @param chainHash Hash of the external chain (keccak256(abi.encode(namespace, chainId))) + /// @param pushAccount UEA or EOA address on Push Chain + /// @return cea Predicted CEA address on the external chain + function getCEAForPushAccount(bytes32 chainHash, address pushAccount) external view returns (address cea); + /// @notice Returns the current UEA migration contract address. /// @return Migration contract address function UEA_MIGRATION_CONTRACT() external view returns (address); @@ -106,4 +124,10 @@ interface IUEAFactory { /// @param vmHash VM type hash /// @param uea UEA implementation address function registerUEA(bytes32 chainHash, bytes32 vmHash, address uea) external; + + /// @notice Sets CEA config for an external chain. + /// @param chainHash Hash of the chain identifier + /// @param ceaFactory CEAFactory address on the external chain + /// @param ceaProxyImplementation CEAProxy implementation on the external chain + function setCEAConfig(bytes32 chainHash, address ceaFactory, address ceaProxyImplementation) external; } diff --git a/src/testnetV0/UEAFactoryV0.sol b/src/testnetV0/UEAFactoryV0.sol index d6d2701..05de430 100644 --- a/src/testnetV0/UEAFactoryV0.sol +++ b/src/testnetV0/UEAFactoryV0.sol @@ -315,4 +315,14 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, function generateSalt(UniversalAccountId memory _id) public pure returns (bytes32) { return keccak256(abi.encode(_id)); } + + /// @dev V0 stub — not implemented in testnet version. + function getCEAForPushAccount(bytes32, address) external pure returns (address) { + revert UEAErrors.InvalidInputArgs(); + } + + /// @dev V0 stub — not implemented in testnet version. + function setCEAConfig(bytes32, address, address) external pure { + revert UEAErrors.InvalidInputArgs(); + } } diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index 341fb70..65a067b 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -2,15 +2,16 @@ pragma solidity 0.8.26; import {IUEA} from "../interfaces/IUEA.sol"; -import {IUEAFactory} from "../interfaces/IUEAFactory.sol"; +import {IUEAFactory, CEAConfig} from "../interfaces/IUEAFactory.sol"; import {UEAErrors} from "../libraries/Errors.sol"; import {UniversalAccountId} from "../libraries/Types.sol"; import {UEAProxy} from "./UEAProxy.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {AccessControlDefaultAdminRulesUpgradeable} from - "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import { + AccessControlDefaultAdminRulesUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; /** @@ -61,6 +62,9 @@ contract UEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, /// @notice Push Chain numeric identifier used in the `getOriginForUEA` synthetic fallback. string public pushChainId; + /// @notice Maps chain identifier hashes to their CEA deployment config on external chains. + mapping(bytes32 => CEAConfig) public CEA_CONFIG; + // ========================= // UF: CONSTRUCTOR // ========================= @@ -141,6 +145,16 @@ contract UEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, return UEA_PROXY_IMPLEMENTATION.predictDeterministicAddress(salt, address(this)); } + /// @inheritdoc IUEAFactory + function getCEAForPushAccount(bytes32 chainHash, address pushAccount) external view returns (address cea) { + CEAConfig memory config = CEA_CONFIG[chainHash]; + if (config.ceaFactory == address(0) || config.ceaProxyImplementation == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + bytes32 salt = keccak256(abi.encode(pushAccount)); + cea = config.ceaProxyImplementation.predictDeterministicAddress(salt, config.ceaFactory); + } + /// @inheritdoc IUEAFactory /// @dev When `isUEA` is false, `account` is a synthetic fallback built from /// `"eip155"` + `pushChainId` + `addr` — NOT a registered origin. @@ -151,8 +165,9 @@ contract UEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, if (account.owner.length > 0) { isUEA = true; } else { - account = - UniversalAccountId({chainNamespace: "eip155", chainId: pushChainId, owner: bytes(abi.encodePacked(addr))}); + account = UniversalAccountId({ + chainNamespace: "eip155", chainId: pushChainId, owner: bytes(abi.encodePacked(addr)) + }); } return (account, isUEA); @@ -249,6 +264,18 @@ contract UEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, pushChainId = _pushChainId; } + /// @inheritdoc IUEAFactory + function setCEAConfig(bytes32 chainHash, address ceaFactory, address ceaProxyImplementation) + external + onlyRole(UEA_ADMIN_ROLE) + { + if (ceaFactory == address(0) || ceaProxyImplementation == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + CEA_CONFIG[chainHash] = CEAConfig(ceaFactory, ceaProxyImplementation); + emit CEAConfigSet(chainHash, ceaFactory, ceaProxyImplementation); + } + /// @inheritdoc IUEAFactory function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyRole(UEA_ADMIN_ROLE) { (, bool isRegistered) = getVMType(_chainHash); diff --git a/test/fork/ForkUniversalCore.t.sol b/test/fork/ForkUniversalCore.t.sol index 5339598..adc8a46 100644 --- a/test/fork/ForkUniversalCore.t.sol +++ b/test/fork/ForkUniversalCore.t.sol @@ -62,12 +62,7 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr // Deploy UniversalCore behind proxy UniversalCore implementation = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( - UniversalCore.initialize.selector, - deployer, - makeAddr("pauser"), - WPC_TOKEN, - UNISWAP_FACTORY, - UNISWAP_ROUTER + UniversalCore.initialize.selector, deployer, makeAddr("pauser"), WPC_TOKEN, UNISWAP_FACTORY, UNISWAP_ROUTER ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); universalCore = UniversalCore(payable(proxyAddress)); diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index a979fe6..a328182 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -918,11 +918,7 @@ contract CEATest is Test { // Create multicall with wrong selector (try to call initializeCEA) Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( - address(ceaInstance), - 0, - abi.encodeWithSignature( - "initializeCEA(address,address)", address(0), address(0) - ) + address(ceaInstance), 0, abi.encodeWithSignature("initializeCEA(address,address)", address(0), address(0)) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1346,11 +1342,7 @@ contract CEATest is Test { // Create multicall with wrong selector (try to call initializeCEA) Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( - address(ceaInstance), - 0, - abi.encodeWithSignature( - "initializeCEA(address,address)", address(0), address(0) - ) + address(ceaInstance), 0, abi.encodeWithSignature("initializeCEA(address,address)", address(0), address(0)) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1730,8 +1722,7 @@ contract CEATest is Test { function testInitializeCEA_CannotBeCalledAgainAfterProxyDeployment() public deployCEA { vm.expectRevert(Errors.AlreadyInitialized.selector); - CEA(payable(address(ceaInstance))) - .initializeCEA(ueaOnPush, address(factory)); + CEA(payable(address(ceaInstance))).initializeCEA(ueaOnPush, address(factory)); } function testReceive_DirectETHTransferSucceeds() public deployCEA { diff --git a/test/tests_ueaMigration/BaseTest.t.sol b/test/tests_ueaMigration/BaseTest.t.sol index 908166d..298dd02 100644 --- a/test/tests_ueaMigration/BaseTest.t.sol +++ b/test/tests_ueaMigration/BaseTest.t.sol @@ -174,7 +174,8 @@ contract BaseTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser"), "42101"); + bytes memory initData = + abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser"), "42101"); ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(factoryProxy)); diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index fd326dc..fbcb518 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -13,11 +13,14 @@ import {UEAErrors as Errors} from "../../src/libraries/Errors.sol"; import {IUEA} from "../../src/interfaces/IUEA.sol"; import {IUEAFactory} from "../../src/Interfaces/IUEAFactory.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; -import {IAccessControlDefaultAdminRules} from - "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UEAProxy} from "../../src/uea/UEAProxy.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {CEAConfig} from "../../src/interfaces/IUEAFactory.sol"; contract UEAFactoryTest is Test { UEAFactory factory; @@ -1324,4 +1327,123 @@ contract UEAFactoryTest is Test { vm.prank(pauser); factory.unpause(); } + + // ============================================ + // CEA CONFIG & getCEAForPushAccount TESTS + // ============================================ + + function testSetCEAConfig_HappyPath() public { + address mockCeaFactory = makeAddr("ceaFactory"); + address mockCeaProxyImpl = makeAddr("ceaProxyImpl"); + + factory.setCEAConfig(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + + (address storedFactory, address storedImpl) = factory.CEA_CONFIG(ethereumChainHash); + assertEq(storedFactory, mockCeaFactory); + assertEq(storedImpl, mockCeaProxyImpl); + } + + function testSetCEAConfig_EmitsEvent() public { + address mockCeaFactory = makeAddr("ceaFactory"); + address mockCeaProxyImpl = makeAddr("ceaProxyImpl"); + + vm.expectEmit(true, false, false, true, address(factory)); + emit IUEAFactory.CEAConfigSet(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + + factory.setCEAConfig(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + } + + function testSetCEAConfig_RevertsOnZeroCeaFactory() public { + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.setCEAConfig(ethereumChainHash, address(0), makeAddr("ceaProxyImpl")); + } + + function testSetCEAConfig_RevertsOnZeroCeaProxyImpl() public { + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.setCEAConfig(ethereumChainHash, makeAddr("ceaFactory"), address(0)); + } + + function testSetCEAConfig_OnlyUEAAdmin() public { + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) + ); + vm.prank(nonOwner); + factory.setCEAConfig(ethereumChainHash, makeAddr("ceaFactory"), makeAddr("ceaProxyImpl")); + } + + function testSetCEAConfig_CanOverwrite() public { + address factory1 = makeAddr("ceaFactory1"); + address impl1 = makeAddr("ceaProxyImpl1"); + address factory2 = makeAddr("ceaFactory2"); + address impl2 = makeAddr("ceaProxyImpl2"); + + factory.setCEAConfig(ethereumChainHash, factory1, impl1); + factory.setCEAConfig(ethereumChainHash, factory2, impl2); + + (address storedFactory, address storedImpl) = factory.CEA_CONFIG(ethereumChainHash); + assertEq(storedFactory, factory2); + assertEq(storedImpl, impl2); + } + + function testGetCEAForPushAccount_HappyPath() public { + address mockCeaFactory = makeAddr("ceaFactory"); + address mockCeaProxyImpl = makeAddr("ceaProxyImpl"); + factory.setCEAConfig(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + + address pushAccount = makeAddr("pushAccount"); + address cea = factory.getCEAForPushAccount(ethereumChainHash, pushAccount); + + bytes32 salt = keccak256(abi.encode(pushAccount)); + address expected = Clones.predictDeterministicAddress(mockCeaProxyImpl, salt, mockCeaFactory); + assertEq(cea, expected); + } + + function testGetCEAForPushAccount_RevertsWhenConfigNotSet() public { + bytes32 unregisteredChain = keccak256(abi.encode("eip155", "999")); + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.getCEAForPushAccount(unregisteredChain, makeAddr("pushAccount")); + } + + function testGetCEAForPushAccount_DifferentAccountsDifferentAddresses() public { + address mockCeaFactory = makeAddr("ceaFactory"); + address mockCeaProxyImpl = makeAddr("ceaProxyImpl"); + factory.setCEAConfig(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + + address cea1 = factory.getCEAForPushAccount(ethereumChainHash, makeAddr("account1")); + address cea2 = factory.getCEAForPushAccount(ethereumChainHash, makeAddr("account2")); + assertTrue(cea1 != cea2); + } + + function testGetCEAForPushAccount_MatchesCEAFactoryFormula() public { + address mockCeaFactory = makeAddr("ceaFactory"); + address mockCeaProxyImpl = makeAddr("ceaProxyImpl"); + factory.setCEAConfig(ethereumChainHash, mockCeaFactory, mockCeaProxyImpl); + + address pushAccount = makeAddr("pushAccount"); + address cea = factory.getCEAForPushAccount(ethereumChainHash, pushAccount); + + bytes32 salt = keccak256(abi.encode(pushAccount)); + address manual = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + mockCeaFactory, + salt, + keccak256( + abi.encodePacked( + hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", + mockCeaProxyImpl, + hex"5af43d82803e903d91602b57fd5bf3" + ) + ) + ) + ) + ) + ) + ); + assertEq(cea, manual); + } } diff --git a/test/tests_uea_and_factory/UEAProxyCalls.t.sol b/test/tests_uea_and_factory/UEAProxyCalls.t.sol index de50541..bb59656 100644 --- a/test/tests_uea_and_factory/UEAProxyCalls.t.sol +++ b/test/tests_uea_and_factory/UEAProxyCalls.t.sol @@ -52,7 +52,8 @@ contract ProxyCallTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser"), "42101"); + bytes memory initData = + abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index 83d8daf..c723623 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -958,9 +958,8 @@ contract UEASVMTest is Test { // This test verifies that the DOMAIN_SEPARATOR_TYPEHASH_SVM constant matches the expected hash // If the EIP712Domain_SVM struct definition changes, this test will fail - bytes32 expectedHash = keccak256( - "EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)" - ); + bytes32 expectedHash = + keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)"); // Access the constant from the deployed instance bytes32 actualHash = svmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH_SVM();