diff --git a/.gitignore b/.gitignore index a93954a..a1913cf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ out/ .claude/ docs-internal/ +# Deployments +deployments + # Ignores development broadcast logs !/broadcast /broadcast/*/31337/ @@ -19,3 +22,4 @@ docs-internal/ # Safe to ignore .wake/ .vscode/ +foundry.lock diff --git a/CLAUDE.md b/CLAUDE.md index 800452c..4dbf309 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,6 +93,7 @@ forge fmt --check - Implementation: `CEA` (src/CEA/CEA.sol) - Deployment: Deterministic via `CEAFactory` using CREATE2 (src/CEA/CEAFactory.sol:32) - Identity: One UEA → one CEA per external chain +- Gateway integration: Routes CEA→UEA transfers through `IUniversalGateway.sendUniversalTxViaCEA()` for FUNDS and FUNDS_AND_PAYLOAD tx types ### Proxy Pattern Architecture @@ -122,10 +123,11 @@ Both UEA and CEA use minimal proxy (EIP-1167 clone) architecture: **CEA Migration:** - Mirrors UEA migration pattern for safe proxy upgrades (v1 → v2) - `CEAMigration` (src/CEA/CEAMigration.sol) - Slot-writer migration singleton -- CEA detects `MIGRATION_SELECTOR` and routes to `_handleMigration()` +- CEA detects `MIGRATION_SELECTOR` at top-level payload (three-way branch in `_handleExecution`, aligned with UEA_EVM pattern) +- Migration payload format: `MIGRATION_SELECTOR` directly (no Multicall wrapper) - Factory tracks migration contract via `CEA_MIGRATION_CONTRACT` state variable - Migration executed via delegatecall (preserves all state and funds) -- Safety constraints: self-targeted, zero-value, standalone execution only +- Explicit prevention: `MIGRATION_SELECTOR` inside a Multicall array reverts with `InvalidCall` - CEA stores factory reference to fetch migration contract at runtime ### Token Primitives @@ -186,8 +188,16 @@ struct UniversalPayload { ``` **Special Selectors:** -- `MULTICALL_SELECTOR = bytes4(keccak256("UEA_MULTICALL"))` - Batch multiple calls -- `MIGRATION_SELECTOR = bytes4(keccak256("UEA_MIGRATION"))` - Trigger migration +- `MULTICALL_SELECTOR = bytes4(keccak256("UEA_MULTICALL"))` - Batch multiple calls (0xc25b8d90) +- `MIGRATION_SELECTOR = bytes4(keccak256("UEA_MIGRATION"))` - Trigger migration (0xb0c47dc5) + +**UEA_EVM Execution Routing (`UEA_EVM._handleExecution`):** +- Three-way dispatch based on `payload.data`: + 1. `isMulticall(payload.data)` → `_handleMulticall(payload)` — batch execution + 2. `isMigration(payload.data)` → `_handleMigration(payload)` — delegatecall to factory's migration contract + 3. else → `_handleSingleCall(payload)` — single target call +- `UE_MODULE` (0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7) can execute without signature verification +- Factory reference stored for fetching migration contract at runtime ### CEA Execution Flow @@ -196,6 +206,7 @@ struct UniversalPayload { **Payload Format:** - New format: `MULTICALL_SELECTOR + abi.encode(Multicall[])` - Old format: Direct `abi.encode(Multicall[])` (backwards compatible) +- Migration format: `MIGRATION_SELECTOR` at top level (no Multicall wrapper) **Multicall Structure (src/libraries/Types.sol:31):** ```solidity @@ -207,23 +218,26 @@ struct Multicall { ``` **Execution Routing (`CEA._handleExecution`):** -1. Check if payload starts with `MULTICALL_SELECTOR` -2. If yes, decode as `Multicall[]` and route based on content: - - Single-element with `MIGRATION_SELECTOR` → `_handleMigration()` - - Otherwise → `_handleMulticall()` -3. If no, route to `_handleSingleCall()` (backwards compatibility) +1. Check if payload starts with `MULTICALL_SELECTOR` → decode as `Multicall[]`, route to `_handleMulticall()` +2. Check if payload starts with `MIGRATION_SELECTOR` → route to `_handleMigration()` (no params, top-level) +3. Otherwise → route to `_handleSingleCall()` (backwards compatibility) **Self-Call Pattern:** -- `sendUniversalTxToUEA(token, amount)` - Transfer funds from CEA to UEA +- `sendUniversalTxToUEA(token, amount, payload)` - Transfer funds/payload from CEA to UEA via gateway - Only callable via self-call (`msg.sender == address(this)`) - Must be included in multicall array for execution -- Always sends empty payload/signature (funds-only transfer) +- Supports two tx types: + - **FUNDS**: amount > 0, payload empty + - **FUNDS_AND_PAYLOAD**: amount > 0, payload non-empty +- Both tx types route through `IUniversalGateway.sendUniversalTxViaCEA()` - SDK must include ERC20 approval steps before this call **Safety Constraints:** -- Self-calls must have `value == 0` -- Migration must be standalone (cannot be batched) +- Self-calls must have `value == 0` (enforced in `_handleMulticall`) +- Migration must be standalone (cannot appear inside multicall arrays — `isMigrationCall` check) +- Migration rejects `msg.value > 0` (logic upgrade, not value transfer) - All calls executed sequentially via `.call()` +- No strict `msg.value == totalValue` enforcement (CEA can spend pre-existing balance) ## Key Contracts Reference @@ -252,6 +266,7 @@ struct Multicall { - `ICEA`: src/Interfaces/ICEA.sol - `ICEAFactory`: src/Interfaces/ICEAFactory.sol - `IUniversalCore`: src/Interfaces/IUniversalCore.sol +- `IUniversalGateway`: src/Interfaces/IUniversalGateway.sol - UniversalTxRequest struct, sendUniversalTx/sendUniversalTxViaCEA - `IPRC20`: src/Interfaces/IPRC20.sol ## Test Structure @@ -260,10 +275,11 @@ Tests are organized by component: - `test/tests_uea_and_factory/` - UEA and factory tests - `test/tests_cea/` - CEA and factory tests - `CEA.t.sol` - Core CEA tests (82 tests, canonical helpers) - - `CEA_multicalls.t.sol` - Multicall execution tests (130 tests) - - `CEA_selfCalls.t.sol` - Self-call pattern tests (93 tests) - - Total: 418 CEA tests -- `test/tests_ceaMigration/` - CEA migration tests (42 tests) + - `CEA_multicalls.t.sol` - Multicall execution tests (132 tests) + - `CEA_selfCalls.t.sol` - Self-call pattern tests (120 tests) + - `CEAFactory.t.sol` - Factory tests (116 tests) + - Total: 450 CEA tests +- `test/tests_ceaMigration/` - CEA migration tests (43 tests) - `CEAMigration.t.sol` - Unit tests for migration contract - `CEAFactory_Migration.t.sol` - Factory migration management - `CEA_Migration.t.sol` - CEA migration logic tests @@ -279,8 +295,11 @@ Test files follow the pattern `ContractName.t.sol` and use Foundry's testing fra **Key Test Helpers (CEA.t.sol):** - `makeCall(to, value, data)` - Create Multicall struct - `encodeCalls(Multicall[])` - Encode with MULTICALL_SELECTOR -- `buildWithdrawPayload(token, amount)` - Build sendUniversalTxToUEA payload +- `buildSendToUEAPayload(token, amount)` - Build sendUniversalTxToUEA payload (funds-only, empty payload) +- `buildSendToUEAPayloadWithData(token, amount, payload)` - Build sendUniversalTxToUEA with UEA payload (FUNDS_AND_PAYLOAD) - `buildExternalSingleCall(to, value, data)` - Single external call +- `buildSelfSendToUEACall(token, amount)` - Self-call Multicall with value=0 +- `buildSendToUEAMulticallPayload(token, amount, approveGateway)` - Full multicall with optional ERC20 approval ## Important Constants diff --git a/foundry.toml b/foundry.toml index 4551477..0e0f9d6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,4 +13,7 @@ solc = "0.8.26" optimizer = true optimizer_runs = 99999 auto_detect_solc = false -evm_version = "shanghai" \ No newline at end of file +evm_version = "shanghai" +via_ir = true + +fs_permissions = [{ access = "read-write", path = "deployments/" }] \ No newline at end of file diff --git a/scripts/cea/deployCEAFactory.s.sol b/scripts/cea/deployCEAFactory.s.sol index e5f6d1e..cab19e8 100644 --- a/scripts/cea/deployCEAFactory.s.sol +++ b/scripts/cea/deployCEAFactory.s.sol @@ -26,7 +26,7 @@ import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.s */ contract DeployCEAFactoryScript is Script { // ============================================================================ - // DEPLOYMENT PARAMETERS - Pre-Deployement Checklist + // DEPLOYMENT PARAMETERS - Pre-Deployement Checklist // ============================================================================ // Owner of the CEAFactory (can update implementations, pause, etc.) @@ -36,13 +36,13 @@ contract DeployCEAFactoryScript is Script { address public VAULT_ADDRESS = 0x0000000000000000000000000000000000000001; // UniversalGateway contract address on this chain (handles cross-chain messages) - address public UNIVERSAL_GATEWAY_ADDRESS = 0x0000000000000000000000000000000000000002; - + address public UNIVERSAL_GATEWAY_ADDRESS = 0x4DCab975cDe839632db6695e2e936A29ce3e325E; function run() external { // Get chain ID and deployer info uint256 chainId = block.chainid; - address deployer = vm.addr(vm.envUint("KEY")); + uint256 deployerKey = vm.envUint("KEY"); + address deployer = vm.addr(deployerKey); console.log("=== CEAFactory Deployment Configuration ==="); console.log("Chain ID:", chainId); @@ -63,7 +63,7 @@ contract DeployCEAFactoryScript is Script { console.log("Universal Gateway:", universalGateway); console.log(""); - vm.startBroadcast(); + vm.startBroadcast(deployerKey); // 1. Deploy CEA implementation (logic contract) CEA ceaImplementation = new CEA(); @@ -84,19 +84,16 @@ contract DeployCEAFactoryScript is Script { // 5. Prepare initialization data for CEAFactory bytes memory initData = abi.encodeWithSelector( CEAFactory.initialize.selector, - owner, // initialOwner - vault, // initialVault - address(ceaProxyImplementation), // ceaProxyImplementation - address(ceaImplementation), // ceaImplementation - universalGateway // universalGateway + owner, // initialOwner + vault, // initialVault + address(ceaProxyImplementation), // ceaProxyImplementation + address(ceaImplementation), // ceaImplementation + universalGateway // universalGateway ); // 6. Deploy TransparentUpgradeableProxy wrapping CEAFactory - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - address(ceaFactoryImplementation), - address(proxyAdmin), - initData - ); + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(address(ceaFactoryImplementation), address(proxyAdmin), initData); console.log("[5/7] CEAFactory Proxy (CANONICAL):", address(proxy)); // 7. Wrap proxy in CEAFactory interface for verification @@ -124,7 +121,11 @@ contract DeployCEAFactoryScript is Script { console.log("Factory Owner:", verifiedOwner, verifiedOwner == owner ? "[OK]" : "[MISMATCH]"); console.log("Vault:", verifiedVault, verifiedVault == vault ? "[OK]" : "[MISMATCH]"); - console.log("CEA Proxy Impl:", verifiedCEAProxy, verifiedCEAProxy == address(ceaProxyImplementation) ? "[OK]" : "[MISMATCH]"); + console.log( + "CEA Proxy Impl:", + verifiedCEAProxy, + verifiedCEAProxy == address(ceaProxyImplementation) ? "[OK]" : "[MISMATCH]" + ); console.log("CEA Impl:", verifiedCEA, verifiedCEA == address(ceaImplementation) ? "[OK]" : "[MISMATCH]"); console.log("Gateway:", verifiedGateway, verifiedGateway == universalGateway ? "[OK]" : "[MISMATCH]"); console.log("ProxyAdmin:", verifiedProxyAdmin); @@ -137,20 +138,42 @@ contract DeployCEAFactoryScript is Script { // 10. Generate JSON output for deployment tracking console.log("\n=== Deployment Addresses (JSON) ==="); - string memory json = string(abi.encodePacked( - '{\n', - ' "chainId": ', vm.toString(chainId), ',\n', - ' "deployer": "', vm.toString(deployer), '",\n', - ' "ceaImplementation": "', vm.toString(address(ceaImplementation)), '",\n', - ' "ceaProxyImplementation": "', vm.toString(address(ceaProxyImplementation)), '",\n', - ' "ceaFactoryImplementation": "', vm.toString(address(ceaFactoryImplementation)), '",\n', - ' "proxyAdmin": "', vm.toString(address(proxyAdmin)), '",\n', - ' "ceaFactoryProxy": "', vm.toString(address(proxy)), '",\n', - ' "owner": "', vm.toString(owner), '",\n', - ' "vault": "', vm.toString(vault), '",\n', - ' "universalGateway": "', vm.toString(universalGateway), '"\n', - '}' - )); + string memory json = string( + abi.encodePacked( + "{\n", + ' "chainId": ', + vm.toString(chainId), + ",\n", + ' "deployer": "', + vm.toString(deployer), + '",\n', + ' "ceaImplementation": "', + vm.toString(address(ceaImplementation)), + '",\n', + ' "ceaProxyImplementation": "', + vm.toString(address(ceaProxyImplementation)), + '",\n', + ' "ceaFactoryImplementation": "', + vm.toString(address(ceaFactoryImplementation)), + '",\n', + ' "proxyAdmin": "', + vm.toString(address(proxyAdmin)), + '",\n', + ' "ceaFactoryProxy": "', + vm.toString(address(proxy)), + '",\n', + ' "owner": "', + vm.toString(owner), + '",\n', + ' "vault": "', + vm.toString(vault), + '",\n', + ' "universalGateway": "', + vm.toString(universalGateway), + '"\n', + "}" + ) + ); console.log(json); // Write to file diff --git a/src/CEA/CEA.sol b/src/CEA/CEA.sol index ee9b0ed..0b7ead9 100644 --- a/src/CEA/CEA.sol +++ b/src/CEA/CEA.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.26; import {ICEA} from "../interfaces/ICEA.sol"; -import {CEAErrors} from "../libraries/Errors.sol"; +import {CEAErrors, CommonErrors} from "../libraries/Errors.sol"; import {IUniversalGateway, UniversalTxRequest, RevertInstructions} from "../interfaces/IUniversalGateway.sol"; import {Multicall, MULTICALL_SELECTOR, MIGRATION_SELECTOR} from "../libraries/Types.sol"; import {ICEAFactory} from "../interfaces/ICEAFactory.sol"; @@ -115,15 +115,17 @@ contract CEA is ICEA, ReentrancyGuard { _handleExecution(txID, universalTxID, originCaller, payload); } - /// @notice Sends funds from CEA to its UEA on Push Chain. + /// @notice Sends funds (and optionally a payload) from CEA to its UEA on Push Chain. /// @dev Only callable via self-call through multicall execution (msg.sender == address(this)). /// For ERC20 tokens, SDK must include approval steps in multicall before this call. - /// This function only transfers funds and does not send payload or signature data. + /// Always routes through sendUniversalTxViaCEA for both tx types: + /// - FUNDS (payload empty, amount > 0) + /// - FUNDS_AND_PAYLOAD (payload non-empty, amount > 0) /// @param token Token address (address(0) for native) /// @param amount Amount to send - function sendUniversalTxToUEA(address token, uint256 amount) external { - // Enforce: Only CEA can call this function via self-call - if (msg.sender != address(this)) revert CEAErrors.NotVault(); + /// @param payload Payload bytes for UEA execution (empty for funds-only) + function sendUniversalTxToUEA(address token, uint256 amount, bytes calldata payload) external { + if (msg.sender != address(this)) revert CommonErrors.Unauthorized(); if (amount == 0) revert CEAErrors.InvalidInput(); @@ -131,61 +133,53 @@ contract CEA is ICEA, ReentrancyGuard { recipient: UEA, token: token, amount: amount, - payload: "", + payload: payload, revertInstruction: RevertInstructions({fundRecipient: UEA, revertMsg: ""}), signatureData: "" }); if (token == address(0)) { if (address(this).balance < amount) revert CEAErrors.InsufficientBalance(); - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTx{value: amount}(req); + IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxViaCEA{value: amount}(req); } else { if (IERC20(token).balanceOf(address(this)) < amount) revert CEAErrors.InsufficientBalance(); - // Note: SDK must have included ERC20 approval in multicall before this call - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTx(req); + IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxViaCEA(req); } - emit WithdrawalToUEA(address(this), UEA, token, amount); + emit UniversalTxToUEA(address(this), UEA, token, amount); } //======================== // Internal Helpers //======================== - /// @notice Routes execution based on payload type (MULTICALL vs SINGLE CALL vs MIGRATION). - /// @dev Checks if payload starts with MULTICALL_SELECTOR to determine routing. - /// Detects standalone migration multicalls and routes to _handleMigration(). + /// @notice Routes execution based on payload type (MULTICALL vs MIGRATION vs SINGLE CALL). + /// @dev Three-way branch matching UEA_EVM pattern: + /// 1. isMulticall → decode + _handleMulticall + /// 2. isMigration → _handleMigration (top-level, no Multicall wrapper) + /// 3. else → _handleSingleCall (backwards compatibility) /// @param txID Transaction identifier for event emission /// @param universalTxID Universal transaction identifier for event emission /// @param originCaller Origin caller for event emission - /// @param payload Raw payload bytes (either multicall or single call) + /// @param payload Raw payload bytes function _handleExecution(bytes32 txID, bytes32 universalTxID, address originCaller, bytes calldata payload) internal { if (isMulticall(payload)) { Multicall[] memory calls = decodeCalls(payload); - - // Detect single-element migration multicall - if (calls.length == 1 && isMigration(calls[0].data)) { - _handleMigration(calls[0]); - // Emit event for migration execution - emit UniversalTxExecuted(txID, universalTxID, originCaller, address(this), calls[0].data); - return; - } - - // Normal multicall execution _handleMulticall(txID, universalTxID, originCaller, calls); + } else if (isMigration(payload)) { + _handleMigration(); + emit UniversalTxExecuted(txID, universalTxID, originCaller, address(this), payload); } else { - // Old format: route to backwards-compatible handler _handleSingleCall(txID, universalTxID, originCaller, payload); } } /// @notice Internal handler for multicall execution. - /// @dev Validates msg.value matches total call values, then executes each call sequentially. - /// All calls use the same .call execution path (including self-calls). + /// @dev Executes each call sequentially. No strict msg.value == totalValue enforcement; + /// CEA can spend pre-existing balance in addition to Vault-provided msg.value. /// Self-calls must have value == 0 (enforced here). - /// Reverts if any call fails, bubbling revert data if available. /// @param txID Transaction identifier for event emission /// @param universalTxID Universal transaction identifier for event emission /// @param originCaller Origin caller for event emission @@ -193,28 +187,15 @@ contract CEA is ICEA, ReentrancyGuard { function _handleMulticall(bytes32 txID, bytes32 universalTxID, address originCaller, Multicall[] memory calls) internal { - // Validate msg.value matches sum of all call values - uint256 totalValue = 0; - for (uint256 i = 0; i < calls.length; i++) { - totalValue += calls[i].value; - } - if (msg.value != totalValue) revert CEAErrors.InvalidAmount(); - - // Execute each call in sequence for (uint256 i = 0; i < calls.length; i++) { if (calls[i].to == address(0)) revert CEAErrors.InvalidTarget(); - // Enforce: self-calls to CEA must not include value + // Self-calls to CEA must not include value if (calls[i].to == address(this) && calls[i].value != 0) { revert CEAErrors.InvalidInput(); } - // Prevent migration selector in batched multicalls (must be standalone) - if (isMigration(calls[i].data)) { - revert CEAErrors.InvalidCall(); - } - - (bool success, bytes memory returnData) = calls[i].to.call{value: calls[i].value}(calls[i].data); + (bool success,) = calls[i].to.call{value: calls[i].value}(calls[i].data); if (!success) revert CEAErrors.ExecutionFailed(); @@ -238,37 +219,18 @@ contract CEA is ICEA, ReentrancyGuard { _handleMulticall(txID, universalTxID, originCaller, calls); } - /// @notice Internal handler for migration execution - /// @dev Validates migration constraints and delegates to migration contract - /// @dev SAFETY CONSTRAINTS: - /// - Must target self (call.to == address(this)) - /// - Must have zero value (call.value == 0) - /// - Migration contract must be set in factory - /// - Executed via delegatecall (preserves proxy state) - /// @param call The migration Multicall struct - function _handleMigration(Multicall memory call) internal { - if (call.to != address(this)) { - revert CEAErrors.InvalidTarget(); - } - if (call.value != 0) { - revert CEAErrors.InvalidInput(); - } - - // Fetch migration contract address from factory + /// @notice Internal handler for migration execution. + /// @dev Fetches migration contract from factory and executes via delegatecall. + /// Migration payload is top-level MIGRATION_SELECTOR (no Multicall wrapper). + /// Migration contract must be set in factory (non-zero address). + /// Rejects msg.value > 0 — migration is a logic upgrade, not a value transfer. + function _handleMigration() internal { + if (msg.value != 0) revert CEAErrors.InvalidInput(); address migrationContract = factory.CEA_MIGRATION_CONTRACT(); + if (migrationContract == address(0)) revert CEAErrors.InvalidCall(); - // CONSTRAINT: Migration contract must be set - if (migrationContract == address(0)) { - revert CEAErrors.InvalidCall(); - } - - // Prepare delegatecall to migration contract bytes memory migrateCallData = abi.encodeWithSignature("migrateCEA()"); - - // Execute migration via delegatecall (writes to proxy storage) - (bool success, bytes memory returnData) = migrationContract.delegatecall(migrateCallData); - - // Bubble revert data on failure + (bool success,) = migrationContract.delegatecall(migrateCallData); if (!success) revert CEAErrors.ExecutionFailed(); } @@ -297,18 +259,13 @@ contract CEA is ICEA, ReentrancyGuard { return abi.decode(strippedData, (Multicall[])); } - /// @notice Checks whether the call data is a migration request. - /// @dev Determines if the data starts with MIGRATION_SELECTOR. - /// Uses bytes memory because it's called on Multicall.data (memory). - /// @param data Call data bytes - /// @return bool True if data starts with MIGRATION_SELECTOR - function isMigration(bytes memory data) private pure returns (bool) { + /// @notice Checks whether a top-level payload is a migration request. + /// @dev Calldata variant for use in _handleExecution's three-way branch. + /// @param data Raw payload bytes (calldata) + /// @return bool True if payload starts with MIGRATION_SELECTOR + function isMigration(bytes calldata data) private pure returns (bool) { if (data.length < 4) return false; - bytes4 selector; - assembly { - selector := mload(add(data, 32)) - } - return selector == MIGRATION_SELECTOR; + return bytes4(data[0:4]) == MIGRATION_SELECTOR; } //======================== diff --git a/src/Interfaces/ICEA.sol b/src/Interfaces/ICEA.sol index d187898..d61cf42 100644 --- a/src/Interfaces/ICEA.sol +++ b/src/Interfaces/ICEA.sol @@ -10,9 +10,9 @@ pragma solidity 0.8.26; * - CEAs are NOT user-owned wallets in v1. They are system-controlled accounts: * * Only the external-chain Vault may call state-changing functions. * * CEAs hold user positions and balances on external chains. - * - CEAs preserve the identity of a UEA on the external chains. + * - CEAs preserve the identity of a UEA on the external chains. * - Any action requested by UEA ( from Push Chain ) is executed by the CEA on the external chain. - * + * */ interface ICEA { //======================== @@ -26,19 +26,15 @@ interface ICEA { /// @param target Target contract address for this call step /// @param data Calldata executed on target contract event UniversalTxExecuted( - bytes32 indexed txID, - bytes32 indexed universalTxID, - address indexed originCaller, - address target, - bytes data + bytes32 indexed txID, bytes32 indexed universalTxID, address indexed originCaller, address target, bytes data ); - /// @notice Emitted when funds are withdrawn to the UEA on Push Chain. - /// @param _cea Address of the CEA that is withdrawing funds + /// @notice Emitted when a universal tx is sent from CEA to UEA on Push Chain. + /// @param _cea Address of the CEA sending the tx /// @param _uea Address of the UEA on Push Chain that this CEA represents. - /// @param token Token address being withdrawn - /// @param amount Amount of token being withdrawn - event WithdrawalToUEA(address indexed _cea, address indexed _uea, address indexed token, uint256 amount); + /// @param token Token address being sent + /// @param amount Amount of token being sent + event UniversalTxToUEA(address indexed _cea, address indexed _uea, address indexed token, uint256 amount); //======================== // Views //======================== @@ -73,7 +69,6 @@ interface ICEA { * - SDK is responsible for crafting correct multicall steps, including: * * ERC20 approvals before calls that need token spending * * ERC20 approval resets after operations (if needed) - * * Self-calls to withdrawFundsToUEA for returning funds to UEA * - CEA no longer performs automatic approval/reset logic. * - This ensures flexible, composable execution paths. * @@ -82,10 +77,7 @@ interface ICEA { * @param originCaller UEA on Push Chain that this CEA represents (verified) * @param payload ABI-encoded Multicall[] containing execution steps */ - function executeUniversalTx( - bytes32 txID, - bytes32 universalTxID, - address originCaller, - bytes calldata payload - ) external payable; -} \ No newline at end of file + function executeUniversalTx(bytes32 txID, bytes32 universalTxID, address originCaller, bytes calldata payload) + external + payable; +} diff --git a/src/Interfaces/IUniversalGateway.sol b/src/Interfaces/IUniversalGateway.sol index afd5943..960fbc4 100644 --- a/src/Interfaces/IUniversalGateway.sol +++ b/src/Interfaces/IUniversalGateway.sol @@ -22,4 +22,6 @@ interface IUniversalGateway { function sendUniversalTx(UniversalTxRequest calldata req) external payable; + function sendUniversalTxViaCEA(UniversalTxRequest calldata req) external payable; + } \ No newline at end of file diff --git a/test/mocks/MockUniversalGateway.sol b/test/mocks/MockUniversalGateway.sol index 4c3852a..c4698dc 100644 --- a/test/mocks/MockUniversalGateway.sol +++ b/test/mocks/MockUniversalGateway.sol @@ -20,6 +20,10 @@ contract MockUniversalGateway is IUniversalGateway { bytes public lastRevertMsg; bytes public lastSignatureData; + // Track which gateway function was called + bool public lastCallWasViaCEA; + uint256 public viaCEACallCount; + // Revert control for testing bool private shouldRevert; string private revertMessage; @@ -34,6 +38,21 @@ contract MockUniversalGateway is IUniversalGateway { revert(revertMessage); } + _storeRequest(req); + lastCallWasViaCEA = false; + } + + function sendUniversalTxViaCEA(UniversalTxRequest calldata req) external payable { + if (shouldRevert) { + revert(revertMessage); + } + + _storeRequest(req); + lastCallWasViaCEA = true; + viaCEACallCount++; + } + + function _storeRequest(UniversalTxRequest calldata req) private { lastRecipient = req.recipient; lastToken = req.token; lastAmount = req.amount; @@ -44,7 +63,7 @@ contract MockUniversalGateway is IUniversalGateway { lastValue = msg.value; callCount++; } - + // Helper to get full request struct (for event verification) function getLastRequest() external view returns (UniversalTxRequest memory) { return UniversalTxRequest({ @@ -60,4 +79,3 @@ contract MockUniversalGateway is IUniversalGateway { }); } } - diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 63c69cb..581031e 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -13,7 +13,7 @@ import {ICEAProxy} from "../../src/interfaces/ICEAProxy.sol"; import {CEAProxy} from "../../src/CEA/CEAProxy.sol"; import "../../src/interfaces/ICEA.sol"; import {IUniversalGateway, UniversalTxRequest, RevertInstructions} from "../../src/interfaces/IUniversalGateway.sol"; -import {CEAErrors as Errors} from "../../src/libraries/Errors.sol"; +import {CEAErrors as Errors, CommonErrors} from "../../src/libraries/Errors.sol"; import {Multicall, MULTICALL_SELECTOR} from "../../src/libraries/Types.sol"; import {Target} from "../../src/mocks/Target.sol"; import {MockUniversalGateway} from "../mocks/MockUniversalGateway.sol"; @@ -26,7 +26,6 @@ import {RevertingTarget} from "../mocks/RevertingTarget.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - contract CEATest is Test { // Core contracts CEA public ceaImplementation; @@ -47,35 +46,35 @@ contract CEATest is Test { // Constants bytes32 private constant CEA_LOGIC_SLOT = 0x8b2ae8ee8c8678fc65d38e03fd33865426627999aa5e8fab985583dec5888813; - + function setUp() public { owner = address(this); // Test contract as owner vault = makeAddr("vault"); ueaOnPush = makeAddr("ueaOnPush"); universalGateway = makeAddr("universalGateway"); nonVault = makeAddr("nonVault"); - + target = new Target(); mockUniversalGateway = new MockUniversalGateway(); ceaImplementation = new CEA(); - + ceaProxyImplementation = new CEAProxy(); - + CEAFactory factoryImpl = new CEAFactory(); - + bytes memory initData = abi.encodeWithSelector( CEAFactory.initialize.selector, - owner, - vault, - address(ceaProxyImplementation), - address(ceaImplementation), - address(mockUniversalGateway) + owner, + vault, + address(ceaProxyImplementation), + address(ceaImplementation), + address(mockUniversalGateway) ); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = CEAFactory(address(proxy)); } - + modifier deployCEA() { vm.prank(vault); address ceaAddress = factory.deployCEA(ueaOnPush); @@ -121,21 +120,26 @@ contract CEATest is Test { return abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); } - /// @notice Build payload for sendUniversalTxToUEA self-call (funds only, no payload/signature) - function buildWithdrawPayload(address token, uint256 amount) internal pure returns (bytes memory) { - return abi.encodeWithSignature( - "sendUniversalTxToUEA(address,uint256)", - token, - amount - ); + /// @notice Build payload for sendUniversalTxToUEA self-call (funds only, empty payload) + function buildSendToUEAPayload(address token, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeWithSignature("sendUniversalTxToUEA(address,uint256,bytes)", token, amount, ""); + } + + /// @notice Build payload for sendUniversalTxToUEA self-call with payload (FUNDS_AND_PAYLOAD) + function buildSendToUEAPayloadWithData(address token, uint256 amount, bytes memory payload) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSignature("sendUniversalTxToUEA(address,uint256,bytes)", token, amount, payload); } /// @notice Build single external call payload (no approvals) - function buildExternalSingleCall( - address to, - uint256 value, - bytes memory data - ) internal pure returns (bytes memory) { + function buildExternalSingleCall(address to, uint256 value, bytes memory data) + internal + pure + returns (bytes memory) + { Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall(to, value, data); return encodeCalls(calls); @@ -146,14 +150,10 @@ contract CEATest is Test { return encodeCalls(calls); } - /// @notice Build self-call withdraw payload (single step, no approvals) + /// @notice Build self-call sendUniversalTxToUEA payload (single step, no approvals) /// @dev For ERC20, SDK must include approval steps separately - function buildSelfWithdrawCall(address token, uint256 amount) internal view returns (Multicall memory) { - return makeCall( - address(ceaInstance), - 0, - buildWithdrawPayload(token, amount) - ); + function buildSelfSendToUEACall(address token, uint256 amount) internal view returns (Multicall memory) { + return makeCall(address(ceaInstance), 0, buildSendToUEAPayload(token, amount)); } /// @notice Build Multicall[] payload for ERC20 operations (with approval flow) @@ -162,34 +162,22 @@ contract CEATest is Test { /// @param amount Amount to approve /// @param targetCalldata Calldata for target contract /// @return Encoded Multicall[] array - function buildERC20MulticallPayload( - address token, - address target, - uint256 amount, - bytes memory targetCalldata - ) internal pure returns (bytes memory) { + function buildERC20MulticallPayload(address token, address target, uint256 amount, bytes memory targetCalldata) + internal + pure + returns (bytes memory) + { Multicall[] memory calls = new Multicall[](3); // Step 1: Reset approval to 0 - calls[0] = Multicall({ - to: token, - value: 0, - data: abi.encodeWithSelector(IERC20.approve.selector, target, 0) - }); + calls[0] = Multicall({to: token, value: 0, data: abi.encodeWithSelector(IERC20.approve.selector, target, 0)}); // Step 2: Approve amount - calls[1] = Multicall({ - to: token, - value: 0, - data: abi.encodeWithSelector(IERC20.approve.selector, target, amount) - }); + calls[1] = + Multicall({to: token, value: 0, data: abi.encodeWithSelector(IERC20.approve.selector, target, amount)}); // Step 3: Execute target call - calls[2] = Multicall({ - to: target, - value: 0, - data: targetCalldata - }); + calls[2] = Multicall({to: target, value: 0, data: targetCalldata}); return abi.encode(calls); } @@ -199,43 +187,35 @@ contract CEATest is Test { /// @param value Native token value to send /// @param targetCalldata Calldata for target contract /// @return Encoded Multicall[] array - function buildNativeMulticallPayload( - address target, - uint256 value, - bytes memory targetCalldata - ) internal pure returns (bytes memory) { + function buildNativeMulticallPayload(address target, uint256 value, bytes memory targetCalldata) + internal + pure + returns (bytes memory) + { Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: target, - value: value, - data: targetCalldata - }); + calls[0] = Multicall({to: target, value: value, data: targetCalldata}); return abi.encode(calls); } - /// @notice Build Multicall[] payload for self-call (withdrawFundsToUEA) + /// @notice Build Multicall[] payload for self-call (sendUniversalTxToUEA) /// @param token Token address (address(0) for native) - /// @param amount Amount to withdraw - /// @param approveGateway Whether to approve gateway for ERC20 (true for ERC20 withdrawals) + /// @param amount Amount to send + /// @param approveGateway Whether to approve gateway for ERC20 (true for ERC20 sends) /// @return Encoded Multicall[] array - function buildWithdrawMulticallPayload( - address token, - uint256 amount, - bool approveGateway - ) internal view returns (bytes memory) { + function buildSendToUEAMulticallPayload(address token, uint256 amount, bool approveGateway) + internal + view + returns (bytes memory) + { if (!approveGateway || token == address(0)) { - // Native token withdrawal or no approval needed + // Native token send or no approval needed Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: address(ceaInstance), - value: 0, - data: buildWithdrawPayload(token, amount) - }); + calls[0] = Multicall({to: address(ceaInstance), value: 0, data: buildSendToUEAPayload(token, amount)}); return abi.encode(calls); } else { - // ERC20 withdrawal with gateway approval + // ERC20 send with gateway approval Multicall[] memory calls = new Multicall[](3); // Step 1: Reset approval to gateway @@ -252,12 +232,8 @@ contract CEATest is Test { data: abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), amount) }); - // Step 3: Self-call to withdrawFundsToUEA - calls[2] = Multicall({ - to: address(ceaInstance), - value: 0, - data: buildWithdrawPayload(token, amount) - }); + // Step 3: Self-call to sendUniversalTxToUEA + calls[2] = Multicall({to: address(ceaInstance), value: 0, data: buildSendToUEAPayload(token, amount)}); return abi.encode(calls); } @@ -268,22 +244,16 @@ contract CEATest is Test { /// @param value Native token value to send /// @param targetCalldata Calldata for target contract /// @return Encoded Multicall[] array - function buildSimpleMulticallPayload( - address target, - uint256 value, - bytes memory targetCalldata - ) internal pure returns (bytes memory) { + function buildSimpleMulticallPayload(address target, uint256 value, bytes memory targetCalldata) + internal + pure + returns (bytes memory) + { Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: target, - value: value, - data: targetCalldata - }); + calls[0] = Multicall({to: target, value: value, data: targetCalldata}); return abi.encode(calls); } - - // ========================================================================= // Initialize and Setup Tests // ========================================================================= @@ -292,7 +262,7 @@ contract CEATest is Test { assertTrue(ceaInstance.isInitialized(), "CEA should be initialized"); assertEq(ceaInstance.UEA(), ueaOnPush, "UEA should match"); assertEq(ceaInstance.VAULT(), vault, "VAULT should match"); - + // Verify event was emitted during deployment (factory calls initializeCEA) // Note: The event is emitted during factory.deployCEA, so we verify via state address cea = address(ceaInstance); @@ -301,30 +271,30 @@ contract CEATest is Test { assertEq(returnedCEA, cea, "Factory reverse mapping should be correct"); assertTrue(isDeployed, "CEA should be marked as deployed"); } - + function testRevertWhenInitializingTwice() public { CEA newCEA = new CEA(); - + newCEA.initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); - + vm.expectRevert(Errors.AlreadyInitialized.selector); newCEA.initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); } - + function testRevertWhenInitializingWithZeroUEA() public { CEA newCEA = new CEA(); - + vm.expectRevert(Errors.ZeroAddress.selector); newCEA.initializeCEA(address(0), vault, address(mockUniversalGateway), address(factory)); } - + function testRevertWhenInitializingWithZeroVault() public { CEA newCEA = new CEA(); - + vm.expectRevert(Errors.ZeroAddress.selector); newCEA.initializeCEA(ueaOnPush, address(0), address(mockUniversalGateway), address(factory)); } - + function testRevertWhenInitializingWithZeroUniversalGateway() public { CEA newCEA = new CEA(); @@ -341,37 +311,37 @@ contract CEATest is Test { function testIsInitializedBeforeInitialization() public { CEA newCEA = new CEA(); - + assertFalse(newCEA.isInitialized(), "CEA should not be initialized before initializeCEA is called"); } - + function testFactoryDeployment() public { vm.prank(vault); address ceaAddress = factory.deployCEA(ueaOnPush); - + assertTrue(factory.isCEA(ceaAddress), "Factory should recognize deployed CEA"); assertEq(factory.getUEAForCEA(ceaAddress), ueaOnPush, "Factory should map CEA to UEA"); (address mappedCEA, bool isDeployed) = factory.getCEAForUEA(ueaOnPush); assertEq(mappedCEA, ceaAddress, "Factory should map UEA to CEA"); assertTrue(isDeployed, "Factory should mark CEA as deployed"); } - + function testRevertWhenDeployingCEAAsNonVault() public { vm.prank(nonVault); vm.expectRevert(); factory.deployCEA(ueaOnPush); } - + function testRevertWhenDeployingCEAWithZeroUEA() public { vm.prank(vault); vm.expectRevert(); factory.deployCEA(address(0)); } - + function testRevertWhenDeployingCEATwice() public { vm.prank(vault); factory.deployCEA(ueaOnPush); - + vm.prank(vault); vm.expectRevert(); factory.deployCEA(ueaOnPush); @@ -388,21 +358,11 @@ contract CEATest is Test { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory targetCalldata = abi.encodeWithSignature("setMagicNumber(uint256)", 42); - bytes memory payload = buildERC20MulticallPayload( - address(token), - address(target), - 100 ether, - targetCalldata - ); + bytes memory payload = buildERC20MulticallPayload(address(token), address(target), 100 ether, targetCalldata); vm.prank(nonVault); vm.expectRevert(Errors.NotVault.selector); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - payload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } function testExecuteUniversalTx_SuccessWhenCalledByVault() public deployCEA { @@ -413,20 +373,10 @@ contract CEATest is Test { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory targetCalldata = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); - bytes memory payload = buildERC20MulticallPayload( - address(token), - address(spender), - 100 ether, - targetCalldata - ); + bytes memory payload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, targetCalldata); vm.prank(vault); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - payload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(spender.totalReceived(address(token)), 100 ether, "Target should receive tokens"); @@ -444,30 +394,15 @@ contract CEATest is Test { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory targetCalldata = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); - bytes memory payload = buildERC20MulticallPayload( - address(token), - address(spender), - 100 ether, - targetCalldata - ); + bytes memory payload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, targetCalldata); vm.prank(vault); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - payload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); // Try to execute same txID again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - payload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } // ------------------------------------------------------------------------- @@ -477,7 +412,7 @@ contract CEATest is Test { function testExecuteUniversalTx_RevertWhenInvalidUEA() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -486,23 +421,13 @@ contract CEATest is Test { vm.expectRevert(Errors.InvalidUEA.selector); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - makeAddr("wrongUEA"), - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, makeAddr("wrongUEA"), multicallPayload); } function testExecuteUniversalTx_RevertWhenTargetIsZero() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -511,23 +436,13 @@ contract CEATest is Test { vm.expectRevert(Errors.InvalidTarget.selector); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(0), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } function testExecuteUniversalTx_SuccessWithSufficientTokenBalance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 100 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -536,17 +451,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Exact balance should work"); } @@ -558,9 +463,9 @@ contract CEATest is Test { function testExecuteUniversalTx_ResetsApprovalBeforeGranting() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); - + // Set an existing approval vm.prank(address(ceaInstance)); token.approve(address(spender), 500 ether); @@ -573,17 +478,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Approval should be reset to 0 after execution assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Approval should be reset"); @@ -592,7 +487,7 @@ contract CEATest is Test { function testExecuteUniversalTx_GrantsCorrectApprovalAmount() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -602,17 +497,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Correct amount should be approved and spent"); } @@ -620,7 +505,7 @@ contract CEATest is Test { function testExecuteUniversalTx_ResetsApprovalAfterExecution() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -629,17 +514,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Approval should be reset to 0 after execution assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Approval should be reset after execution"); @@ -648,11 +523,11 @@ contract CEATest is Test { function testExecuteUniversalTx_TokenRevertsOnZeroApproval() public deployCEA { NonStandardERC20Token token = new NonStandardERC20Token("NonStdToken", "NST", 18); fundCEAWithTokens(address(token), 1000 ether); - + // Set an existing approval first vm.prank(address(ceaInstance)); token.approve(address(target), 500 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -662,19 +537,11 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); + assertEq( + spender.totalReceived(address(token)), 100 ether, "Execution should succeed despite zero approval revert" ); - - assertEq(spender.totalReceived(address(token)), 100 ether, "Execution should succeed despite zero approval revert"); } // ------------------------------------------------------------------------- @@ -684,7 +551,7 @@ contract CEATest is Test { function testExecuteUniversalTx_SuccessfulCallToTarget() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -692,17 +559,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); } @@ -710,26 +567,17 @@ contract CEATest is Test { function testExecuteUniversalTx_TargetReceivesCorrectTokenAmount() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + TokenReceiverTarget receiver = new TokenReceiverTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("receiveTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); - bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(receiver), 100 ether, payload); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = + buildERC20MulticallPayload(address(token), address(receiver), 100 ether, payload); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(receiver.tokenBalances(address(token)), 100 ether, "Target should receive correct amount"); assertEq(MockGasToken(token).balanceOf(address(receiver)), 100 ether, "Balance should be correct"); @@ -738,37 +586,30 @@ contract CEATest is Test { function testExecuteUniversalTx_RevertWhenTargetReverts() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + RevertingTarget reverter = new RevertingTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("revertWithReason()"); vm.prank(vault); - bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); + bytes memory multicallPayload = + buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); // Expect ExecutionFailed (revert data no longer bubbled) vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // txID should NOT be marked as executed when execution fails - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked as executed on failure"); + assertFalse( + CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked as executed on failure" + ); } function testExecuteUniversalTx_SuccessWithEmptyPayload() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + TokenSpenderTarget spender = new TokenSpenderTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -779,19 +620,10 @@ contract CEATest is Test { bytes memory spendPayload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); - bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, spendPayload); - - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, + bytes memory multicallPayload = + buildERC20MulticallPayload(address(token), address(spender), 100 ether, spendPayload); - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Empty payload should work"); } @@ -799,7 +631,7 @@ contract CEATest is Test { function testExecuteUniversalTx_ExecutesPayloadCorrectly() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 magicValue = 999; @@ -808,17 +640,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(target.getMagicNumber(), magicValue, "Payload should execute with correct parameters"); } @@ -829,7 +651,7 @@ contract CEATest is Test { function testExecuteUniversalTx_RevertWhenCalledByNonVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -839,22 +661,12 @@ contract CEATest is Test { vm.expectRevert(Errors.NotVault.selector); bytes memory multicallPayload = buildNativeMulticallPayload(address(target), 0.1 ether, payload); - ceaInstance.executeUniversalTx{value: 0.1 ether}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, multicallPayload); } function testExecuteUniversalTx_RevertWhenInvalidUEA_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -864,47 +676,33 @@ contract CEATest is Test { vm.expectRevert(Errors.InvalidUEA.selector); bytes memory multicallPayload = buildNativeMulticallPayload(address(target), 0.1 ether, payload); - ceaInstance.executeUniversalTx{value: 0.1 ether}( - - txID, - - universalTxID, - - makeAddr("wrongUEA"), - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, makeAddr("wrongUEA"), multicallPayload); } - function testExecuteUniversalTx_RevertWhenMsgValueDoesNotMatchAmount_Native() public deployCEA { + + function testExecuteUniversalTx_MsgValueExceedsCallValue_Native_Succeeds() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); - + vm.prank(vault); vm.deal(vault, 0.2 ether); - vm.expectRevert(Errors.InvalidAmount.selector); - bytes memory multicallPayload = buildNativeMulticallPayload(address(target), 0.1 ether, // Different from msg.value - payload); - - ceaInstance.executeUniversalTx{value: 0.2 ether}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - + bytes memory multicallPayload = buildNativeMulticallPayload( + address(target), + 0.1 ether, + payload ); + + // Excess msg.value stays in CEA — no strict equality check + ceaInstance.executeUniversalTx{value: 0.2 ether}(txID, universalTxID, ueaOnPush, multicallPayload); + + assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); } function testExecuteUniversalTx_SuccessWhenMsgValueEqualsAmount_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -914,17 +712,7 @@ contract CEATest is Test { vm.deal(vault, amount); bytes memory multicallPayload = buildNativeMulticallPayload(address(target), amount, payload); - ceaInstance.executeUniversalTx{value: amount}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(address(target).balance, amount, "Target should receive correct amount"); } @@ -932,11 +720,11 @@ contract CEATest is Test { // Note: Native token balance check doesn't apply here because: // - Validation only checks msg.value == amount for native tokens // - The CEA receives msg.value, so balance is always sufficient - // - Insufficient balance only matters for self-calls (withdrawFundsToUEA) + // - Insufficient balance only matters for self-calls (sendUniversalTxToUEA) function testExecuteUniversalTx_SuccessfulCallToTarget_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -945,17 +733,7 @@ contract CEATest is Test { vm.deal(vault, 0.1 ether); bytes memory multicallPayload = buildNativeMulticallPayload(address(target), 0.1 ether, payload); - ceaInstance.executeUniversalTx{value: 0.1 ether}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); assertEq(address(target).balance, 0.1 ether, "Target should receive native tokens"); @@ -963,7 +741,7 @@ contract CEATest is Test { function testExecuteUniversalTx_TargetReceivesCorrectNativeAmount() public deployCEA { fundCEAWithNative(1000 ether); - + TokenReceiverTarget receiver = new TokenReceiverTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -974,24 +752,14 @@ contract CEATest is Test { vm.deal(vault, amount); bytes memory multicallPayload = buildNativeMulticallPayload(address(receiver), amount, payload); - ceaInstance.executeUniversalTx{value: amount}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(receiver.nativeBalance(), amount, "Target should receive correct native amount"); } function testExecuteUniversalTx_RevertWhenTargetReverts_Native() public deployCEA { fundCEAWithNative(1000 ether); - + RevertingTarget reverter = new RevertingTarget(); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -1002,20 +770,9 @@ contract CEATest is Test { vm.expectRevert(Errors.ExecutionFailed.selector); bytes memory multicallPayload = buildNativeMulticallPayload(address(reverter), 0.1 ether, payload); - ceaInstance.executeUniversalTx{value: 0.1 ether}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, multicallPayload); } - // ========================================================================= // Event Emission Tests // ========================================================================= @@ -1023,7 +780,7 @@ contract CEATest is Test { function testExecuteUniversalTx_EmitsUniversalTxExecutedEvent() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -1035,28 +792,12 @@ contract CEATest is Test { vm.expectEmit(true, true, true, true); emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(target), payload); - - ceaInstance.executeUniversalTx( - - - txID, - - - universalTxID, - - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } function testExecuteUniversalTx_EmitsUniversalTxExecutedEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -1069,80 +810,39 @@ contract CEATest is Test { vm.expectEmit(true, true, true, true); emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(target), payload); - - ceaInstance.executeUniversalTx{value: amount}( - - - txID, - - - universalTxID, - - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, multicallPayload); } - - // ========================================================================= - // withdrawFundsToUEA Tests - ERC20 Token Version - // ========================================================================= - // ------------------------------------------------------------------------- // 1. ACCESS CONTROL & AUTHORIZATION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_RevertWhenCalledByNonVault() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenCalledByNonVault() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(nonVault); vm.expectRevert(Errors.NotVault.selector); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_SuccessWhenCalledByVault() public deployCEA { + function testSendUniversalTxToUEA_SuccessWhenCalledByVault() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); @@ -1152,71 +852,41 @@ contract CEATest is Test { // 2. _handleSelfCalls VALIDATION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_RevertWhenTxIDAlreadyExecuted() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenTxIDAlreadyExecuted() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Try to execute same txID again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenInvalidUEA() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInvalidUEA() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); vm.expectRevert(Errors.InvalidUEA.selector); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - universalTxID, - - makeAddr("wrongUEA"), - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, makeAddr("wrongUEA"), multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenPayloadTooShort() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenPayloadTooShort() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); @@ -1231,15 +901,10 @@ contract CEATest is Test { vm.prank(vault); // After removing _handleSelfCall, malformed calls execute via .call() and fail vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenInvalidSelector() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInvalidSelector() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); @@ -1251,7 +916,9 @@ contract CEATest is Test { calls[0] = makeCall( address(ceaInstance), 0, - abi.encodeWithSignature("initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0)) + abi.encodeWithSignature( + "initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0) + ) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1259,92 +926,57 @@ contract CEATest is Test { // Calls initializeCEA via .call() which reverts with AlreadyInitialized // but we now get ExecutionFailed instead of bubbled error vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } // ------------------------------------------------------------------------- // 3. BALANCE VALIDATION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_RevertWhenInsufficientERC20Balance() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInsufficientERC20Balance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 100 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_SuccessWithExactERC20Balance() public deployCEA { + function testSendUniversalTxToUEA_SuccessWithExactERC20Balance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 500 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } - function testWithdrawFundsToUEA_SuccessWithMoreThanRequiredBalance() public deployCEA { + function testSendUniversalTxToUEA_SuccessWithMoreThanRequiredBalance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, - - ueaOnPush, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); } @@ -1353,29 +985,19 @@ contract CEATest is Test { // 4. UNIVERSAL GATEWAY INTERACTION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_CallsGatewayWithCorrectParams_ERC20() public deployCEA { + function testSendUniversalTxToUEA_CallsGatewayWithCorrectParams_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), amount); + bytes memory payload = buildSendToUEAPayload(address(token), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), amount, true); - - ceaInstance.executeUniversalTx( + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(token), "Token should match"); @@ -1386,30 +1008,20 @@ contract CEATest is Test { assertEq(mockUniversalGateway.lastValue(), 0, "No native value should be sent"); } - function testWithdrawFundsToUEA_CallsGatewayExactlyOnce_ERC20() public deployCEA { + function testSendUniversalTxToUEA_CallsGatewayExactlyOnce_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); uint256 callCountBefore = mockUniversalGateway.callCount(); - - vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload + vm.prank(vault); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(mockUniversalGateway.callCount(), callCountBefore + 1, "Gateway should be called exactly once"); } @@ -1418,230 +1030,167 @@ contract CEATest is Test { // 5. ERC20 APPROVAL PATTERN TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_ResetsApprovalBeforeGranting() public deployCEA { + function testSendUniversalTxToUEA_ResetsApprovalBeforeGranting() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + // Set an existing approval to gateway vm.prank(address(ceaInstance)); token.approve(address(mockUniversalGateway), 300 ether); - assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), 300 ether, "Initial approval should exist"); + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 300 ether, + "Initial approval should exist" + ); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Approval should be set to amount (gateway may or may not consume it) - assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), 500 ether, "Approval should be set to amount"); + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 500 ether, + "Approval should be set to amount" + ); } - function testWithdrawFundsToUEA_GrantsCorrectApprovalAmount() public deployCEA { + function testSendUniversalTxToUEA_GrantsCorrectApprovalAmount() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), amount); + bytes memory payload = buildSendToUEAPayload(address(token), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), amount, true); - - ceaInstance.executeUniversalTx( + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Gateway should have approval for exact amount - assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), amount, "Approval should match amount"); + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), amount, "Approval should match amount" + ); } // ------------------------------------------------------------------------- // 6. STATE CHANGES TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_MarksTxIDAsExecuted() public deployCEA { + function testSendUniversalTxToUEA_MarksTxIDAsExecuted() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(token), 500 ether); assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be executed before"); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed after"); } - function testWithdrawFundsToUEA_ERC20BalanceDecreases() public deployCEA { + function testSendUniversalTxToUEA_ERC20BalanceDecreases() public deployCEA { MockGasToken token = new MockGasToken(); uint256 initialBalance = 1000 ether; fundCEAWithTokens(address(token), initialBalance); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), withdrawAmount); + uint256 sendAmount = 500 ether; + bytes memory payload = buildSendToUEAPayload(address(token), sendAmount); uint256 balanceBefore = token.balanceOf(address(ceaInstance)); - - vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), withdrawAmount, true); - - ceaInstance.executeUniversalTx( - - txID, - universalTxID, - - ueaOnPush, - - multicallPayload + vm.prank(vault); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), sendAmount, true); - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Gateway receives approval but mock doesn't transfer tokens // So balance remains the same, but approval should be granted uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); - assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), withdrawAmount, "Gateway should have approval"); + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + sendAmount, + "Gateway should have approval" + ); } // ------------------------------------------------------------------------- // 7. EVENT EMISSION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_EmitsWithdrawalToUEAEvent_ERC20() public deployCEA { + function testSendUniversalTxToUEA_EmitsUniversalTxToUEAEvent_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), amount); + bytes memory payload = buildSendToUEAPayload(address(token), amount); vm.prank(vault); vm.expectEmit(true, true, true, true); - emit ICEA.WithdrawalToUEA(address(ceaInstance), ueaOnPush, address(token), amount); - - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), amount, true); - - - ceaInstance.executeUniversalTx( - - - txID, + emit ICEA.UniversalTxToUEA(address(ceaInstance), ueaOnPush, address(token), amount); - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_EmitsUniversalTxExecutedEvent_ERC20() public deployCEA { + function testSendUniversalTxToUEA_EmitsUniversalTxExecutedEvent_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), amount); + bytes memory payload = buildSendToUEAPayload(address(token), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), amount, true); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); vm.expectEmit(true, true, true, true); emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); - - ceaInstance.executeUniversalTx( - - - txID, - - - universalTxID, - - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } // ------------------------------------------------------------------------- // 8. EDGE CASES & SECURITY TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_HandlesZeroAmount_ERC20() public deployCEA { + function testSendUniversalTxToUEA_HandlesZeroAmount_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(token), 0); + bytes memory payload = buildSendToUEAPayload(address(token), 0); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 0, true); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 0, true); - // Zero amount withdrawals revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) + // Zero amount sends revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_MultipleWithdrawalsWithDifferentTxIDs_ERC20() public deployCEA { + function testSendUniversalTxToUEA_MultipleSendsWithDifferentTxIDs_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 2000 ether); @@ -1650,22 +1199,12 @@ contract CEATest is Test { for (uint256 i = 1; i <= 3; i++) { bytes32 txID = generateTxID(i); bytes32 universalTxID = generateUniversalTxID(i); - bytes memory payload = buildWithdrawPayload(address(token), amount); + bytes memory payload = buildSendToUEAPayload(address(token), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), amount, true); - - ceaInstance.executeUniversalTx( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); } @@ -1673,202 +1212,115 @@ contract CEATest is Test { assertEq(mockUniversalGateway.callCount(), 3, "Gateway should be called 3 times"); } - // REMOVED: Non-standard token compatibility is SDK responsibility, not contract - // USDT-style tokens that revert on approve(X, 0) when allowance > 0 require - // SDK to use compatible approval patterns (skip reset, or use approve(max)) - // CEA executes multicall as provided - token-specific quirks handled by SDK - // See FAILED_TEST_ANALYSIS.md Category 4, Test 1 for details - // function testWithdrawFundsToUEA_WithNonStandardToken() public deployCEA { - // NonStandardERC20Token token = new NonStandardERC20Token("NonStdToken", "NST", 18); - // fundCEAWithTokens(address(token), 1000 ether); - // - // // Set an existing approval first - // vm.prank(address(ceaInstance)); - // token.approve(address(mockUniversalGateway), 300 ether); - // - // bytes32 txID = generateTxID(1); - // bytes32 universalTxID = generateUniversalTxID(1); - // bytes memory payload = buildWithdrawPayload(address(token), 500 ether); - // - // vm.prank(vault); - // bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - // - // ceaInstance.executeUniversalTx( - // txID, - // universalTxID, - // ueaOnPush, - // multicallPayload - // ); - // - // assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "Execution should succeed despite zero approval revert"); - // assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), 500 ether, "Approval should be set"); - // } - // ------------------------------------------------------------------------- // 9. INTEGRATION TESTS // ------------------------------------------------------------------------- - function testWithdrawFundsToUEA_FullFlow_ERC20() public deployCEA { + function testSendUniversalTxToUEA_FullFlow_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); uint256 initialBalance = 1000 ether; fundCEAWithTokens(address(token), initialBalance); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(token), withdrawAmount); + uint256 sendAmount = 500 ether; + bytes memory payload = buildSendToUEAPayload(address(token), sendAmount); uint256 balanceBefore = token.balanceOf(address(ceaInstance)); uint256 gatewayCallCountBefore = mockUniversalGateway.callCount(); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), withdrawAmount, true); - - ceaInstance.executeUniversalTx( - - txID, - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), sendAmount, true); - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, multicallPayload); // Verify all state changes assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(mockUniversalGateway.callCount(), gatewayCallCountBefore + 1, "Gateway should be called once"); - + assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(token), "Token should match"); - assertEq(mockUniversalGateway.lastAmount(), withdrawAmount, "Amount should match"); - + assertEq(mockUniversalGateway.lastAmount(), sendAmount, "Amount should match"); + // Gateway receives approval but mock doesn't transfer tokens // So balance remains the same, but approval should be granted uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); - assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), withdrawAmount, "Gateway should have approval"); + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + sendAmount, + "Gateway should have approval" + ); } // ========================================================================= - // withdrawFundsToUEA Tests - Native Token Version + // sendUniversalTxToUEA Tests - Native Token Version // ========================================================================= - function testWithdrawFundsToUEA_RevertWhenCalledByNonVault_Native() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenCalledByNonVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(nonVault); vm.deal(nonVault, 0.1 ether); vm.expectRevert(Errors.NotVault.selector); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_SuccessWhenCalledByVault_Native() public deployCEA { + function testSendUniversalTxToUEA_SuccessWhenCalledByVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } - function testWithdrawFundsToUEA_RevertWhenTxIDAlreadyExecuted_Native() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenTxIDAlreadyExecuted_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); // Try to execute same txID again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenInvalidUEA_Native() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInvalidUEA_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); vm.expectRevert(Errors.InvalidUEA.selector); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - makeAddr("wrongUEA"), - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, makeAddr("wrongUEA"), multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenPayloadTooShort_Native() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenPayloadTooShort_Native() public deployCEA { fundCEAWithNative(1000 ether); bytes32 txID = generateTxID(1); @@ -1882,15 +1334,10 @@ contract CEATest is Test { vm.prank(vault); // After removing _handleSelfCall, malformed calls execute via .call() and fail vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx{value: 0}( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenInvalidSelector_Native() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInvalidSelector_Native() public deployCEA { fundCEAWithNative(1000 ether); bytes32 txID = generateTxID(1); @@ -1901,7 +1348,9 @@ contract CEATest is Test { calls[0] = makeCall( address(ceaInstance), 0, - abi.encodeWithSignature("initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0)) + abi.encodeWithSignature( + "initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0) + ) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1909,111 +1358,66 @@ contract CEATest is Test { // Calls initializeCEA via .call() which reverts with AlreadyInitialized // but we now get ExecutionFailed instead of bubbled error vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx{value: 0}( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_RevertWhenInsufficientNativeBalance() public deployCEA { + function testSendUniversalTxToUEA_RevertWhenInsufficientNativeBalance() public deployCEA { // Don't fund CEA bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_SuccessWithExactNativeBalance() public deployCEA { + function testSendUniversalTxToUEA_SuccessWithExactNativeBalance() public deployCEA { uint256 balance = 500 ether; fundCEAWithNative(balance); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), balance); + bytes memory payload = buildSendToUEAPayload(address(0), balance); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), balance, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), balance, false); - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } - function testWithdrawFundsToUEA_SuccessWithMoreThanRequiredBalance_Native() public deployCEA { + function testSendUniversalTxToUEA_SuccessWithMoreThanRequiredBalance_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); } - function testWithdrawFundsToUEA_CallsGatewayWithCorrectParams_Native() public deployCEA { + function testSendUniversalTxToUEA_CallsGatewayWithCorrectParams_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(0), amount); + bytes memory payload = buildSendToUEAPayload(address(0), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), amount, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(0), "Token should be address(0) for native"); @@ -2023,178 +1427,111 @@ contract CEATest is Test { assertEq(mockUniversalGateway.lastValue(), amount, "Native value should match amount"); } - function testWithdrawFundsToUEA_CallsGatewayExactlyOnce_Native() public deployCEA { + function testSendUniversalTxToUEA_CallsGatewayExactlyOnce_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); uint256 callCountBefore = mockUniversalGateway.callCount(); - - vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - ueaOnPush, - - multicallPayload + vm.prank(vault); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertEq(mockUniversalGateway.callCount(), callCountBefore + 1, "Gateway should be called exactly once"); } - function testWithdrawFundsToUEA_MarksTxIDAsExecuted_Native() public deployCEA { + function testSendUniversalTxToUEA_MarksTxIDAsExecuted_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be executed before"); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed after"); } - function testWithdrawFundsToUEA_NativeBalanceDecreases() public deployCEA { + function testSendUniversalTxToUEA_NativeBalanceDecreases() public deployCEA { uint256 initialBalance = 1000 ether; fundCEAWithNative(initialBalance); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(0), withdrawAmount); + uint256 sendAmount = 500 ether; + bytes memory payload = buildSendToUEAPayload(address(0), sendAmount); uint256 balanceBefore = address(ceaInstance).balance; - - vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), withdrawAmount, false); - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, + vm.prank(vault); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), sendAmount, false); - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); uint256 balanceAfter = address(ceaInstance).balance; - assertEq(balanceAfter, balanceBefore - withdrawAmount, "Balance should decrease by exact amount"); - assertEq(mockUniversalGateway.lastValue(), withdrawAmount, "Gateway should receive correct value"); + assertEq(balanceAfter, balanceBefore - sendAmount, "Balance should decrease by exact amount"); + assertEq(mockUniversalGateway.lastValue(), sendAmount, "Gateway should receive correct value"); } - function testWithdrawFundsToUEA_EmitsWithdrawalToUEAEvent_Native() public deployCEA { + function testSendUniversalTxToUEA_EmitsUniversalTxToUEAEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(0), amount); + bytes memory payload = buildSendToUEAPayload(address(0), amount); vm.prank(vault); vm.expectEmit(true, true, true, true); - emit ICEA.WithdrawalToUEA(address(ceaInstance), ueaOnPush, address(0), amount); - - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), amount, false); - - - ceaInstance.executeUniversalTx{value: 0}( - - - txID, + emit ICEA.UniversalTxToUEA(address(ceaInstance), ueaOnPush, address(0), amount); - - universalTxID, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_EmitsUniversalTxExecutedEvent_Native() public deployCEA { + function testSendUniversalTxToUEA_EmitsUniversalTxExecutedEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - + bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(0), amount); + bytes memory payload = buildSendToUEAPayload(address(0), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), amount, false); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); vm.expectEmit(true, true, true, true); emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); - - ceaInstance.executeUniversalTx{value: 0}( - - - txID, - - - universalTxID, - - - ueaOnPush, - - - multicallPayload - - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_HandlesZeroAmount_Native() public deployCEA { + function testSendUniversalTxToUEA_HandlesZeroAmount_Native() public deployCEA { fundCEAWithNative(1000 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildWithdrawPayload(address(0), 0); + bytes memory payload = buildSendToUEAPayload(address(0), 0); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 0, false); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - // Zero amount withdrawals revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) + // Zero amount sends revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx{value: 0}( - txID, - universalTxID, - ueaOnPush, - multicallPayload - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); } - function testWithdrawFundsToUEA_MultipleWithdrawalsWithDifferentTxIDs_Native() public deployCEA { + function testSendUniversalTxToUEA_MultipleSendsWithDifferentTxIDs_Native() public deployCEA { fundCEAWithNative(2000 ether); uint256 amount = 500 ether; @@ -2202,22 +1539,12 @@ contract CEATest is Test { for (uint256 i = 1; i <= 3; i++) { bytes32 txID = generateTxID(i); bytes32 universalTxID = generateUniversalTxID(i); - bytes memory payload = buildWithdrawPayload(address(0), amount); + bytes memory payload = buildSendToUEAPayload(address(0), amount); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), amount, false); - - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); } @@ -2225,32 +1552,22 @@ contract CEATest is Test { assertEq(mockUniversalGateway.callCount(), 3, "Gateway should be called 3 times"); } - function testWithdrawFundsToUEA_FullFlow_Native() public deployCEA { + function testSendUniversalTxToUEA_FullFlow_Native() public deployCEA { uint256 initialBalance = 1000 ether; fundCEAWithNative(initialBalance); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; - bytes memory payload = buildWithdrawPayload(address(0), withdrawAmount); + uint256 sendAmount = 500 ether; + bytes memory payload = buildSendToUEAPayload(address(0), sendAmount); uint256 balanceBefore = address(ceaInstance).balance; uint256 gatewayCallCountBefore = mockUniversalGateway.callCount(); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), withdrawAmount, false); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), sendAmount, false); - ceaInstance.executeUniversalTx{value: 0}( - - txID, - - universalTxID, - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, multicallPayload); // Verify all state changes assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); @@ -2258,39 +1575,36 @@ contract CEATest is Test { assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(0), "Token should be address(0) for native"); - assertEq(mockUniversalGateway.lastAmount(), withdrawAmount, "Amount should match"); - assertEq(mockUniversalGateway.lastValue(), withdrawAmount, "Gateway should receive correct value"); + assertEq(mockUniversalGateway.lastAmount(), sendAmount, "Amount should match"); + assertEq(mockUniversalGateway.lastValue(), sendAmount, "Gateway should receive correct value"); uint256 balanceAfter = address(ceaInstance).balance; - assertEq(balanceAfter, balanceBefore - withdrawAmount, "Balance should decrease"); + assertEq(balanceAfter, balanceBefore - sendAmount, "Balance should decrease"); } // ========================================================================= // executeUniversalTx ERC20 Gap Tests // ========================================================================= - function testExecuteUniversalTx_ERC20_RevertWhenMsgValueNotZero() public deployCEA { + function testExecuteUniversalTx_ERC20_MsgValueNonZero_ExcessStaysInCEA() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); vm.deal(vault, 1 ether); - bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); + TokenSpenderTarget spender = new TokenSpenderTarget(); + bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); - vm.expectRevert(Errors.InvalidAmount.selector); - bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - - ceaInstance.executeUniversalTx{value: 1 ether}( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, + bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - multicallPayload + uint256 ceaBalanceBefore = address(ceaInstance).balance; + ceaInstance.executeUniversalTx{value: 1 ether}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload ); + + // Excess ETH stays in CEA + assertEq(address(ceaInstance).balance, ceaBalanceBefore + 1 ether, "Excess msg.value stays in CEA"); } function testExecuteUniversalTx_ERC20_AllowanceRemainsZeroAfterRevert() public deployCEA { @@ -2303,24 +1617,19 @@ contract CEATest is Test { uint256 allowanceBefore = token.allowance(address(ceaInstance), address(reverter)); vm.prank(vault); - bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); + bytes memory multicallPayload = + buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); // Expect ExecutionFailed (revert data no longer bubbled) vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload); // Whole tx reverts so allowance is unchanged (stays at 0) - assertEq(token.allowance(address(ceaInstance), address(reverter)), allowanceBefore, "Allowance should revert to original"); + assertEq( + token.allowance(address(ceaInstance), address(reverter)), + allowanceBefore, + "Allowance should revert to original" + ); } // ========================================================================= @@ -2337,15 +1646,7 @@ contract CEATest is Test { bytes memory multicallPayload = buildNativeMulticallPayload(address(reverter), amount, bytes("")); ceaInstance.executeUniversalTx{value: amount}( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload - + generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload ); } @@ -2359,19 +1660,11 @@ contract CEATest is Test { vm.expectRevert(Errors.ExecutionFailed.selector); bytes memory multicallPayload = buildNativeMulticallPayload(address(reverter), amount, bytes("")); - ceaInstance.executeUniversalTx{value: amount}( - - txID, - - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload + ceaInstance.executeUniversalTx{value: amount}(txID, generateUniversalTxID(1), ueaOnPush, multicallPayload); + assertFalse( + CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked executed on failure" ); - - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked executed on failure"); } // ========================================================================= @@ -2379,26 +1672,16 @@ contract CEATest is Test { // ========================================================================= function testHandleSelfCalls_AcceptsValueWithSelfCall() public deployCEA { - // Fund CEA with native, execute self-call withdrawal + // Fund CEA with native, execute self-call sendUniversalTxToUEA fundCEAWithNative(500 ether); - bytes memory payload = buildWithdrawPayload(address(0), 500 ether); + bytes memory payload = buildSendToUEAPayload(address(0), 500 ether); vm.prank(vault); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 500 ether, false); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); // Note: msg.value must match total multicall values (0 in this case, as self-call doesn't need value) - ceaInstance.executeUniversalTx{value: 0}( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx{value: 0}(generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } @@ -2407,92 +1690,37 @@ contract CEATest is Test { fundCEAWithNative(100 ether); // Exactly 4 bytes (selector only) - abi.decode on empty payload[4:] will panic - bytes memory selectorOnly = abi.encodePacked(bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes,bytes)"))); + bytes memory selectorOnly = abi.encodePacked(bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes)"))); vm.prank(vault); vm.expectRevert(); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 0, false); - - ceaInstance.executeUniversalTx( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - multicallPayload - - ); + ceaInstance.executeUniversalTx(generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload); } function testHandleSelfCalls_RevertWhenArgsAreMalformed() public deployCEA { fundCEAWithNative(100 ether); // Correct selector but truncated args - bytes4 selector = bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes,bytes)")); + bytes4 selector = bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes)")); bytes memory malformed = abi.encodePacked(selector, bytes28(0)); vm.prank(vault); vm.expectRevert(); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(0), 0, false); - - ceaInstance.executeUniversalTx( - - generateTxID(1), + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload - - ); + ceaInstance.executeUniversalTx(generateTxID(1), generateUniversalTxID(1), ueaOnPush, multicallPayload); } - // ========================================================================= - // withdrawFundsToUEA: ERC20 approval failure - // ========================================================================= - - // REMOVED: Test expects CEA to detect when approve() returns false instead of reverting - // Per FAILED_TEST_ANALYSIS.md Category 4, this is documented as SDK responsibility - // Most modern ERC20s revert on failure; checking return values adds gas/complexity - // SDK can verify approval via allowance() call if needed for non-standard tokens - /* - function testWithdrawFundsToUEA_RevertWhenTokenApprovalFails() public deployCEA { - MockGasToken token = new MockGasToken(); - fundCEAWithTokens(address(token), 1000 ether); - - // Make approve return false (triggers _resetApproval or _safeApprove guard) - token.setWillSucceed(false); - - bytes memory payload = buildWithdrawPayload(address(token), 500 ether); - - vm.prank(vault); - vm.expectRevert(Errors.InvalidInput.selector); - bytes memory multicallPayload = buildWithdrawMulticallPayload(address(token), 500 ether, true); - - ceaInstance.executeUniversalTx( - - generateTxID(1), - - generateUniversalTxID(1), - - ueaOnPush, - - multicallPayload - - ); - } - */ - // ========================================================================= // General Invariants / Misc // ========================================================================= function testInitializeCEA_CannotBeCalledAgainAfterProxyDeployment() public deployCEA { vm.expectRevert(Errors.AlreadyInitialized.selector); - CEA(payable(address(ceaInstance))).initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); + CEA(payable(address(ceaInstance))) + .initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); } function testReceive_DirectETHTransferSucceeds() public deployCEA { @@ -2500,11 +1728,9 @@ contract CEATest is Test { vm.deal(address(this), amount); uint256 balanceBefore = address(ceaInstance).balance; - (bool success, ) = address(ceaInstance).call{value: amount}(""); + (bool success,) = address(ceaInstance).call{value: amount}(""); assertTrue(success, "Direct ETH transfer should succeed"); assertEq(address(ceaInstance).balance, balanceBefore + amount, "CEA balance should increase"); } } - - diff --git a/test/tests_cea/CEA_multicalls.t.sol b/test/tests_cea/CEA_multicalls.t.sol index 58756f3..83720a1 100644 --- a/test/tests_cea/CEA_multicalls.t.sol +++ b/test/tests_cea/CEA_multicalls.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import "./CEA.t.sol"; +import {MIGRATION_SELECTOR} from "../../src/libraries/Types.sol"; /** * @title CEA_NewMulticallTests @@ -9,7 +10,6 @@ import "./CEA.t.sol"; * @dev Tests organized per CEA_MULTICALL_TESTS.md requirements */ contract CEA_NewMulticallTests is CEATest { - // ========================================================================= // 1) Top-level executeUniversalTx validation cases // ========================================================================= @@ -22,7 +22,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory invalidPayload = "not a valid multicall"; vm.prank(vault); - vm.expectRevert(); // Will revert during abi.decode + vm.expectRevert(); // Will revert during abi.decode ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, invalidPayload); } @@ -63,7 +63,7 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 value = 0.1 ether; // Target requires exactly 0.1 ETH fee + uint256 value = 0.1 ether; // Target requires exactly 0.1 ETH fee vm.deal(vault, value); bytes memory targetCalldata = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -106,16 +106,11 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](2); // Step 1: Approve spender - calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(spender), 100 ether) - ); + calls[0] = + makeCall(address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(spender), 100 ether)); // Step 2: Call spender which uses the approval calls[1] = makeCall( - address(spender), - 0, - abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether) + address(spender), 0, abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether) ); bytes memory payload = buildExternalBatch(calls); @@ -136,11 +131,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildExternalSingleCall( - address(reverter), - 0, - abi.encodeWithSignature("revertWithReason()") - ); + bytes memory payload = + buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown @@ -175,11 +167,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildExternalSingleCall( - address(reverter), - 0, - abi.encodeWithSignature("revertWithReason()") - ); + bytes memory payload = + buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown @@ -195,11 +184,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildExternalSingleCall( - address(reverter), - 0, - abi.encodeWithSignature("revertWithReason()") - ); + bytes memory payload = + buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); vm.prank(vault); vm.recordLogs(); @@ -220,7 +206,7 @@ contract CEA_NewMulticallTests is CEATest { bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); - calls[0] = makeCall(address(ceaInstance), 0, "123"); // Only 3 bytes + calls[0] = makeCall(address(ceaInstance), 0, "123"); // Only 3 bytes bytes memory payload = encodeCalls(calls); @@ -230,7 +216,7 @@ contract CEA_NewMulticallTests is CEATest { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } - function test_RevertWhen_SelfCallSelectorNotWithdraw() public deployCEA { + function test_RevertWhen_SelfCallSelectorNotSendToUEA() public deployCEA { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -251,27 +237,25 @@ contract CEA_NewMulticallTests is CEATest { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } - function test_SelfCallWithdraw_ERC20_Success() public deployCEA { + function test_SelfCallSendToUEA_ERC20_Success() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; + uint256 sendAmount = 500 ether; - // Build multicall with approval + withdraw + // Build multicall with approval + sendToUEA Multicall[] memory calls = new Multicall[](3); calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 0) + address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 0) ); calls[1] = makeCall( address(token), 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), withdrawAmount) + abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), sendAmount) ); - calls[2] = buildSelfWithdrawCall(address(token), withdrawAmount); + calls[2] = buildSelfSendToUEACall(address(token), sendAmount); bytes memory payload = encodeCalls(calls); @@ -281,16 +265,16 @@ contract CEA_NewMulticallTests is CEATest { assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } - function test_RevertWhen_SelfCallWithdraw_InsufficientERC20Balance() public deployCEA { + function test_RevertWhen_SelfCallSendToUEA_InsufficientERC20Balance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 100 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; // More than balance + uint256 sendAmount = 500 ether; // More than balance Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(token), withdrawAmount); + calls[0] = buildSelfSendToUEACall(address(token), sendAmount); bytes memory payload = encodeCalls(calls); @@ -299,15 +283,15 @@ contract CEA_NewMulticallTests is CEATest { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } - function test_SelfCallWithdraw_Native_Success() public deployCEA { + function test_SelfCallSendToUEA_Native_Success() public deployCEA { fundCEAWithNative(1000 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; + uint256 sendAmount = 500 ether; Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(0), withdrawAmount); + calls[0] = buildSelfSendToUEACall(address(0), sendAmount); bytes memory payload = encodeCalls(calls); @@ -317,15 +301,15 @@ contract CEA_NewMulticallTests is CEATest { assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } - function test_RevertWhen_SelfCallWithdraw_InsufficientNativeBalance() public deployCEA { + function test_RevertWhen_SelfCallSendToUEA_InsufficientNativeBalance() public deployCEA { fundCEAWithNative(100 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 500 ether; // More than balance + uint256 sendAmount = 500 ether; // More than balance Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(0), withdrawAmount); + calls[0] = buildSelfSendToUEACall(address(0), sendAmount); bytes memory payload = encodeCalls(calls); @@ -348,14 +332,12 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](4); // External call calls[0] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); - // Approve for withdraw + // Approve for sendToUEA calls[1] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) + address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) ); - // Self-call withdraw - calls[2] = buildSelfWithdrawCall(address(token), 100 ether); + // Self-call sendToUEA + calls[2] = buildSelfSendToUEACall(address(token), 100 ether); // Another external call calls[3] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 99)); @@ -376,17 +358,11 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload1 = buildExternalSingleCall( - address(target), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 42) - ); + bytes memory payload1 = + buildExternalSingleCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); - bytes memory payload2 = buildExternalSingleCall( - address(target), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 99) - ); + bytes memory payload2 = + buildExternalSingleCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 99)); // First execution succeeds vm.prank(vault); @@ -403,11 +379,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID2 = generateTxID(2); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildExternalSingleCall( - address(target), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 42) - ); + bytes memory payload = + buildExternalSingleCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); // First execution vm.prank(vault); @@ -463,11 +436,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildExternalSingleCall( - address(malicious), - 0, - abi.encodeWithSignature("execute(bytes)", "") - ); + bytes memory payload = + buildExternalSingleCall(address(malicious), 0, abi.encodeWithSignature("execute(bytes)", "")); vm.prank(vault); // The malicious contract will try to reenter but should be blocked @@ -482,48 +452,52 @@ contract CEA_NewMulticallTests is CEATest { // 10) msg.value accounting (A - missing tests from TEST_ANALYSIS.md) // ========================================================================= - function test_RevertWhen_MsgValue_MismatchInMultiCall() public deployCEA { + function test_MsgValueLessThanSum_CEAHasPreExistingBalance_Succeeds() public deployCEA { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 value1 = 0.1 ether; - uint256 value2 = 0.2 ether; - uint256 totalValue = value1 + value2; + uint256 feePerCall = 0.1 ether; - vm.deal(vault, totalValue); + // Pre-fund CEA so it can cover the shortfall + fundCEAWithNative(0.1 ether); + + vm.deal(vault, 0.1 ether); Multicall[] memory calls = new Multicall[](2); - calls[0] = makeCall(address(target), value1, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 10)); - calls[1] = makeCall(address(target), value2, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 20)); + calls[0] = makeCall(address(target), feePerCall, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 10)); + calls[1] = makeCall(address(target), feePerCall, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 20)); bytes memory payload = encodeCalls(calls); - // Send wrong amount - less than sum + // msg.value (0.1) < sum of call values (0.2), but CEA has pre-existing 0.1 balance vm.prank(vault); - vm.expectRevert(Errors.InvalidAmount.selector); - ceaInstance.executeUniversalTx{value: value1}(txID, universalTxID, ueaOnPush, payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, payload); + + assertEq(target.magicNumber(), 20, "Final magic number should be 20"); } - function test_RevertWhen_MsgValue_ExceedsSumInMultiCall() public deployCEA { + function test_MsgValueExceedsSum_ExcessStaysInCEA() public deployCEA { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 value1 = 0.1 ether; - uint256 value2 = 0.2 ether; - uint256 totalValue = value1 + value2; + uint256 feePerCall = 0.1 ether; + uint256 totalValue = feePerCall * 2; + uint256 excess = 0.5 ether; - vm.deal(vault, totalValue + 1 ether); + vm.deal(vault, totalValue + excess); Multicall[] memory calls = new Multicall[](2); - calls[0] = makeCall(address(target), value1, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 10)); - calls[1] = makeCall(address(target), value2, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 20)); + calls[0] = makeCall(address(target), feePerCall, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 10)); + calls[1] = makeCall(address(target), feePerCall, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 20)); bytes memory payload = encodeCalls(calls); - // Send more than sum - ETH would be trapped + // Excess ETH stays in CEA (belongs to user's UEA) vm.prank(vault); - vm.expectRevert(Errors.InvalidAmount.selector); - ceaInstance.executeUniversalTx{value: totalValue + 0.5 ether}(txID, universalTxID, ueaOnPush, payload); + ceaInstance.executeUniversalTx{value: totalValue + excess}(txID, universalTxID, ueaOnPush, payload); + + assertEq(target.magicNumber(), 20, "Final magic number should be 20"); + assertEq(address(ceaInstance).balance, excess, "Excess ETH should remain in CEA"); } function test_SuccessWhen_MsgValue_MatchesSumExactly() public deployCEA { @@ -533,7 +507,7 @@ contract CEA_NewMulticallTests is CEATest { // Both calls require exactly 0.1 ETH each uint256 value1 = 0.1 ether; uint256 value2 = 0.1 ether; - uint256 totalValue = value1 + value2; // 0.2 ether + uint256 totalValue = value1 + value2; // 0.2 ether vm.deal(vault, totalValue); @@ -562,8 +536,8 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( address(ceaInstance), - 0.1 ether, // Non-zero value to self-call - abi.encodeWithSignature("sendUniversalTxToUEA(address,uint256,bytes,bytes)", address(0), 0.1 ether, "", "") + 0.1 ether, // Non-zero value to self-call + abi.encodeWithSignature("sendUniversalTxToUEA(address,uint256,bytes)", address(0), 0.1 ether, "") ); bytes memory payload = encodeCalls(calls); @@ -579,27 +553,27 @@ contract CEA_NewMulticallTests is CEATest { fundCEAWithTokens(address(token), 1000 ether); // Try to call sendUniversalTxToUEA directly (not through executeUniversalTx) - vm.expectRevert(Errors.NotVault.selector); - CEA(payable(address(ceaInstance))).sendUniversalTxToUEA(address(token), 100 ether); + vm.expectRevert(CommonErrors.Unauthorized.selector); + CEA(payable(address(ceaInstance))).sendUniversalTxToUEA(address(token), 100 ether, ""); } // ========================================================================= // 12) Gateway integration edge cases (C - missing tests from TEST_ANALYSIS.md) // ========================================================================= - function test_RevertWhen_WithdrawNative_InsufficientBalanceInBatch() public deployCEA { + function test_RevertWhen_SendNativeToUEA_InsufficientBalanceInBatch() public deployCEA { // Fund CEA with 0.5 ETH initially fundCEAWithNative(0.5 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - // Batch: external call first (uses 0.1 ETH), then withdraw more than remaining balance + // Batch: external call first (uses 0.1 ETH), then send more than remaining balance Multicall[] memory calls = new Multicall[](2); calls[0] = makeCall(address(target), 0.1 ether, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); // After first call, CEA has 0.5 ETH left (0.5 initial + 0.1 msg.value - 0.1 to target) - // Try to withdraw 1 ETH - should fail - calls[1] = buildSelfWithdrawCall(address(0), 1 ether); + // Try to send 1 ETH - should fail + calls[1] = buildSelfSendToUEACall(address(0), 1 ether); bytes memory payload = encodeCalls(calls); @@ -627,12 +601,10 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](2); // Approve gateway calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) + address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) ); - // Withdraw (gateway will revert) - calls[1] = buildSelfWithdrawCall(address(token), 100 ether); + // SendToUEA (gateway will revert) + calls[1] = buildSelfSendToUEACall(address(token), 100 ether); bytes memory payload = encodeCalls(calls); @@ -660,11 +632,8 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](2); // Approve spender - calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(spender), 100 ether) - ); + calls[0] = + makeCall(address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(spender), 100 ether)); // External call that fails calls[1] = makeCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); @@ -678,7 +647,7 @@ contract CEA_NewMulticallTests is CEATest { assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Allowance should be 0"); } - function test_RevertWhen_SelfWithdrawInMiddle_LaterCallFails_NoGatewayCall() public deployCEA { + function test_RevertWhen_SelfSendToUEAInMiddle_LaterCallFails_NoGatewayCall() public deployCEA { MockGasToken token = new MockGasToken(); RevertingTarget reverter = new RevertingTarget(); @@ -690,12 +659,10 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](3); // Approve gateway calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) + address(token), 0, abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), 100 ether) ); - // Self-withdraw - calls[1] = buildSelfWithdrawCall(address(token), 100 ether); + // Self-call sendToUEA + calls[1] = buildSelfSendToUEACall(address(token), 100 ether); // Later call fails calls[2] = makeCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); @@ -719,11 +686,8 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory failingPayload = buildExternalSingleCall( - address(reverter), - 0, - abi.encodeWithSignature("revertWithReason()") - ); + bytes memory failingPayload = + buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); // First attempt - should revert vm.prank(vault); @@ -734,11 +698,8 @@ contract CEA_NewMulticallTests is CEATest { assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked"); // Retry with same txID but different (succeeding) payload - bytes memory succeedingPayload = buildExternalSingleCall( - address(target), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 42) - ); + bytes memory succeedingPayload = + buildExternalSingleCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); vm.prank(vault); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, succeedingPayload); @@ -758,9 +719,7 @@ contract CEA_NewMulticallTests is CEATest { vm.deal(vault, value1 + value2); bytes memory payload = buildExternalSingleCall( - address(target), - value1, - abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42) + address(target), value1, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42) ); // First execution with value1 @@ -823,21 +782,21 @@ contract CEA_NewMulticallTests is CEATest { assertEq(eventIndex, 2, "Should have validated 2 events"); } - function test_Events_SelfCallWithdraw_EmittedCorrectly() public deployCEA { + function test_Events_SelfCallSendToUEA_EmittedCorrectly() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 withdrawAmount = 100 ether; + uint256 sendAmount = 100 ether; Multicall[] memory calls = new Multicall[](2); calls[0] = makeCall( address(token), 0, - abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), withdrawAmount) + abi.encodeWithSelector(IERC20.approve.selector, address(mockUniversalGateway), sendAmount) ); - calls[1] = buildSelfWithdrawCall(address(token), withdrawAmount); + calls[1] = buildSelfSendToUEACall(address(token), sendAmount); bytes memory payload = encodeCalls(calls); @@ -880,12 +839,13 @@ contract CEA_NewMulticallTests is CEATest { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - uint256 transferAmount = 0.1 ether; // Exact fee required by target + uint256 transferAmount = 0.1 ether; // Exact fee required by target vm.deal(vault, transferAmount); Multicall[] memory calls = new Multicall[](2); // Send native to target - calls[0] = makeCall(address(target), transferAmount, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); + calls[0] = + makeCall(address(target), transferAmount, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); // Later call reverts calls[1] = makeCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); @@ -901,7 +861,6 @@ contract CEA_NewMulticallTests is CEATest { assertEq(address(target).balance, targetBalanceBefore, "Target balance should not change due to rollback"); } - // ========================================================================= // 1) MULTICALL_SELECTOR Flow Tests // ========================================================================= @@ -914,15 +873,9 @@ contract CEA_NewMulticallTests is CEATest { // Create multicall with MULTICALL_SELECTOR prefix Multicall[] memory calls = new Multicall[](2); calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSignature("approve(address,uint256)", address(testTarget), 100 ether) - ); - calls[1] = makeCall( - address(testTarget), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 42) + address(token), 0, abi.encodeWithSignature("approve(address,uint256)", address(testTarget), 100 ether) ); + calls[1] = makeCall(address(testTarget), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); bytes memory payload = encodeCalls(calls); // This now includes MULTICALL_SELECTOR @@ -945,15 +898,9 @@ contract CEA_NewMulticallTests is CEATest { // Create multicall WITHOUT MULTICALL_SELECTOR (old format) Multicall[] memory calls = new Multicall[](2); calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSignature("approve(address,uint256)", address(testTarget), 100 ether) - ); - calls[1] = makeCall( - address(testTarget), - 0, - abi.encodeWithSignature("setMagicNumber(uint256)", 123) + address(token), 0, abi.encodeWithSignature("approve(address,uint256)", address(testTarget), 100 ether) ); + calls[1] = makeCall(address(testTarget), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 123)); // Encode without MULTICALL_SELECTOR (old way - direct abi.encode) bytes memory payload = abi.encode(calls); @@ -969,34 +916,31 @@ contract CEA_NewMulticallTests is CEATest { assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); } - function test_MulticallSelector_ValidatesMsgValue() public deployCEA { + function test_MulticallSelector_MsgValueExceeds_NoRevert() public deployCEA { + Target testTarget = new Target(); + Multicall[] memory calls = new Multicall[](1); - calls[0] = makeCall( - address(0x123), - 1 ether, - "" - ); + calls[0] = makeCall(address(testTarget), 0.1 ether, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); - bytes memory payload = encodeCalls(calls); // Includes MULTICALL_SELECTOR + bytes memory payload = encodeCalls(calls); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.deal(vault, 2 ether); vm.prank(vault); - vm.expectRevert(Errors.InvalidAmount.selector); ceaInstance.executeUniversalTx{value: 2 ether}(txID, universalTxID, ueaOnPush, payload); + + assertEq(testTarget.magicNumber(), 42, "Target should have magic number set"); + assertEq(address(ceaInstance).balance, 1.9 ether, "Excess should stay in CEA"); } function test_MulticallSelector_CorrectMsgValuePasses() public deployCEA { Target testTarget = new Target(); Multicall[] memory calls = new Multicall[](1); - calls[0] = makeCall( - address(testTarget), - 0.1 ether, - abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 999) - ); + calls[0] = + makeCall(address(testTarget), 0.1 ether, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 999)); bytes memory payload = encodeCalls(calls); // Includes MULTICALL_SELECTOR @@ -1018,10 +962,7 @@ contract CEA_NewMulticallTests is CEATest { function test_InvalidPayload_WithMulticallSelector_ButMalformedData() public deployCEA { // Payload with MULTICALL_SELECTOR but malformed data after it - bytes memory invalidPayload = abi.encodePacked( - MULTICALL_SELECTOR, - bytes("malformed data") - ); + bytes memory invalidPayload = abi.encodePacked(MULTICALL_SELECTOR, bytes("malformed data")); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -1090,13 +1031,9 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( address(ceaInstance), - 0.1 ether, // Non-zero value to self-call - should revert + 0.1 ether, // Non-zero value to self-call - should revert abi.encodeWithSignature( - "sendUniversalTxToUEA(address,uint256,bytes,bytes)", - address(token), - 100 ether, - "", - "" + "sendUniversalTxToUEA(address,uint256,bytes)", address(token), 100 ether, "" ) ); @@ -1117,18 +1054,12 @@ contract CEA_NewMulticallTests is CEATest { Multicall[] memory calls = new Multicall[](2); calls[0] = makeCall( - address(token), - 0, - abi.encodeWithSignature("approve(address,uint256)", universalGateway, 100 ether) + address(token), 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, 100 ether) ); calls[1] = makeCall( address(ceaInstance), - 0, // Zero value to self-call - OK - abi.encodeWithSignature( - "sendUniversalTxToUEA(address,uint256)", - address(token), - 100 ether - ) + 0, // Zero value to self-call - OK + abi.encodeWithSignature("sendUniversalTxToUEA(address,uint256,bytes)", address(token), 100 ether, "") ); bytes memory payload = encodeCalls(calls); @@ -1141,4 +1072,89 @@ contract CEA_NewMulticallTests is CEATest { assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); } + + // ========================================================================= + // msg.value relaxation tests (CEA can spend pre-existing balance) + // ========================================================================= + + function test_MsgValueZero_CEAPreExistingBalance_Succeeds() public deployCEA { + // CEA has pre-existing ETH, Vault sends msg.value=0 + fundCEAWithNative(1 ether); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall(address(target), 0.1 ether, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); + + bytes memory payload = encodeCalls(calls); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, payload + ); + + assertEq(target.magicNumber(), 42, "Target should have magic number set"); + assertEq(address(ceaInstance).balance, 0.9 ether, "CEA should have 0.9 ETH remaining"); + } + + function test_MsgValueGtTotalCallValues_ExcessStaysInCEA() public deployCEA { + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall(address(target), 0.1 ether, abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42)); + + bytes memory payload = encodeCalls(calls); + + vm.deal(vault, 1 ether); + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 1 ether}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, payload + ); + + assertEq(target.magicNumber(), 42, "Target should have magic number set"); + assertEq(address(ceaInstance).balance, 0.9 ether, "Excess ETH should stay in CEA"); + } + + // ========================================================================= + // Migration-in-multicall prevention tests + // ========================================================================= + + function test_MigrationSelectorInMulticall_Reverts() public deployCEA { + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); + calls[1] = makeCall(address(ceaInstance), 0, abi.encodePacked(MIGRATION_SELECTOR)); + + bytes memory payload = encodeCalls(calls); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, payload + ); + } + + function test_MigrationSelectorAtPositionZero_Reverts() public deployCEA { + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall(address(ceaInstance), 0, abi.encodePacked(MIGRATION_SELECTOR)); + calls[1] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); + + bytes memory payload = encodeCalls(calls); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, payload + ); + } + + function test_MigrationSelectorAtLastPosition_Reverts() public deployCEA { + Multicall[] memory calls = new Multicall[](3); + calls[0] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 10)); + calls[1] = makeCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 20)); + calls[2] = makeCall(address(ceaInstance), 0, abi.encodePacked(MIGRATION_SELECTOR)); + + bytes memory payload = encodeCalls(calls); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, payload + ); + } } diff --git a/test/tests_cea/CEA_selfCalls.t.sol b/test/tests_cea/CEA_selfCalls.t.sol index 62fb78a..f0b5626 100644 --- a/test/tests_cea/CEA_selfCalls.t.sol +++ b/test/tests_cea/CEA_selfCalls.t.sol @@ -12,7 +12,7 @@ import {Target} from "../../src/mocks/Target.sol"; contract CEA_ComprehensiveTests is CEATest { // Event declaration (from ICEA interface) - event WithdrawalToUEA(address indexed _cea, address indexed _uea, address indexed token, uint256 amount); + event UniversalTxToUEA(address indexed _cea, address indexed _uea, address indexed token, uint256 amount); // ========================================================================= // 1) UniversalTxRequest Field Verification Tests @@ -32,7 +32,7 @@ contract CEA_ComprehensiveTests is CEATest { 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, amount) ); - calls[1] = buildSelfWithdrawCall(address(token), amount); + calls[1] = buildSelfSendToUEACall(address(token), amount); bytes memory payload = encodeCalls(calls); @@ -52,7 +52,7 @@ contract CEA_ComprehensiveTests is CEATest { uint256 amount = 500 ether; Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(0), amount); + calls[0] = buildSelfSendToUEACall(address(0), amount); bytes memory payload = encodeCalls(calls); @@ -78,7 +78,7 @@ contract CEA_ComprehensiveTests is CEATest { 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, amount) ); - calls[1] = buildSelfWithdrawCall(address(token), amount); + calls[1] = buildSelfSendToUEACall(address(token), amount); bytes memory payload = encodeCalls(calls); @@ -104,7 +104,7 @@ contract CEA_ComprehensiveTests is CEATest { 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, amount) ); - calls[1] = buildSelfWithdrawCall(address(token), amount); + calls[1] = buildSelfSendToUEACall(address(token), amount); bytes memory payload = encodeCalls(calls); @@ -135,7 +135,7 @@ contract CEA_ComprehensiveTests is CEATest { 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, totalBalance) ); - calls[1] = buildSelfWithdrawCall(address(token), totalBalance); + calls[1] = buildSelfSendToUEACall(address(token), totalBalance); bytes memory payload = encodeCalls(calls); @@ -162,7 +162,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(0), totalBalance); + calls[0] = buildSelfSendToUEACall(address(0), totalBalance); bytes memory payload = encodeCalls(calls); @@ -199,7 +199,7 @@ contract CEA_ComprehensiveTests is CEATest { 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, minAmount) ); - calls[1] = buildSelfWithdrawCall(address(token), minAmount); + calls[1] = buildSelfSendToUEACall(address(token), minAmount); bytes memory payload = encodeCalls(calls); @@ -218,7 +218,7 @@ contract CEA_ComprehensiveTests is CEATest { uint256 minAmount = 1 wei; Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(0), minAmount); + calls[0] = buildSelfSendToUEACall(address(0), minAmount); bytes memory payload = encodeCalls(calls); @@ -241,21 +241,21 @@ contract CEA_ComprehensiveTests is CEATest { bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](4); - // First withdrawal + // First send calls[0] = makeCall( address(token), 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, 500 ether) ); - calls[1] = buildSelfWithdrawCall(address(token), 500 ether); + calls[1] = buildSelfSendToUEACall(address(token), 500 ether); - // Second withdrawal + // Second send calls[2] = makeCall( address(token), 0, abi.encodeWithSignature("approve(address,uint256)", universalGateway, 300 ether) ); - calls[3] = buildSelfWithdrawCall(address(token), 300 ether); + calls[3] = buildSelfSendToUEACall(address(token), 300 ether); bytes memory payload = encodeCalls(calls); @@ -287,7 +287,7 @@ contract CEA_ComprehensiveTests is CEATest { abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42) ); // Second call: Send 0.4 ETH to UEA (0.4 initial balance remains after first call) - calls[1] = buildSelfWithdrawCall(address(0), 0.4 ether); + calls[1] = buildSelfSendToUEACall(address(0), 0.4 ether); bytes memory payload = encodeCalls(calls); @@ -316,7 +316,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); - calls[0] = buildSelfWithdrawCall(address(token), type(uint256).max); + calls[0] = buildSelfSendToUEACall(address(token), type(uint256).max); bytes memory payload = encodeCalls(calls); @@ -325,4 +325,657 @@ contract CEA_ComprehensiveTests is CEATest { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } + // ========================================================================= + // ========================================================================= + // SelfCalls: FUNDS_AND_PAYLOAD + // ========================================================================= + // ========================================================================= + + // ========================================================================= + // 1) Access Control & Call-Path Integrity (Self-call Only via CEA) + // ========================================================================= + + function test_FundsAndPayload_RevertWhen_DirectCall_NotSelfCall() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + vm.expectRevert(CommonErrors.Unauthorized.selector); + CEA(payable(address(ceaInstance))).sendUniversalTxToUEA( + address(token), 500 ether, ueaPayload + ); + } + + function test_FundsAndPayload_RevertWhen_CalledByVaultDirectly() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + vm.prank(vault); + vm.expectRevert(CommonErrors.Unauthorized.selector); + CEA(payable(address(ceaInstance))).sendUniversalTxToUEA( + address(token), 500 ether, ueaPayload + ); + } + + function test_FundsAndPayload_RevertWhen_CalledByEOA() public deployCEA { + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + vm.prank(makeAddr("randomEOA")); + vm.expectRevert(CommonErrors.Unauthorized.selector); + CEA(payable(address(ceaInstance))).sendUniversalTxToUEA( + address(0), 1 ether, ueaPayload + ); + } + + // ========================================================================= + // 2) Input Validation (Amount / Token / Payload) + // ========================================================================= + + function test_FundsAndPayload_RevertWhen_ZeroAmount_ERC20() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), 0, ueaPayload) + ); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + function test_FundsAndPayload_RevertWhen_ZeroAmount_Native() public deployCEA { + fundCEAWithNative(1 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), 0, ueaPayload) + ); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + function test_FundsAndPayload_RevertWhen_InsufficientNativeBalance() public deployCEA { + fundCEAWithNative(0.1 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), 1 ether, ueaPayload) + ); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + function test_FundsAndPayload_RevertWhen_InsufficientERC20Balance() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 100 ether); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), 500 ether) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), 500 ether, ueaPayload) + ); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + // ========================================================================= + // 3) Correct Gateway Function Selection (Core Behavior) + // ========================================================================= + + function test_FundsAndPayload_Native_CallsSendUniversalTxViaCEA() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "Should call sendUniversalTxViaCEA for non-empty payload"); + assertEq(mockUniversalGateway.viaCEACallCount(), 1, "viaCEA call count should be 1"); + } + + function test_FundsAndPayload_ERC20_CallsSendUniversalTxViaCEA() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "Should call sendUniversalTxViaCEA for non-empty payload"); + } + + function test_FundsOnly_Native_CallsSendUniversalTxViaCEA() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + + Multicall[] memory calls = new Multicall[](1); + calls[0] = buildSelfSendToUEACall(address(0), amount); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "Should call sendUniversalTxViaCEA for all tx types"); + assertEq(mockUniversalGateway.viaCEACallCount(), 1, "viaCEA call count should be 1"); + } + + function test_FundsOnly_ERC20_CallsSendUniversalTxViaCEA() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = buildSelfSendToUEACall(address(token), amount); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "Should call sendUniversalTxViaCEA for all tx types"); + } + + // ========================================================================= + // 4) Correct Request Construction (Req Field-Level Assertions) + // ========================================================================= + + function test_FundsAndPayload_ReqFieldsCorrect_Native() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "req.recipient should be UEA"); + assertEq(req.token, address(0), "req.token should be address(0) for native"); + assertEq(req.amount, amount, "req.amount should match"); + assertEq(req.payload, ueaPayload, "req.payload should match the passed-in payload"); + assertEq(req.signatureData.length, 0, "req.signatureData should be empty"); + assertEq(req.revertInstruction.fundRecipient, ueaOnPush, "fundRecipient should be UEA"); + assertEq(req.revertInstruction.revertMsg, "", "revertMsg should be empty"); + } + + function test_FundsAndPayload_ReqFieldsCorrect_ERC20() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = hex"deadbeef"; + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "req.recipient should be UEA"); + assertEq(req.token, address(token), "req.token should match token"); + assertEq(req.amount, amount, "req.amount should match"); + assertEq(req.payload, ueaPayload, "req.payload should match exact bytes"); + assertEq(req.signatureData.length, 0, "req.signatureData should be empty"); + assertEq(req.revertInstruction.fundRecipient, ueaOnPush, "fundRecipient should be UEA"); + } + + function test_FundsOnly_ReqPayloadIsEmpty() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + + Multicall[] memory calls = new Multicall[](1); + calls[0] = buildSelfSendToUEACall(address(0), amount); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.payload.length, 0, "FUNDS-only: req.payload must be empty"); + } + + // ========================================================================= + // 5) Value Semantics (Native vs ERC20) + // ========================================================================= + + function test_FundsAndPayload_Native_MsgValueForwardedCorrectly() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertEq(mockUniversalGateway.lastValue(), amount, "Native FUNDS_AND_PAYLOAD: msg.value should equal amount"); + } + + function test_FundsAndPayload_ERC20_MsgValueIsZero() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertEq(mockUniversalGateway.lastValue(), 0, "ERC20 FUNDS_AND_PAYLOAD: msg.value should be 0"); + } + + function test_FundsAndPayload_RevertWhen_SelfCallHasNonZeroValue() public deployCEA { + fundCEAWithNative(10 ether); + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), + 1 ether, // Non-zero value to self-call + buildSendToUEAPayloadWithData(address(0), 5 ether, ueaPayload) + ); + + vm.deal(vault, 1 ether); + vm.prank(vault); + vm.expectRevert(Errors.InvalidInput.selector); + ceaInstance.executeUniversalTx{value: 1 ether}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + // ========================================================================= + // 6) Approval / Allowance Behavior (ERC20 Path) + // ========================================================================= + + function test_FundsAndPayload_ERC20_SucceedsWithApproval() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "Should use viaCEA path"); + } + + function test_FundsAndPayload_ERC20_ApproveAndResetAllowance() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](3); + // Approve + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + // Send funds + payload + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + // Reset allowance to 0 + calls[2] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), 0) + ); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 0, + "Allowance should be reset to 0" + ); + } + + // ========================================================================= + // 7) Event Emission (CEA-Side) + // ========================================================================= + + function test_FundsAndPayload_EmitsUniversalTxToUEA_Native() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), amount, ueaPayload) + ); + + vm.prank(vault); + vm.expectEmit(true, true, true, true); + emit UniversalTxToUEA(address(ceaInstance), ueaOnPush, address(0), amount); + + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + function test_FundsAndPayload_EmitsUniversalTxToUEA_ERC20() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + vm.prank(vault); + vm.expectEmit(true, true, true, true); + emit UniversalTxToUEA(address(ceaInstance), ueaOnPush, address(token), amount); + + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + } + + // ========================================================================= + // 8) Failure Modes & Revert Surfacing + // ========================================================================= + + function test_FundsAndPayload_RevertWhen_GatewayReverts() public deployCEA { + fundCEAWithNative(10 ether); + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + mockUniversalGateway.setWillRevert(true, "GatewayError"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), 5 ether, ueaPayload) + ); + + bytes32 txID = generateTxID(1); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx{value: 0}( + txID, generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertFalse( + CEA(payable(address(ceaInstance))).isExecuted(txID), + "txID should NOT be marked executed on gateway revert" + ); + } + + function test_FundsAndPayload_MulticallRevert_NoPartialEffects() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + fundCEAWithNative(10 ether); + + Target payableTarget = new Target(); + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + // Gateway reverts on second call + mockUniversalGateway.setWillRevert(true, "GatewayError"); + + Multicall[] memory calls = new Multicall[](2); + // First call: set magic number (should succeed in isolation) + calls[0] = makeCall( + address(payableTarget), 0, + abi.encodeWithSignature("setMagicNumber(uint256)", 42) + ); + // Second call: sendUniversalTxToUEA with payload (gateway will revert) + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), 5 ether, ueaPayload) + ); + + vm.prank(vault); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx{value: 0}( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + // Magic number should NOT be set because whole multicall reverted + assertEq(payableTarget.getMagicNumber(), 0, "No partial effects on revert"); + } + + // ========================================================================= + // 9) End-to-End "CEA-only" Simulation (Using Mock Gateway) + // ========================================================================= + + function test_E2E_NativeFundsOnly() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + + Multicall[] memory calls = new Multicall[](1); + calls[0] = buildSelfSendToUEACall(address(0), amount); + + bytes32 txID = generateTxID(1); + bytes32 universalTxID = generateUniversalTxID(1); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + txID, universalTxID, ueaOnPush, encodeCalls(calls) + ); + + // Assertions + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS-only should use sendUniversalTxViaCEA"); + assertEq(mockUniversalGateway.lastValue(), amount, "msg.value should match amount"); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "recipient == UEA"); + assertEq(req.token, address(0), "token == address(0)"); + assertEq(req.amount, amount, "amount correct"); + assertEq(req.payload.length, 0, "payload empty"); + + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID marked executed"); + assertEq(address(ceaInstance).balance, 5 ether, "CEA balance decreased"); + } + + function test_E2E_ERC20FundsOnly() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = buildSelfSendToUEACall(address(token), amount); + + bytes32 txID = generateTxID(1); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + txID, generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS-only should use sendUniversalTxViaCEA"); + assertEq(mockUniversalGateway.lastValue(), 0, "msg.value should be 0 for ERC20"); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "recipient == UEA"); + assertEq(req.amount, amount, "amount correct"); + assertEq(req.payload.length, 0, "payload empty"); + } + + function test_E2E_NativeFundsAndPayload() public deployCEA { + fundCEAWithNative(10 ether); + uint256 amount = 5 ether; + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(0), amount, ueaPayload) + ); + + bytes32 txID = generateTxID(1); + + vm.prank(vault); + ceaInstance.executeUniversalTx{value: 0}( + txID, generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS_AND_PAYLOAD should use sendUniversalTxViaCEA"); + assertEq(mockUniversalGateway.lastValue(), amount, "msg.value should match amount"); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "recipient == UEA"); + assertEq(req.amount, amount, "amount correct"); + assertEq(req.payload, ueaPayload, "payload matches"); + + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID marked executed"); + assertEq(address(ceaInstance).balance, 5 ether, "CEA balance decreased"); + } + + function test_E2E_ERC20FundsAndPayload() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 1000 ether); + uint256 amount = 500 ether; + bytes memory ueaPayload = hex"cafebabe"; + + Multicall[] memory calls = new Multicall[](2); + calls[0] = makeCall( + address(token), 0, + abi.encodeWithSignature("approve(address,uint256)", address(mockUniversalGateway), amount) + ); + calls[1] = makeCall( + address(ceaInstance), 0, + buildSendToUEAPayloadWithData(address(token), amount, ueaPayload) + ); + + bytes32 txID = generateTxID(1); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + txID, generateUniversalTxID(1), ueaOnPush, encodeCalls(calls) + ); + + assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS_AND_PAYLOAD should use sendUniversalTxViaCEA"); + assertEq(mockUniversalGateway.lastValue(), 0, "msg.value should be 0 for ERC20"); + + UniversalTxRequest memory req = mockUniversalGateway.getLastRequest(); + assertEq(req.recipient, ueaOnPush, "recipient == UEA"); + assertEq(req.token, address(token), "token correct"); + assertEq(req.amount, amount, "amount correct"); + assertEq(req.payload, ueaPayload, "payload matches exact bytes"); + } + } diff --git a/test/tests_ceaMigration/CEAMigration_Integration.t.sol b/test/tests_ceaMigration/CEAMigration_Integration.t.sol index 683af26..7a71c13 100644 --- a/test/tests_ceaMigration/CEAMigration_Integration.t.sol +++ b/test/tests_ceaMigration/CEAMigration_Integration.t.sol @@ -86,15 +86,8 @@ contract CEAMigration_IntegrationTest is Test { return keccak256(abi.encodePacked("universalTxID", nonce)); } - function buildMigrationPayload(address ceaProxy) internal pure returns (bytes memory) { - Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: ceaProxy, - value: 0, - data: abi.encodePacked(MIGRATION_SELECTOR) - }); - - return abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); + function buildMigrationPayload(address) internal pure returns (bytes memory) { + return abi.encodePacked(MIGRATION_SELECTOR); } function executeMigration() internal { diff --git a/test/tests_ceaMigration/CEA_Migration.t.sol b/test/tests_ceaMigration/CEA_Migration.t.sol index a65e367..7b6a569 100644 --- a/test/tests_ceaMigration/CEA_Migration.t.sol +++ b/test/tests_ceaMigration/CEA_Migration.t.sol @@ -75,15 +75,8 @@ contract CEA_MigrationTest is Test { return keccak256(abi.encodePacked("universalTxID", nonce)); } - function buildMigrationPayload(address ceaProxy) internal pure returns (bytes memory) { - Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: ceaProxy, - value: 0, - data: abi.encodePacked(MIGRATION_SELECTOR) - }); - - return abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); + function buildMigrationPayload(address) internal pure returns (bytes memory) { + return abi.encodePacked(MIGRATION_SELECTOR); } // ========================================================================= @@ -132,52 +125,58 @@ contract CEA_MigrationTest is Test { // _handleMigration Validation Tests // ========================================================================= - function test_handleMigration_WrongTarget() public { + function test_handleMigration_TopLevelFormat_Succeeds() public { // Set migration contract factory.setCEAMigrationContract(address(migration)); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - // Build payload targeting wrong address - address wrongTarget = makeAddr("wrongTarget"); - Multicall[] memory calls = new Multicall[](1); - calls[0] = Multicall({ - to: wrongTarget, // Wrong target! - value: 0, - data: abi.encodePacked(MIGRATION_SELECTOR) - }); - bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); + // Top-level MIGRATION_SELECTOR (no Multicall wrapper) + bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); - // Expect InvalidTarget revert vm.prank(vault); - vm.expectRevert(Errors.InvalidTarget.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); + + // Verify implementation changed + address implAfter = CEAProxy(payable(address(ceaInstance))).getImplementation(); + assertEq(implAfter, migration.CEA_IMPLEMENTATION(), "Implementation should be CEA v2"); + } + + function test_handleMigration_NonZeroMsgValue_Reverts() public { + factory.setCEAMigrationContract(address(migration)); + + bytes32 txID = generateTxID(1); + bytes32 universalTxID = generateUniversalTxID(1); + bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); + + vm.deal(vault, 1 ether); + + vm.prank(vault); + vm.expectRevert(Errors.InvalidInput.selector); + ceaInstance.executeUniversalTx{value: 1 ether}(txID, universalTxID, ueaOnPush, payload); } - function test_handleMigration_NonZeroValue() public { + function test_handleMigration_MigrationInsideMulticall_Reverts() public { // Set migration contract factory.setCEAMigrationContract(address(migration)); bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - // Build payload with non-zero value + // MIGRATION_SELECTOR wrapped in multicall fails as generic execution failure + // (CEA has no function matching the migration selector, so .call() reverts) Multicall[] memory calls = new Multicall[](1); calls[0] = Multicall({ to: address(ceaInstance), - value: 1 ether, // Non-zero value! + value: 0, data: abi.encodePacked(MIGRATION_SELECTOR) }); bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); - // Fund vault with ether to send - vm.deal(vault, 10 ether); - - // Expect InvalidInput revert vm.prank(vault); - vm.expectRevert(Errors.InvalidInput.selector); - ceaInstance.executeUniversalTx{value: 1 ether}(txID, universalTxID, ueaOnPush, payload); + vm.expectRevert(Errors.ExecutionFailed.selector); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } function test_handleMigration_NoMigrationContract() public { @@ -220,9 +219,9 @@ contract CEA_MigrationTest is Test { }); bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); - // Expect InvalidCall revert (migration must be standalone) + // Migration selector in multicall fails as generic execution failure vm.prank(vault); - vm.expectRevert(Errors.InvalidCall.selector); + vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); } @@ -247,9 +246,9 @@ contract CEA_MigrationTest is Test { }); bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); - // Expect InvalidCall revert (migration must be standalone) + // Migration selector in multicall fails as generic execution failure vm.prank(vault); - vm.expectRevert(Errors.InvalidCall.selector); + vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, payload); }