Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Interfaces/IUEAFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
// =========================
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
10 changes: 10 additions & 0 deletions src/testnetV0/UEAFactoryV0.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
37 changes: 32 additions & 5 deletions src/uea/UEAFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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
// =========================
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 1 addition & 6 deletions test/fork/ForkUniversalCore.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
15 changes: 3 additions & 12 deletions test/tests_cea/CEA.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion test/tests_ueaMigration/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
126 changes: 124 additions & 2 deletions test/tests_uea_and_factory/UEAFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
3 changes: 2 additions & 1 deletion test/tests_uea_and_factory/UEAProxyCalls.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
5 changes: 2 additions & 3 deletions test/tests_uea_and_factory/UEA_SVM.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down