From fdb936c3e9839c4f052544c28dd17785eb9769d0 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 19 Mar 2026 19:03:18 +0400 Subject: [PATCH 01/56] sendUniversalTxToUEA allows zero amount --- docs/addresses/bsc_testnet.md | 24 +++++++------------- src/cea/CEA.sol | 1 - test/fuzz/CEA_Fuzz.t.sol | 35 +++++++++++++++++++++++------- test/tests_cea/CEA.t.sol | 31 +++++++++++++------------- test/tests_cea/CEA_selfCalls.t.sol | 12 ++++++---- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/docs/addresses/bsc_testnet.md b/docs/addresses/bsc_testnet.md index cccbda3..2f5bfce 100644 --- a/docs/addresses/bsc_testnet.md +++ b/docs/addresses/bsc_testnet.md @@ -1,19 +1,11 @@ # BSC Testnet (Chain ID: 97) - | Contract | Address | - |---|---| - | CEA (logic) | `0x60A4140429446E515aB15A02f8FB46F7c81fE3b2` | - | CEAProxy (clone template) | `0xcF66462F97daea2c0Ef549c542D54C6F4dc3e8B1` | - | CEAFactory (implementation) | `0x95E6c8aACe87e8401cd4bDFF1B9DaC53059C8808` | - | ProxyAdmin | `0xBD083Bf209D8c791aF71E96ace5aF67D15d83Cef` | - | CEAFactory (proxy) | `0xac52b7be327C1e6A617937CFfE90269aDccD211d` | - -## New Latest CEA Contracts for BSC - - | Contract | Address | - |---|---| - | CEA (logic) | `0xa66C8832bB97203E07B65d876d2ceAe3801709B6` | - | CEAProxy (clone template) | `0xBDF06996BA23AE797a4aA9C8C5994D313D763a7c` | + | Contract | Address | + | --------------------------- | -------------------------------------------- | + | CEA (logic) | `0x2fAB4C65529f3b18756f943b56eFf01772457304` | + | CEAProxy (clone template) | `0xBDF06996BA23AE797a4aA9C8C5994D313D763a7c` | | CEAFactory (implementation) | `0xC0D35725Dd054B09931740DC231cDea89B0FEd3b` | - | ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | - | CEAFactory (proxy) | `0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719` | + | ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | + | CEAFactory (proxy) | `0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719` | + | CEA_V2 (logic) | `0x97Fd78453D4741E28dFdd6b2f7BD2a4bA0b9E04E` | + | CEAMigration | `0x2cc02Ab38367684b0A90e9a09e6216540bBe3F52` | diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 6b23493..a3b1f7b 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -114,7 +114,6 @@ contract CEA is ICEA, ReentrancyGuard { if (msg.sender != address(this)) { revert CommonErrors.Unauthorized(); } - if (amount == 0) revert CEAErrors.InvalidInput(); if (revertRecipient == address(0)) { revert CEAErrors.InvalidInput(); } diff --git a/test/fuzz/CEA_Fuzz.t.sol b/test/fuzz/CEA_Fuzz.t.sol index a1cef1a..c87cb46 100644 --- a/test/fuzz/CEA_Fuzz.t.sol +++ b/test/fuzz/CEA_Fuzz.t.sol @@ -384,26 +384,45 @@ contract CEA_FuzzTest is Test { // 8.7 SendUniversalTxToUEA Properties // ========================================================================= - /// @dev amount == 0 always reverts with InvalidInput. - function testFuzz_sendUniversalTxToUEA_zeroAmount_reverts(address token) public { - // Must call as self to bypass Unauthorized, but amount == 0 still reverts - // We can test this via a multicall that calls sendUniversalTxToUEA(token, 0, "") + /// @dev amount == 0 is allowed for both native and ERC20 — no revert expected. + function testFuzz_sendUniversalTxToUEA_zeroAmount_succeeds_native() public { Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( address(ceaInstance), 0, abi.encodeWithSignature( - "sendUniversalTxToUEA(address,uint256,bytes,address)", token, uint256(0), "", ueaOnPush + "sendUniversalTxToUEA(address,uint256,bytes,address)", address(0), uint256(0), "", ueaOnPush ) ); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("zero_amount", token)); + bytes32 txId = keccak256(abi.encode("zero_amount_native")); - // The inner call reverts with InvalidInput, causing ExecutionFailed at multicall level - vm.expectRevert(CEAErrors.ExecutionFailed.selector); vm.prank(vault); ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for native"); + } + + function testFuzz_sendUniversalTxToUEA_zeroAmount_succeeds_erc20() public { + MockGasToken token = new MockGasToken(); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = makeCall( + address(ceaInstance), + 0, + abi.encodeWithSignature( + "sendUniversalTxToUEA(address,uint256,bytes,address)", address(token), uint256(0), "", ueaOnPush + ) + ); + + bytes memory payload = encodeCalls(calls); + bytes32 txId = keccak256(abi.encode("zero_amount_erc20")); + + vm.prank(vault); + ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for ERC20"); } /// @dev When CEA lacks sufficient ERC20 balance, reverts with InsufficientBalance. diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 8fec249..027c20c 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -1189,14 +1189,13 @@ contract CEATest is Test { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildSendToUEAPayload(address(token), 0, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 0, true); - // Zero amount sends revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) - vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for ERC20"); } function testSendUniversalTxToUEA_MultipleSendsWithDifferentTxIDs_ERC20() public deployCEA { @@ -1532,14 +1531,13 @@ contract CEATest is Test { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); - bytes memory payload = buildSendToUEAPayload(address(0), 0, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - // Zero amount sends revert with ExecutionFailed (bubbled from sendUniversalTxToUEA's InvalidInput) - vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for native"); } function testSendUniversalTxToUEA_MultipleSendsWithDifferentTxIDs_Native() public deployCEA { @@ -1706,15 +1704,17 @@ contract CEATest is Test { function testHandleSelfCalls_RevertWhenPayloadExactly4Bytes() public deployCEA { 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)"))); + // Exactly 4 bytes (selector only) — abi.decode on empty payload[4:] will panic + bytes memory selectorOnly = + abi.encodePacked(bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes,address)"))); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = Multicall({to: address(ceaInstance), value: 0, data: selectorOnly}); vm.prank(vault); vm.expectRevert(); - bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - ceaInstance.executeUniversalTx( - generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), multicallPayload + generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); } @@ -1722,15 +1722,16 @@ contract CEATest is Test { fundCEAWithNative(100 ether); // Correct selector but truncated args - bytes4 selector = bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes)")); + bytes4 selector = bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes,address)")); bytes memory malformed = abi.encodePacked(selector, bytes28(0)); + Multicall[] memory calls = new Multicall[](1); + calls[0] = Multicall({to: address(ceaInstance), value: 0, data: malformed}); + vm.prank(vault); vm.expectRevert(); - bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - ceaInstance.executeUniversalTx( - generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), multicallPayload + generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); } diff --git a/test/tests_cea/CEA_selfCalls.t.sol b/test/tests_cea/CEA_selfCalls.t.sol index 8066a68..4e0ef28 100644 --- a/test/tests_cea/CEA_selfCalls.t.sol +++ b/test/tests_cea/CEA_selfCalls.t.sol @@ -347,7 +347,7 @@ contract CEA_ComprehensiveTests is CEATest { // 2) Input Validation (Amount / Token / Payload) // ========================================================================= - function test_FundsAndPayload_RevertWhen_ZeroAmount_ERC20() public deployCEA { + function test_FundsAndPayload_ZeroAmount_ERC20_Succeeds() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); @@ -358,13 +358,15 @@ contract CEA_ComprehensiveTests is CEATest { makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(address(token), 0, ueaPayload, ueaOnPush)); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for ERC20"); + assertEq(mockUniversalGateway.lastToken(), address(token), "Token should match"); } - function test_FundsAndPayload_RevertWhen_ZeroAmount_Native() public deployCEA { + function test_FundsAndPayload_ZeroAmount_Native_Succeeds() public deployCEA { fundCEAWithNative(1 ether); bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); @@ -374,10 +376,12 @@ contract CEA_ComprehensiveTests is CEATest { makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(address(0), 0, ueaPayload, ueaOnPush)); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx{value: 0}( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for native"); + assertEq(mockUniversalGateway.lastToken(), address(0), "Token should be zero address for native"); } function test_FundsAndPayload_RevertWhen_InsufficientNativeBalance() public deployCEA { From 9d366226fb8a7d6c9e08f0f35689fc7f5161ea1a Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 19 Mar 2026 19:20:59 +0400 Subject: [PATCH 02/56] sendUniversalTxToUEA allows zero amount --- docs/addresses/bsc_testnet.md | 2 +- src/cea/CEA.sol | 18 +++++++++++------- test/tests_cea/CEA_selfCalls.t.sol | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/addresses/bsc_testnet.md b/docs/addresses/bsc_testnet.md index 2f5bfce..304914b 100644 --- a/docs/addresses/bsc_testnet.md +++ b/docs/addresses/bsc_testnet.md @@ -2,7 +2,7 @@ | Contract | Address | | --------------------------- | -------------------------------------------- | - | CEA (logic) | `0x2fAB4C65529f3b18756f943b56eFf01772457304` | + | CEA (logic) | `0xdC3A3a18a17EB4FDa9cF34a8CEee8540e6F2b5Fd` | | CEAProxy (clone template) | `0xBDF06996BA23AE797a4aA9C8C5994D313D763a7c` | | CEAFactory (implementation) | `0xC0D35725Dd054B09931740DC231cDea89B0FEd3b` | | ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index a3b1f7b..0a30373 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -127,15 +127,19 @@ contract CEA is ICEA, ReentrancyGuard { signatureData: "" }); - if (token == address(0)) { - if (address(this).balance < amount) { - revert CEAErrors.InsufficientBalance(); + if (amount > 0) { + if (token == address(0)) { + if (address(this).balance < amount) { + revert CEAErrors.InsufficientBalance(); + } + IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA{value: amount}(req); + } else { + if (IERC20(token).balanceOf(address(this)) < amount) { + revert CEAErrors.InsufficientBalance(); + } + IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA(req); } - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA{value: amount}(req); } else { - if (IERC20(token).balanceOf(address(this)) < amount) { - revert CEAErrors.InsufficientBalance(); - } IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA(req); } diff --git a/test/tests_cea/CEA_selfCalls.t.sol b/test/tests_cea/CEA_selfCalls.t.sol index 4e0ef28..7e26e2a 100644 --- a/test/tests_cea/CEA_selfCalls.t.sol +++ b/test/tests_cea/CEA_selfCalls.t.sol @@ -384,6 +384,27 @@ contract CEA_ComprehensiveTests is CEATest { assertEq(mockUniversalGateway.lastToken(), address(0), "Token should be zero address for native"); } + function test_FundsAndPayload_ZeroAmount_NonContractToken_Succeeds() public deployCEA { + // Zero amount with a non-contract token address should succeed + // because the zero-amount path skips balanceOf entirely + address fakeToken = makeAddr("nonContractToken"); + + bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); + + Multicall[] memory calls = new Multicall[](1); + calls[0] = + makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(fakeToken, 0, ueaPayload, ueaOnPush)); + + vm.prank(vault); + ceaInstance.executeUniversalTx( + generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) + ); + + assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount"); + assertEq(mockUniversalGateway.lastToken(), fakeToken, "Token should match non-contract address"); + assertEq(mockUniversalGateway.lastValue(), 0, "No native value should be sent"); + } + function test_FundsAndPayload_RevertWhen_InsufficientNativeBalance() public deployCEA { fundCEAWithNative(0.1 ether); From ded10cf508507a0cbf3cde738156299583555207 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 19 Mar 2026 20:01:39 +0400 Subject: [PATCH 03/56] added cea_v2 and deployScripts --- docs/addresses/bsc_testnet.md | 4 +- scripts/cea/deployCEAMigration.s.sol | 128 +++++++++++++++++++++++++++ src/testnetV0/CEA_V2.sol | 13 +++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 scripts/cea/deployCEAMigration.s.sol create mode 100644 src/testnetV0/CEA_V2.sol diff --git a/docs/addresses/bsc_testnet.md b/docs/addresses/bsc_testnet.md index 304914b..aeaee3c 100644 --- a/docs/addresses/bsc_testnet.md +++ b/docs/addresses/bsc_testnet.md @@ -7,5 +7,5 @@ | CEAFactory (implementation) | `0xC0D35725Dd054B09931740DC231cDea89B0FEd3b` | | ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | | CEAFactory (proxy) | `0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719` | - | CEA_V2 (logic) | `0x97Fd78453D4741E28dFdd6b2f7BD2a4bA0b9E04E` | - | CEAMigration | `0x2cc02Ab38367684b0A90e9a09e6216540bBe3F52` | + | CEA_V2 (logic) | `0x102B1652ABEDC1c1761355F1Fc71c8487c3a9168` | + | CEAMigration | `0x2a06BF2A9C19dacbb38852f846B42e278e82e855` | diff --git a/scripts/cea/deployCEAMigration.s.sol b/scripts/cea/deployCEAMigration.s.sol new file mode 100644 index 0000000..931c7af --- /dev/null +++ b/scripts/cea/deployCEAMigration.s.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import {CEA_V2} from "../../src/testnetV0/CEA_V2.sol"; +import {CEAMigration} from "../../src/cea/CEAMigration.sol"; +import {CEAFactory} from "../../src/cea/CEAFactory.sol"; + +/** + * @title DeployCEAMigrationScript + * @notice Deploys CEA_V2 + CEAMigration and sets it in the CEAFactory. + * + * @dev Steps: + * 1. Deploy CEA_V2 (new implementation) + * 2. Deploy CEAMigration(ceaV2Address) + * 3. Call CEAFactory.setCEAMigrationContract(migrationAddress) + * + * CONFIGURATION: + * Environment variables needed: KEY, RPC_URL + */ +contract DeployCEAMigrationScript is Script { + // ============================================================================ + // DEPLOYMENT PARAMETERS + // ============================================================================ + + address public CEA_FACTORY_PROXY = 0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719; + + function run() external { + uint256 chainId = block.chainid; + uint256 deployerKey = uint256(vm.envBytes32("KEY")); + address deployer = vm.addr(deployerKey); + + console.log("=== CEA Migration Deployment ==="); + console.log("Chain ID:", chainId); + console.log("Deployer:", deployer); + console.log("CEAFactory Proxy:", CEA_FACTORY_PROXY); + console.log(""); + + vm.startBroadcast(deployerKey); + + // 1. Deploy CEA_V2 + CEA_V2 ceaV2 = new CEA_V2(); + console.log("[1/3] CEA_V2:", address(ceaV2)); + + // 2. Deploy CEAMigration + CEAMigration migration = new CEAMigration(address(ceaV2)); + console.log("[2/3] CEAMigration:", address(migration)); + + // 3. Set migration contract in factory + CEAFactory factory = CEAFactory(CEA_FACTORY_PROXY); + factory.setCEAMigrationContract(address(migration)); + console.log("[3/3] setCEAMigrationContract called"); + + vm.stopBroadcast(); + + // Post-deployment verification + console.log("\n=== Post-Deployment Verification ==="); + + address verifiedMigration = factory.CEA_MIGRATION_CONTRACT(); + address verifiedImpl = migration.CEA_IMPLEMENTATION(); + + console.log( + "CEA_MIGRATION_CONTRACT:", + verifiedMigration, + verifiedMigration == address(migration) ? "[OK]" : "[MISMATCH]" + ); + console.log("CEA_IMPLEMENTATION:", verifiedImpl, verifiedImpl == address(ceaV2) ? "[OK]" : "[MISMATCH]"); + + require(verifiedMigration == address(migration), "Migration contract mismatch"); + require(verifiedImpl == address(ceaV2), "CEA implementation mismatch"); + + // JSON output + string memory json = string( + abi.encodePacked( + "{\n", + ' "chainId": ', + vm.toString(chainId), + ",\n", + ' "deployer": "', + vm.toString(deployer), + '",\n', + ' "ceaV2": "', + vm.toString(address(ceaV2)), + '",\n', + ' "ceaMigration": "', + vm.toString(address(migration)), + '",\n', + ' "ceaFactoryProxy": "', + vm.toString(CEA_FACTORY_PROXY), + '"\n', + "}" + ) + ); + console.log("\n=== Deployment Addresses (JSON) ==="); + console.log(json); + + string memory filename = string(abi.encodePacked("deployments/", vm.toString(chainId), "_cea_migration.json")); + vm.writeFile(filename, json); + console.log("\nDeployment saved to:", filename); + + console.log("\n=== Deployment Complete ==="); + } +} + +/* + * ============================================================================ + * DEPLOYMENT COMMAND + * ============================================================================ + * + * forge script scripts/cea/deployCEAMigration.s.sol:DeployCEAMigrationScript \ + * --rpc-url $BSC_TESTNET_RPC_URL \ + * --private-key $KEY \ + * --broadcast \ + * -vvvv + * + * ============================================================================ + * VERIFICATION COMMANDS + * ============================================================================ + * + * 1. Verify CEA_V2: + * forge verify-contract src/testnetV0/CEA_V2.sol:CEA_V2 \ + * --chain-id 97 --etherscan-api-key $BSCSCAN_API_KEY + * + * 2. Verify CEAMigration: + * forge verify-contract src/cea/CEAMigration.sol:CEAMigration \ + * --chain-id 97 --etherscan-api-key $BSCSCAN_API_KEY \ + * --constructor-args $(cast abi-encode "constructor(address)" ) + */ diff --git a/src/testnetV0/CEA_V2.sol b/src/testnetV0/CEA_V2.sol new file mode 100644 index 0000000..a9f6087 --- /dev/null +++ b/src/testnetV0/CEA_V2.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {CEA} from "../cea/CEA.sol"; + +/// @title CEA_V2 +/// @notice Testnet-only v2 implementation with a VERSION getter for migration verification. +contract CEA_V2 is CEA { + /// @notice Returns the implementation version. + function VERSION() external pure returns (string memory) { + return "2"; + } +} From 753394d5959b2d0d19cdf70d9669c3c25e8015d4 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:13:17 +0530 Subject: [PATCH 04/56] =?UTF-8?q?fix(audit):=20F-2026-15505=20=E2=80=94=20?= =?UTF-8?q?track=20totalSupply=20in=20storage=20instead=20of=20address(thi?= =?UTF-8?q?s).balance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit totalSupply() previously returned address(this).balance, which could be inflated by force-sent native PC (e.g. via selfdestruct). Now tracked via a private _totalSupply variable updated only in deposit() and withdraw(), restoring the ERC-20 invariant totalSupply == sum(balanceOf). --- src/WPC.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/WPC.sol b/src/WPC.sol index d5d3563..892d494 100644 --- a/src/WPC.sol +++ b/src/WPC.sol @@ -16,6 +16,7 @@ contract WPC is IWPC { string public symbol = "WPC"; uint8 public decimals = 18; + uint256 private _totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; @@ -25,6 +26,7 @@ contract WPC is IWPC { /// @inheritdoc IWPC function deposit() public payable { + _totalSupply += msg.value; balanceOf[msg.sender] += msg.value; emit Deposit(msg.sender, msg.value); } @@ -33,6 +35,7 @@ contract WPC is IWPC { function withdraw(uint256 wad) public { require(balanceOf[msg.sender] >= wad, ""); balanceOf[msg.sender] -= wad; + _totalSupply -= wad; payable(msg.sender).transfer(wad); emit Withdrawal(msg.sender, wad); } @@ -43,7 +46,7 @@ contract WPC is IWPC { /// @inheritdoc IWPC function totalSupply() public view returns (uint256) { - return address(this).balance; + return _totalSupply; } /// @inheritdoc IWPC From dc7221322c350b986881ec7ae5a56a24fd0c1a86 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:20:30 +0530 Subject: [PATCH 05/56] =?UTF-8?q?fix(audit):=20F-2026-15506=20=E2=80=94=20?= =?UTF-8?q?use=20SafeERC20=20forceApprove=20consistently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced raw approve() calls with forceApprove() from SafeERC20 in swapAndBurnGas and _autoSwap. The using SafeERC20 for IERC20 declaration was already present but unused — now all approve interactions go through the safe wrapper. --- src/UniversalCore.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 82c1586..0ff5349 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -219,7 +219,7 @@ contract UniversalCore is IWPC(WPC).deposit{value: msg.value}(); - IERC20(WPC).approve(uniswapV3SwapRouter, msg.value); + IERC20(WPC).forceApprove(uniswapV3SwapRouter, msg.value); ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ tokenIn: WPC, @@ -233,7 +233,7 @@ contract UniversalCore is }); uint256 amountInUsed = ISwapRouter(uniswapV3SwapRouter).exactOutputSingle(params); - IERC20(WPC).approve(uniswapV3SwapRouter, 0); + IERC20(WPC).forceApprove(uniswapV3SwapRouter, 0); IPRC20(gasToken).burn(gasFee); @@ -523,7 +523,7 @@ contract UniversalCore is if (minPCOut == 0) revert CommonErrors.ZeroAmount(); IPRC20(prc20).deposit(address(this), amount); - IPRC20(prc20).approve(uniswapV3SwapRouter, amount); + IERC20(prc20).forceApprove(uniswapV3SwapRouter, amount); ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: prc20, @@ -539,7 +539,7 @@ contract UniversalCore is pcOut = ISwapRouter(uniswapV3SwapRouter).exactInputSingle(params); if (pcOut < minPCOut) revert UniversalCoreErrors.SlippageExceeded(); - IPRC20(prc20).approve(uniswapV3SwapRouter, 0); + IERC20(prc20).forceApprove(uniswapV3SwapRouter, 0); IWPC(WPC).withdraw(pcOut); (bool ok,) = recipient.call{value: pcOut}(""); From d41ba00132899dc5be45997de49651f31ce6a3e7 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:25:08 +0530 Subject: [PATCH 06/56] =?UTF-8?q?fix(audit):=20F-2026-15511=20=E2=80=94=20?= =?UTF-8?q?add=20nonReentrant=20to=20depositPRC20Token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit depositPRC20Token was the only UE-module entrypoint missing reentrancy protection. Added nonReentrant for symmetry with depositPRC20WithAutoSwap, refundUnusedGas, and swapAndBurnGas. --- src/UniversalCore.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 0ff5349..cfbe651 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -139,7 +139,7 @@ contract UniversalCore is // ========================= /// @inheritdoc IUniversalCore - function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule whenNotPaused { + function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule whenNotPaused nonReentrant { _validateParams(prc20, amount, recipient); IPRC20(prc20).deposit(recipient, amount); } From b98313f7776a3f19c49144526fb77c883c2b66ba Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:29:00 +0530 Subject: [PATCH 07/56] =?UTF-8?q?fix(audit):=20F-2026-15519=20=E2=80=94=20?= =?UTF-8?q?validate=20allowance=20before=20transfer=20in=20PRC20.transferF?= =?UTF-8?q?rom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reordered transferFrom to check and deduct allowance before calling _transfer, following standard ERC-20 checks-effects-interactions pattern. Updated tests to reflect new event ordering and revert behavior. --- src/PRC20.sol | 4 ++-- test/tests_token_and_core/PRC20.t.sol | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PRC20.sol b/src/PRC20.sol index 369c42f..861d87d 100644 --- a/src/PRC20.sol +++ b/src/PRC20.sol @@ -144,8 +144,6 @@ contract PRC20 is IPRC20, Initializable { /// @inheritdoc IPRC20 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { - _transfer(sender, recipient, amount); - uint256 currentAllowance = _allowances[sender][msg.sender]; if (currentAllowance < amount) revert PRC20Errors.LowAllowance(); unchecked { @@ -153,6 +151,8 @@ contract PRC20 is IPRC20, Initializable { } emit Approval(sender, msg.sender, _allowances[sender][msg.sender]); + _transfer(sender, recipient, amount); + return true; } diff --git a/test/tests_token_and_core/PRC20.t.sol b/test/tests_token_and_core/PRC20.t.sol index f24d89d..1d165a5 100644 --- a/test/tests_token_and_core/PRC20.t.sol +++ b/test/tests_token_and_core/PRC20.t.sol @@ -250,10 +250,10 @@ contract PRC20Test is Test, UpgradeableContractHelper { vm.prank(bob); vm.expectEmit(true, true, false, true); - emit Transfer(alice, bob, APPROVAL_AMOUNT); + emit Approval(alice, bob, 0); vm.expectEmit(true, true, false, true); - emit Approval(alice, bob, 0); + emit Transfer(alice, bob, APPROVAL_AMOUNT); bool success = prc20.transferFrom(alice, bob, APPROVAL_AMOUNT); @@ -276,7 +276,7 @@ contract PRC20Test is Test, UpgradeableContractHelper { function testTransferFromRevertZeroAddressSender() public { vm.prank(bob); - vm.expectRevert(CommonErrors.ZeroAddress.selector); + vm.expectRevert(PRC20Errors.LowAllowance.selector); prc20.transferFrom(address(0), bob, APPROVAL_AMOUNT); } From 31d5417b86520349722d56c9813ebf043635248b Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:37:34 +0530 Subject: [PATCH 08/56] =?UTF-8?q?fix(audit):=20F-2026-15521=20=E2=80=94=20?= =?UTF-8?q?replace=20empty=20revert=20strings=20with=20custom=20errors=20i?= =?UTF-8?q?n=20WPC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added WPCErrors library with InsufficientBalance and InsufficientAllowance custom errors. Replaced three require(..., "") statements in withdraw and transferFrom with structured custom error reverts for gas efficiency and failure observability. --- src/WPC.sol | 7 ++++--- src/libraries/Errors.sol | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/WPC.sol b/src/WPC.sol index 892d494..514e647 100644 --- a/src/WPC.sol +++ b/src/WPC.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {IWPC} from "./interfaces/IWPC.sol"; +import {WPCErrors} from "./libraries/Errors.sol"; /** * @title WPC @@ -33,7 +34,7 @@ contract WPC is IWPC { /// @inheritdoc IWPC function withdraw(uint256 wad) public { - require(balanceOf[msg.sender] >= wad, ""); + if (balanceOf[msg.sender] < wad) revert WPCErrors.InsufficientBalance(); balanceOf[msg.sender] -= wad; _totalSupply -= wad; payable(msg.sender).transfer(wad); @@ -63,10 +64,10 @@ contract WPC is IWPC { /// @inheritdoc IWPC function transferFrom(address src, address dst, uint256 wad) public returns (bool) { - require(balanceOf[src] >= wad, ""); + if (balanceOf[src] < wad) revert WPCErrors.InsufficientBalance(); if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { - require(allowance[src][msg.sender] >= wad, ""); + if (allowance[src][msg.sender] < wad) revert WPCErrors.InsufficientAllowance(); allowance[src][msg.sender] -= wad; } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index d366830..6c2563a 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -50,6 +50,15 @@ library UniversalCoreErrors { error ZeroRescueGasLimit(); } +// ========================= +// WPC-Specific ERRORS +// ========================= + +library WPCErrors { + error InsufficientBalance(); + error InsufficientAllowance(); +} + // ========================= // UEA-Specific ERRORS // ========================= From 5377ac512b0f54a872d6bed29244e8bce52d5cc2 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 10:51:48 +0530 Subject: [PATCH 09/56] =?UTF-8?q?fix(audit):=20F-2026-15527=20=E2=80=94=20?= =?UTF-8?q?add=20events=20for=20admin=20configuration=20setters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added events to 8 privileged setters that were mutating protocol state without emitting: setAutoSwapSupported, setWPC, setUniversalGatewayPC, setUniswapV3Addresses, setDefaultFeeTier, setSlippageTolerance in UniversalCore, and setName, setSymbol in PRC20. Address setters include old and new values. Tests updated with event emission assertions. --- src/PRC20.sol | 4 ++++ src/UniversalCore.sol | 8 +++++++ test/tests_token_and_core/PRC20.t.sol | 12 ++++++---- test/tests_token_and_core/UniversalCore.t.sol | 23 ++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/PRC20.sol b/src/PRC20.sol index 861d87d..5556824 100644 --- a/src/PRC20.sol +++ b/src/PRC20.sol @@ -193,13 +193,17 @@ contract PRC20 is IPRC20, Initializable { /// @notice Update token name. /// @param newName New name string function setName(string memory newName) external onlyUniversalExecutor { + string memory oldName = _name; _name = newName; + emit NameUpdated(oldName, newName); } /// @notice Update token symbol. /// @param newSymbol New symbol string function setSymbol(string memory newSymbol) external onlyUniversalExecutor { + string memory oldSymbol = _symbol; _symbol = newSymbol; + emit SymbolUpdated(oldSymbol, newSymbol); } // ========================= diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index cfbe651..45229b6 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -376,20 +376,25 @@ contract UniversalCore is /// @param supported Whether the token supports auto-swap function setAutoSwapSupported(address token, bool supported) external onlyAdmin { isAutoSwapSupported[token] = supported; + emit SetAutoSwapSupported(token, supported); } /// @notice Set the wrapped PC address. /// @param addr WPC new address function setWPC(address addr) external onlyAdmin { if (addr == address(0)) revert CommonErrors.ZeroAddress(); + address oldAddr = WPC; WPC = addr; + emit SetWPC(oldAddr, addr); } /// @notice Set the UniversalGatewayPC address. /// @param addr UniversalGatewayPC address function setUniversalGatewayPC(address addr) external onlyAdmin { if (addr == address(0)) revert CommonErrors.ZeroAddress(); + address oldAddr = universalGatewayPC; universalGatewayPC = addr; + emit SetUniversalGatewayPC(oldAddr, addr); } /// @notice Setter for Uniswap V3 addresses. @@ -403,6 +408,7 @@ contract UniversalCore is uniswapV3Factory = factory; uniswapV3SwapRouter = swapRouter; uniswapV3Quoter = quoter; + emit SetUniswapV3Addresses(factory, swapRouter, quoter); } /// @notice Set default fee tier for a token. @@ -414,6 +420,7 @@ contract UniversalCore is revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; + emit SetDefaultFeeTier(token, feeTier); } /// @notice Set slippage tolerance for a token. @@ -425,6 +432,7 @@ contract UniversalCore is revert UniversalCoreErrors.InvalidSlippageTolerance(); } slippageTolerance[token] = tolerance; + emit SetSlippageTolerance(token, tolerance); } /// @notice Set default deadline in minutes. diff --git a/test/tests_token_and_core/PRC20.t.sol b/test/tests_token_and_core/PRC20.t.sol index 1d165a5..d98510f 100644 --- a/test/tests_token_and_core/PRC20.t.sol +++ b/test/tests_token_and_core/PRC20.t.sol @@ -43,6 +43,8 @@ contract PRC20Test is Test, UpgradeableContractHelper { event Approval(address indexed owner, address indexed spender, uint256 value); event Deposit(bytes from, address to, uint256 amount); event UpdatedUniversalCore(address universalCore); + event NameUpdated(string oldName, string newName); + event SymbolUpdated(string oldSymbol, string newSymbol); function setUp() public { // Setup actors @@ -551,12 +553,13 @@ contract PRC20Test is Test, UpgradeableContractHelper { function testSetNameFromUExec() public { string memory newName = "New Push Token"; + string memory oldName = prc20.name(); - // Set name from Universal Executor Module vm.prank(uExec); + vm.expectEmit(false, false, false, true); + emit NameUpdated(oldName, newName); prc20.setName(newName); - // Verify name was updated assertEq(prc20.name(), newName); } @@ -571,12 +574,13 @@ contract PRC20Test is Test, UpgradeableContractHelper { function testSetSymbolFromUExec() public { string memory newSymbol = "NPUSH"; + string memory oldSymbol = prc20.symbol(); - // Set symbol from Universal Executor Module vm.prank(uExec); + vm.expectEmit(false, false, false, true); + emit SymbolUpdated(oldSymbol, newSymbol); prc20.setSymbol(newSymbol); - // Verify symbol was updated assertEq(prc20.symbol(), newSymbol); } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 3a7fe09..12a996b 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -46,7 +46,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SystemContractDeployed(); event SetAutoSwapSupported(address indexed token, bool supported); - event SetWPC(address indexed wpc); + event SetWPC(address indexed oldAddr, address indexed newAddr); + event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); + event SetUniswapV3Addresses(address factory, address swapRouter, address quoter); + event SetDefaultFeeTier(address indexed token, uint24 feeTier); + event SetSlippageTolerance(address indexed token, uint256 tolerance); event SetGasPCPool(string indexed chainId, address indexed pool, uint24 fee); event SetGasToken(string indexed chainId, address indexed prc20); event DepositPRC20WithAutoSwap( @@ -215,11 +219,15 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address token = makeAddr("token"); vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit SetAutoSwapSupported(token, true); universalCore.setAutoSwapSupported(token, true); assertTrue(universalCore.isAutoSwapSupported(token)); // Test flipping to false vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit SetAutoSwapSupported(token, false); universalCore.setAutoSwapSupported(token, false); assertFalse(universalCore.isAutoSwapSupported(token)); } @@ -247,8 +255,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetWPCContractAddress_HappyPath() public { address newWPC = makeAddr("newWPC"); + address oldWPC = universalCore.WPC(); vm.prank(deployer); + vm.expectEmit(true, true, false, true); + emit SetWPC(oldWPC, newWPC); universalCore.setWPC(newWPC); assertEq(universalCore.WPC(), newWPC); @@ -1084,7 +1095,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetUniversalGatewayPC_HappyPath() public { address gateway = makeAddr("gateway"); + address oldGateway = universalCore.universalGatewayPC(); + vm.prank(deployer); + vm.expectEmit(true, true, false, true); + emit SetUniversalGatewayPC(oldGateway, gateway); universalCore.setUniversalGatewayPC(gateway); assertEq(universalCore.universalGatewayPC(), gateway); } @@ -1111,6 +1126,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address q = makeAddr("quoter2"); vm.prank(deployer); + vm.expectEmit(false, false, false, true); + emit SetUniswapV3Addresses(f, r, q); universalCore.setUniswapV3Addresses(f, r, q); assertEq(universalCore.uniswapV3Factory(), f); @@ -1169,6 +1186,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetDefaultFeeTier_HappyPath_500() public { address token = makeAddr("token"); vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit SetDefaultFeeTier(token, 500); universalCore.setDefaultFeeTier(token, 500); assertEq(universalCore.defaultFeeTier(token), 500); } @@ -1213,6 +1232,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetSlippageTolerance_HappyPath() public { address token = makeAddr("token"); vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit SetSlippageTolerance(token, 300); universalCore.setSlippageTolerance(token, 300); assertEq(universalCore.slippageTolerance(token), 300); } From 1fe77cafae83f21dbfdb45ed76a220cb2cdccbed Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:07:51 +0530 Subject: [PATCH 10/56] =?UTF-8?q?fix(audit):=20F-2026-15537=20=E2=80=94=20?= =?UTF-8?q?remove=20unused=20uniswapV3Quoter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed uniswapV3Quoter entirely from UniversalCore (not yet deployed on mainnet, no storage layout to preserve). Removed from initialize params and setUniswapV3Addresses. In UniversalCoreV0 (deployed testnet), replaced with __deprecated_uniswapV3Quoter to preserve storage layout. Updated all test files to remove quoter references. --- src/UniversalCore.sol | 12 ++------ src/testnetV0/UniversalCoreV0.sol | 8 ++--- test/fuzz/PRC20_Fuzz.t.sol | 3 +- test/fuzz/UniversalCore_Fuzz.t.sol | 11 ++++--- test/tests_token_and_core/UniversalCore.t.sol | 30 +++++-------------- .../UniversalCoreRefund.t.sol | 4 --- .../UniversalCoreSwapFee.t.sol | 4 --- 7 files changed, 21 insertions(+), 51 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 45229b6..011c434 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -66,7 +66,6 @@ contract UniversalCore is address public uniswapV3Factory; address public uniswapV3SwapRouter; - address public uniswapV3Quoter; mapping(string => address) public gasPCPoolByChainNamespace; mapping(address => bool) public isAutoSwapSupported; mapping(address => uint24) public defaultFeeTier; @@ -110,12 +109,10 @@ contract UniversalCore is /// @param wpc_ Address of the wrapped PC token /// @param uniswapV3Factory_ Address of the Uniswap V3 factory /// @param uniswapV3SwapRouter_ Address of the Uniswap V3 swap router - /// @param uniswapV3Quoter_ Address of the Uniswap V3 quoter function initialize( address wpc_, address uniswapV3Factory_, address uniswapV3SwapRouter_, - address uniswapV3Quoter_, address initialPauser_ ) public virtual initializer { if (initialPauser_ == address(0)) revert CommonErrors.ZeroAddress(); @@ -131,7 +128,6 @@ contract UniversalCore is WPC = wpc_; uniswapV3Factory = uniswapV3Factory_; uniswapV3SwapRouter = uniswapV3SwapRouter_; - uniswapV3Quoter = uniswapV3Quoter_; } // ========================= @@ -400,15 +396,13 @@ contract UniversalCore is /// @notice Setter for Uniswap V3 addresses. /// @param factory Uniswap V3 Factory address /// @param swapRouter Uniswap V3 SwapRouter address - /// @param quoter Uniswap V3 Quoter address - function setUniswapV3Addresses(address factory, address swapRouter, address quoter) external onlyAdmin { - if (factory == address(0) || swapRouter == address(0) || quoter == address(0)) { + function setUniswapV3Addresses(address factory, address swapRouter) external onlyAdmin { + if (factory == address(0) || swapRouter == address(0)) { revert CommonErrors.ZeroAddress(); } uniswapV3Factory = factory; uniswapV3SwapRouter = swapRouter; - uniswapV3Quoter = quoter; - emit SetUniswapV3Addresses(factory, swapRouter, quoter); + emit SetUniswapV3Addresses(factory, swapRouter); } /// @notice Set default fee tier for a token. diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index 212cf35..1c0d518 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -69,8 +69,8 @@ contract UniversalCoreV0 is /// @notice Uniswap V3 SwapRouter. address public uniswapV3SwapRouter; - /// @notice Uniswap V3 Quoter. - address public uniswapV3Quoter; + /// @dev Deprecated. Slot retained for storage layout compatibility with deployed testnet proxy. + address private __deprecated_uniswapV3Quoter; /// @notice Address of the wrapped PC to interact with Uniswap V3. address public WPC; @@ -163,7 +163,7 @@ contract UniversalCoreV0 is WPC = wpc_; uniswapV3Factory = uniswapV3Factory_; uniswapV3SwapRouter = uniswapV3SwapRouter_; - uniswapV3Quoter = uniswapV3Quoter_; + __deprecated_uniswapV3Quoter = uniswapV3Quoter_; } // ========================= @@ -452,7 +452,7 @@ contract UniversalCoreV0 is } uniswapV3Factory = factory; uniswapV3SwapRouter = swapRouter; - uniswapV3Quoter = quoter; + __deprecated_uniswapV3Quoter = quoter; } /// @notice Set default fee tier for a token. diff --git a/test/fuzz/PRC20_Fuzz.t.sol b/test/fuzz/PRC20_Fuzz.t.sol index d131b78..3bc85f3 100644 --- a/test/fuzz/PRC20_Fuzz.t.sol +++ b/test/fuzz/PRC20_Fuzz.t.sol @@ -20,10 +20,9 @@ contract PRC20_Fuzz is Test, UpgradeableContractHelper { address mockWPC = makeAddr("wpc"); address mockFactory = makeAddr("factory"); address mockRouter = makeAddr("router"); - address mockQuoter = makeAddr("quoter"); address mockPauser = makeAddr("pauser"); bytes memory ucInit = abi.encodeWithSelector( - UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, mockQuoter, mockPauser + UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, mockPauser ); address ucProxy = deployUpgradeableContract(address(ucImpl), ucInit); universalCore = UniversalCore(payable(ucProxy)); diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index cbfc4a3..933202b 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -29,12 +29,11 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { address mockWPC = makeAddr("wPC"); address mockFactory = makeAddr("uniswapFactory"); address mockRouter = makeAddr("uniswapRouter"); - address mockQuoter = makeAddr("uniswapQuoter"); pauser = makeAddr("pauser"); UniversalCore impl = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( - UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, mockQuoter, pauser + UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, pauser ); address proxyAddr = deployUpgradeableContract(address(impl), initData); universalCore = UniversalCore(payable(proxyAddr)); @@ -371,15 +370,15 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setUniversalGatewayPC(address(0)); } - function testFuzz_setUniswapV3Addresses_anyZero_reverts(address f, address r, address q) public { - bool anyZero = f == address(0) || r == address(0) || q == address(0); + function testFuzz_setUniswapV3Addresses_anyZero_reverts(address f, address r) public { + bool anyZero = f == address(0) || r == address(0); if (anyZero) { vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(f, r, q); + universalCore.setUniswapV3Addresses(f, r); } else { // No revert expected — just verify it stores values - universalCore.setUniswapV3Addresses(f, r, q); + universalCore.setUniswapV3Addresses(f, r); } } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 12a996b..735f0ea 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -10,7 +10,6 @@ import {UniversalCoreErrors, PRC20Errors, CommonErrors} from "../../src/librarie import "../../test/helpers/UpgradeableContractHelper.sol"; import "../../test/mocks/MockUniswapV3Factory.sol"; import "../../test/mocks/MockUniswapV3Router.sol"; -import "../../test/mocks/MockUniswapV3Quoter.sol"; import "../../test/mocks/MockWPC.sol"; import "../../test/mocks/MockPRC20.sol"; import "../../test/mocks/MaliciousPRC20.sol"; @@ -25,7 +24,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { PRC20 public prc20Token; MockUniswapV3Factory public mockFactory; MockUniswapV3Router public mockRouter; - MockUniswapV3Quoter public mockQuoter; MockWPC public mockWPC; address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; @@ -48,7 +46,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SetAutoSwapSupported(address indexed token, bool supported); event SetWPC(address indexed oldAddr, address indexed newAddr); event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); - event SetUniswapV3Addresses(address factory, address swapRouter, address quoter); + event SetUniswapV3Addresses(address factory, address swapRouter); event SetDefaultFeeTier(address indexed token, uint24 feeTier); event SetSlippageTolerance(address indexed token, uint256 tolerance); event SetGasPCPool(string indexed chainId, address indexed pool, uint24 fee); @@ -79,7 +77,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Deploy mocks mockFactory = new MockUniswapV3Factory(); mockRouter = new MockUniswapV3Router(); - mockQuoter = new MockUniswapV3Quoter(); mockWPC = new MockWPC(); mockPRC20 = new MockPRC20(); @@ -110,7 +107,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address(mockWPC), address(mockFactory), address(mockRouter), - address(mockQuoter), pauser ); @@ -145,7 +141,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { UniversalCore newHandler = new UniversalCore(); // Should not be able to call initialize on implementation directly vm.expectRevert(Initializable.InvalidInitialization.selector); - newHandler.initialize(address(mockWPC), address(mockFactory), address(mockRouter), address(mockQuoter), pauser); + newHandler.initialize(address(mockWPC), address(mockFactory), address(mockRouter), pauser); } function test_Initialize_GrantsAdminRoleToDeployer() public { @@ -160,7 +156,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address(mockWPC), address(mockFactory), address(mockRouter), - address(mockQuoter), newPauser ); @@ -176,13 +171,12 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertEq(universalCore.WPC(), address(mockWPC)); assertEq(universalCore.uniswapV3Factory(), address(mockFactory)); assertEq(universalCore.uniswapV3SwapRouter(), address(mockRouter)); - assertEq(universalCore.uniswapV3Quoter(), address(mockQuoter)); } function test_Initialize_RevertsOnSecondCall() public { vm.expectRevert(Initializable.InvalidInitialization.selector); universalCore.initialize( - address(mockWPC), address(mockFactory), address(mockRouter), address(mockQuoter), pauser + address(mockWPC), address(mockFactory), address(mockRouter), pauser ); } @@ -1123,40 +1117,32 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetUniswapV3Addresses_HappyPath() public { address f = makeAddr("factory2"); address r = makeAddr("router2"); - address q = makeAddr("quoter2"); vm.prank(deployer); vm.expectEmit(false, false, false, true); - emit SetUniswapV3Addresses(f, r, q); - universalCore.setUniswapV3Addresses(f, r, q); + emit SetUniswapV3Addresses(f, r); + universalCore.setUniswapV3Addresses(f, r); assertEq(universalCore.uniswapV3Factory(), f); assertEq(universalCore.uniswapV3SwapRouter(), r); - assertEq(universalCore.uniswapV3Quoter(), q); } function test_SetUniswapV3Addresses_RevertsZeroFactory() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(address(0), makeAddr("r"), makeAddr("q")); + universalCore.setUniswapV3Addresses(address(0), makeAddr("r")); } function test_SetUniswapV3Addresses_RevertsZeroRouter() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(makeAddr("f"), address(0), makeAddr("q")); - } - - function test_SetUniswapV3Addresses_RevertsZeroQuoter() public { - vm.prank(deployer); - vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(makeAddr("f"), makeAddr("r"), address(0)); + universalCore.setUniswapV3Addresses(makeAddr("f"), address(0)); } function test_SetUniswapV3Addresses_OnlyAdmin() public { vm.prank(nonOwner); vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setUniswapV3Addresses(makeAddr("f"), makeAddr("r"), makeAddr("q")); + universalCore.setUniswapV3Addresses(makeAddr("f"), makeAddr("r")); } // ======================================== diff --git a/test/tests_token_and_core/UniversalCoreRefund.t.sol b/test/tests_token_and_core/UniversalCoreRefund.t.sol index 19a003c..7353162 100644 --- a/test/tests_token_and_core/UniversalCoreRefund.t.sol +++ b/test/tests_token_and_core/UniversalCoreRefund.t.sol @@ -9,7 +9,6 @@ import {UniversalCoreErrors, CommonErrors} from "../../src/libraries/Errors.sol" import "../../test/helpers/UpgradeableContractHelper.sol"; import "../../test/mocks/MockUniswapV3Factory.sol"; import "../../test/mocks/MockUniswapV3Router.sol"; -import "../../test/mocks/MockUniswapV3Quoter.sol"; import "../../test/mocks/MockPRC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; @@ -52,7 +51,6 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { UniversalCore public universalCore; MockUniswapV3Factory public mockFactory; MockUniswapV3Router public mockRouter; - MockUniswapV3Quoter public mockQuoter; MockWPCLike public mockWPC; MockPRC20 public gasTokenMock; @@ -81,7 +79,6 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { mockFactory = new MockUniswapV3Factory(); mockRouter = new MockUniswapV3Router(); - mockQuoter = new MockUniswapV3Quoter(); mockWPC = new MockWPCLike(); gasTokenMock = new MockPRC20(); @@ -94,7 +91,6 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { address(mockWPC), address(mockFactory), address(mockRouter), - address(mockQuoter), pauser ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index 104dc1c..13f6385 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -10,7 +10,6 @@ import {UniversalCoreErrors, CommonErrors} from "../../src/libraries/Errors.sol" import "../../test/helpers/UpgradeableContractHelper.sol"; import "../../test/mocks/MockUniswapV3Factory.sol"; import "../../test/mocks/MockUniswapV3Router.sol"; -import "../../test/mocks/MockUniswapV3Quoter.sol"; import "../../test/mocks/MockWPC.sol"; import "../../test/mocks/MockPRC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; @@ -21,7 +20,6 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { PRC20 public prc20Token; MockUniswapV3Factory public mockFactory; MockUniswapV3Router public mockRouter; - MockUniswapV3Quoter public mockQuoter; MockWPC public mockWPC; MockPRC20 public gasTokenMock; @@ -53,7 +51,6 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { mockFactory = new MockUniswapV3Factory(); mockRouter = new MockUniswapV3Router(); - mockQuoter = new MockUniswapV3Quoter(); mockWPC = new MockWPC(); gasTokenMock = new MockPRC20(); @@ -79,7 +76,6 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { address(mockWPC), address(mockFactory), address(mockRouter), - address(mockQuoter), pauser ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); From 9794d515ad5cb60f676e040e237a8eea129afe31 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:15:26 +0530 Subject: [PATCH 11/56] =?UTF-8?q?fix(audit):=20F-2026-15540=20=E2=80=94=20?= =?UTF-8?q?revert=20on=20zero=20baseGasLimit=20in=20getOutboundTxGasAndFee?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ZeroBaseGasLimit error and explicit revert when baseGasLimitByChainNamespace is unconfigured (zero), aligning validation with getRescueFundsGasLimit which already reverts on zero. Prevents silently returning zero gas fees for unconfigured chains. --- src/UniversalCore.sol | 1 + src/libraries/Errors.sol | 1 + test/fuzz/UniversalCore_Fuzz.t.sol | 14 ++++++++++++-- test/tests_token_and_core/UniversalCore.t.sol | 6 +++--- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 011c434..64323f8 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -256,6 +256,7 @@ contract UniversalCore is { chainNamespace = IPRC20(_prc20).SOURCE_CHAIN_NAMESPACE(); uint256 baseLimit = baseGasLimitByChainNamespace[chainNamespace]; + if (baseLimit == 0) revert UniversalCoreErrors.ZeroBaseGasLimit(); if (gasLimitWithBaseLimit == 0) { gasLimitWithBaseLimit = baseLimit; diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 6c2563a..cd805a3 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -47,6 +47,7 @@ library UniversalCoreErrors { error InvalidSlippageTolerance(); error MinPCOutRequired(); error GasLimitBelowBase(uint256 provided, uint256 minimum); + error ZeroBaseGasLimit(); error ZeroRescueGasLimit(); } diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index 933202b..da26dbd 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -117,7 +117,12 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { } function testFuzz_getOutboundTxGasAndFees_zeroGasPrice_reverts(uint128 gasLimit) public { - vm.assume(gasLimit > 0); + uint256 baseLimit = 100_000; + vm.assume(gasLimit >= baseLimit); + + // Set base gas limit so we pass the zero-base check + vm.prank(uExec); + universalCore.setBaseGasLimitByChain(CHAIN_NS, baseLimit); // Set gas price to 0 — setChainMeta is onlyUEModule vm.prank(uExec); @@ -128,7 +133,12 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { } function testFuzz_getOutboundTxGasAndFees_zeroGasToken_reverts(uint128 gasLimit) public { - vm.assume(gasLimit > 0); + uint256 baseLimit = 100_000; + vm.assume(gasLimit >= baseLimit); + + // Set base gas limit for "nogas" chain so we pass the zero-base check + vm.prank(uExec); + universalCore.setBaseGasLimitByChain("nogas", baseLimit); // Deploy a fresh PRC20 on chain "nogas" — no gas token configured for "nogas" PRC20 prc20Impl = new PRC20(); diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 735f0ea..87d69b9 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -706,10 +706,10 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address proxyAddress = deployUpgradeableContract(address(newPrc20Token), initData); PRC20 newToken = PRC20(payable(proxyAddress)); - // Don't set gas token for this chain ID, so it will be address(0) + // Don't set gas token or base gas limit for this chain ID - // Expect revert when getting gas fee - vm.expectRevert(CommonErrors.ZeroAddress.selector); + // Expect revert due to unconfigured base gas limit + vm.expectRevert(UniversalCoreErrors.ZeroBaseGasLimit.selector); universalCore.getOutboundTxGasAndFees(address(newToken), 0); } From 6aef1060e4cf31fbff9187477b4cc4fdec41532c Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:16:56 +0530 Subject: [PATCH 12/56] =?UTF-8?q?fix(audit):=20F-2026-15527/F-2026-15537?= =?UTF-8?q?=20=E2=80=94=20include=20interface=20event=20declarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds event declarations to IPRC20 and IUniversalCore interfaces that were missed in the F-2026-15527 and F-2026-15537 commits due to filesystem case-sensitivity (Interfaces/ vs interfaces/). No behavioral change — events were already emitted correctly from the implementations. --- src/Interfaces/IPRC20.sol | 2 ++ src/Interfaces/IUniversalCore.sol | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/Interfaces/IPRC20.sol b/src/Interfaces/IPRC20.sol index 919fb0c..aaa908f 100644 --- a/src/Interfaces/IPRC20.sol +++ b/src/Interfaces/IPRC20.sol @@ -24,6 +24,8 @@ interface IPRC20 { event Deposit(bytes from, address to, uint256 amount); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); + event NameUpdated(string oldName, string newName); + event SymbolUpdated(string oldSymbol, string newSymbol); // ========================= // PRC20_1: ERC-20 METADATA diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 068996a..555ab84 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -25,6 +25,13 @@ interface IUniversalCore { address indexed gasToken, uint256 amount, address indexed recipient, bool swapped, uint256 pcOut ); + event SetAutoSwapSupported(address indexed token, bool supported); + event SetWPC(address indexed oldAddr, address indexed newAddr); + event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); + event SetUniswapV3Addresses(address factory, address swapRouter); + event SetDefaultFeeTier(address indexed token, uint24 feeTier); + event SetSlippageTolerance(address indexed token, uint256 tolerance); + /// @notice Emitted when the PAUSER_ROLE is granted to a new address. /// @param pauser Address that was granted the pauser role event PauserRoleGranted(address indexed pauser); From 68cb7484fc31c1c8412f192cb1ff59ab78333c92 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:24:14 +0530 Subject: [PATCH 13/56] =?UTF-8?q?fix(audit):=20F-2026-15544=20=E2=80=94=20?= =?UTF-8?q?document=20gasPCPool=20as=20informational-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added NatSpec to gasPCPoolByChainNamespace and setGasPCPool clarifying the stored pool is for off-chain observability only. Runtime swap flows resolve pools dynamically from the factory. No code change. --- src/UniversalCore.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 64323f8..831b242 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -66,6 +66,8 @@ contract UniversalCore is address public uniswapV3Factory; address public uniswapV3SwapRouter; + /// @notice Stored gas PC pool per chain — informational only for off-chain consumers. + /// @dev Not used in runtime swap logic. Pool resolution is dynamic via uniswapV3Factory. mapping(string => address) public gasPCPoolByChainNamespace; mapping(address => bool) public isAutoSwapSupported; mapping(address => uint24) public defaultFeeTier; @@ -324,7 +326,9 @@ contract UniversalCore is emit SetSupportedToken(prc20, supported); } - /// @notice Set the gas PC pool for a chain. + /// @notice Set the gas PC pool for a chain (informational — not enforced at runtime). + /// @dev The stored pool is for off-chain observability only. Runtime swap flows + /// (swapAndBurnGas, _autoSwap) resolve pools dynamically from the factory. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasToken Gas coin address /// @param fee Uniswap V3 fee tier From 295749c23f105d3d73ec872bfee803ae6ecbd798 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:29:27 +0530 Subject: [PATCH 14/56] =?UTF-8?q?fix(audit):=20F-2026-15545=20=E2=80=94=20?= =?UTF-8?q?replace=20magic=20numbers=20with=20named=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added FEE_TIER_LOW (500), FEE_TIER_MEDIUM (3000), FEE_TIER_HIGH (10000), and MAX_SLIPPAGE_BPS (5000) constants in both UniversalCore and UniversalCoreV0. Replaced hardcoded literals in setDefaultFeeTier and setSlippageTolerance. --- src/UniversalCore.sol | 12 ++++++++++-- src/testnetV0/UniversalCoreV0.sol | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 831b242..e9acb03 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -44,6 +44,14 @@ contract UniversalCore is bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + // -- Uniswap V3 fee tiers -- + uint24 public constant FEE_TIER_LOW = 500; + uint24 public constant FEE_TIER_MEDIUM = 3000; + uint24 public constant FEE_TIER_HIGH = 10000; + + // -- Slippage cap (basis points) -- + uint256 public constant MAX_SLIPPAGE_BPS = 5000; + // -- Protocol addresses -- address public universalGatewayPC; address public WPC; @@ -415,7 +423,7 @@ contract UniversalCore is /// @param feeTier Fee tier (500, 3000, 10000) function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != 500 && feeTier != 3000 && feeTier != 10000) { + if (feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; @@ -427,7 +435,7 @@ contract UniversalCore is /// @param tolerance Slippage tolerance in basis points (e.g., 300 = 3%) function setSlippageTolerance(address token, uint256 tolerance) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (tolerance > 5000) { + if (tolerance > MAX_SLIPPAGE_BPS) { revert UniversalCoreErrors.InvalidSlippageTolerance(); } slippageTolerance[token] = tolerance; diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index 1c0d518..d50c35b 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -87,6 +87,14 @@ contract UniversalCoreV0 is /// @notice Role for managing gas-related configurations. bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + // -- Uniswap V3 fee tiers -- + uint24 public constant FEE_TIER_LOW = 500; + uint24 public constant FEE_TIER_MEDIUM = 3000; + uint24 public constant FEE_TIER_HIGH = 10000; + + // -- Slippage cap (basis points) -- + uint256 public constant MAX_SLIPPAGE_BPS = 5000; + /// @notice (Deprecated) Base gas limit — now per-chain via baseGasLimitByChainNamespace. /// @dev Only included to avoid storage collision in Testnet UniversalCore. uint256 public BASE_GAS_LIMIT = 500_000; @@ -460,7 +468,7 @@ contract UniversalCoreV0 is /// @param feeTier Fee tier (500, 3000, 10000) function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != 500 && feeTier != 3000 && feeTier != 10000) { + if (feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; @@ -471,7 +479,7 @@ contract UniversalCoreV0 is /// @param tolerance Slippage tolerance in basis points (e.g., 300 = 3%) function setSlippageTolerance(address token, uint256 tolerance) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (tolerance > 5000) { + if (tolerance > MAX_SLIPPAGE_BPS) { revert UniversalCoreErrors.InvalidSlippageTolerance(); } slippageTolerance[token] = tolerance; From 706f3c1922864ad3e81ae92f3eb12151db95f336 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:31:08 +0530 Subject: [PATCH 15/56] =?UTF-8?q?fix(audit):=20F-2026-15562=20=E2=80=94=20?= =?UTF-8?q?remove=20unused=20SafeERC20=20import=20from=20CEA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CEA only uses IERC20.balanceOf — no transfers or approvals that would need SafeERC20 wrappers. Removed the dead import and using statement. --- src/cea/CEA.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 0a30373..433595e 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -8,7 +8,6 @@ import {IUniversalGateway, UniversalTxRequest} from "../interfaces/IUniversalGat import {Multicall, MULTICALL_SELECTOR, MIGRATION_SELECTOR} from "../libraries/Types.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** @@ -19,8 +18,6 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol * In v1 only the Vault may call state-changing functions. */ contract CEA is ICEA, ReentrancyGuard { - using SafeERC20 for IERC20; - // ========================= // CEA: STATE VARIABLES // ========================= From 820068a3f42b6c60e0b08e0c74dae8ccf905dafd Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:32:16 +0530 Subject: [PATCH 16/56] =?UTF-8?q?fix(audit):=20F-2026-15563=20=E2=80=94=20?= =?UTF-8?q?add=20zero-address=20check=20to=20UEAProxy.initializeUEA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UEAProxy now validates _logic != address(0) before storing the implementation, matching CEAProxy's initialization pattern. --- src/uea/UEAProxy.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/uea/UEAProxy.sol b/src/uea/UEAProxy.sol index 565d0b5..287cbd1 100644 --- a/src/uea/UEAProxy.sol +++ b/src/uea/UEAProxy.sol @@ -31,6 +31,10 @@ contract UEAProxy is Initializable, Proxy { /// @dev Can only be called once. Intended caller: UEAFactory. /// @param _logic Address of the UEA implementation contract function initializeUEA(address _logic) external initializer { + if (_logic == address(0)) { + revert UEAErrors.InvalidCall(); + } + address currentImpl = getImplementation(); if (currentImpl != address(0)) { revert UEAErrors.InvalidCall(); From b7d1f23de85647f0f3c0a89184d02c7a9c5b0ef8 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 30 Mar 2026 11:47:10 +0530 Subject: [PATCH 17/56] =?UTF-8?q?fix(audit):=20F-2026-15564=20=E2=80=94=20?= =?UTF-8?q?propagate=20revert=20data=20in=20CEA=20multicall=20and=20single?= =?UTF-8?q?=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture return data from failed low-level calls in _handleMulticall and _handleSingleCall. When revert data is available, propagate it via assembly revert. Fall back to CEAErrors.ExecutionFailed() when return data is empty. Matches the pattern already used in UEA_EVM. Updated 26 tests across 5 test files to expect actual underlying revert reasons. --- src/cea/CEA.sol | 26 +++++++++++++++++++++----- test/fuzz/CEA_Fuzz.t.sol | 4 ++-- test/fuzz/UEAProxy_Fuzz.t.sol | 12 +++--------- test/tests_cea/CEA.t.sol | 18 ++++++++---------- test/tests_cea/CEA_multicalls.t.sol | 27 +++++++++++++-------------- test/tests_cea/CEA_selfCalls.t.sol | 12 ++++++------ test/tests_cea/CEA_singleCall.t.sol | 2 +- 7 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 433595e..7d77310 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -190,9 +190,17 @@ contract CEA is ICEA, ReentrancyGuard { revert CEAErrors.InvalidInput(); } - (bool success,) = calls[i].to.call{value: calls[i].value}(calls[i].data); - - if (!success) revert CEAErrors.ExecutionFailed(); + (bool success, bytes memory returnData) = calls[i].to.call{value: calls[i].value}(calls[i].data); + + if (!success) { + if (returnData.length > 0) { + assembly { + revert(add(32, returnData), mload(returnData)) + } + } else { + revert CEAErrors.ExecutionFailed(); + } + } emit UniversalTxExecuted(txId, universalTxId, originCaller, calls[i].to, calls[i].data); } @@ -225,8 +233,16 @@ contract CEA is ICEA, ReentrancyGuard { revert CEAErrors.InvalidRecipient(); } - (bool success,) = recipient.call{value: msg.value}(payload); - if (!success) revert CEAErrors.ExecutionFailed(); + (bool success, bytes memory returnData) = recipient.call{value: msg.value}(payload); + if (!success) { + if (returnData.length > 0) { + assembly { + revert(add(32, returnData), mload(returnData)) + } + } else { + revert CEAErrors.ExecutionFailed(); + } + } emit UniversalTxExecuted(txId, universalTxId, originCaller, recipient, payload); } diff --git a/test/fuzz/CEA_Fuzz.t.sol b/test/fuzz/CEA_Fuzz.t.sol index c87cb46..a77b143 100644 --- a/test/fuzz/CEA_Fuzz.t.sol +++ b/test/fuzz/CEA_Fuzz.t.sol @@ -444,8 +444,8 @@ contract CEA_FuzzTest is Test { bytes memory payload = encodeCalls(calls); bytes32 txId = keccak256(abi.encode("insufficient_balance", amount)); - // The inner call reverts with InsufficientBalance, causing ExecutionFailed - vm.expectRevert(CEAErrors.ExecutionFailed.selector); + // The inner call reverts with InsufficientBalance, now propagated + vm.expectRevert(CEAErrors.InsufficientBalance.selector); vm.prank(vault); ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); } diff --git a/test/fuzz/UEAProxy_Fuzz.t.sol b/test/fuzz/UEAProxy_Fuzz.t.sol index c33a800..4e6666c 100644 --- a/test/fuzz/UEAProxy_Fuzz.t.sol +++ b/test/fuzz/UEAProxy_Fuzz.t.sol @@ -47,17 +47,11 @@ contract UEAProxy_Fuzz is Test { proxy.initializeUEA(logic2); } - function testFuzz_initializeUEA_zeroAddress_behavior(bytes calldata) public { - // initializeUEA(address(0)) stores address(0) in UEA_LOGIC_SLOT. - // A subsequent delegatecall then reverts because _implementation() checks for zero. + function testFuzz_initializeUEA_zeroAddress_reverts(bytes calldata) public { + // initializeUEA(address(0)) now reverts with InvalidCall (matching CEAProxy) UEAProxy proxy = new UEAProxy(); + vm.expectRevert(UEAErrors.InvalidCall.selector); proxy.initializeUEA(address(0)); - - assertEq(proxy.getImplementation(), address(0)); - - // Any external call to the proxy should revert (no implementation set) - (bool ok,) = address(proxy).call(abi.encodeWithSignature("getValue()")); - assertFalse(ok); } // ========================================================================= diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 027c20c..5e1623d 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -607,8 +607,8 @@ contract CEATest is Test { bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); - // Expect ExecutionFailed (revert data no longer bubbled) - vm.expectRevert(Errors.ExecutionFailed.selector); + // Underlying revert reason is now propagated + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); // txID should NOT be marked as executed when execution fails @@ -933,8 +933,7 @@ contract CEATest is Test { vm.prank(vault); // Calls initializeCEA via .call() which reverts with AlreadyInitialized - // but we now get ExecutionFailed instead of bubbled error - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert(Errors.AlreadyInitialized.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); } @@ -951,7 +950,7 @@ contract CEATest is Test { bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); @@ -1366,8 +1365,7 @@ contract CEATest is Test { vm.prank(vault); // Calls initializeCEA via .call() which reverts with AlreadyInitialized - // but we now get ExecutionFailed instead of bubbled error - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert(Errors.AlreadyInitialized.selector); ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); } @@ -1378,7 +1376,7 @@ contract CEATest is Test { bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); @@ -1629,8 +1627,8 @@ contract CEATest is Test { bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(reverter), 100 ether, payload); - // Expect ExecutionFailed (revert data no longer bubbled) - vm.expectRevert(Errors.ExecutionFailed.selector); + // Underlying revert reason is now propagated + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), multicallPayload ); diff --git a/test/tests_cea/CEA_multicalls.t.sol b/test/tests_cea/CEA_multicalls.t.sol index 5a72df4..031d86d 100644 --- a/test/tests_cea/CEA_multicalls.t.sol +++ b/test/tests_cea/CEA_multicalls.t.sol @@ -135,7 +135,7 @@ contract CEA_NewMulticallTests is CEATest { buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); } @@ -154,7 +154,7 @@ contract CEA_NewMulticallTests is CEATest { uint256 magicBefore = target.magicNumber(); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); // First call's effect should be rolled back @@ -171,7 +171,7 @@ contract CEA_NewMulticallTests is CEATest { buildExternalSingleCall(address(reverter), 0, abi.encodeWithSignature("revertWithReason()")); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); // txID should NOT be marked as executed since the tx reverted @@ -190,7 +190,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.recordLogs(); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -231,8 +231,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - // Calls initializeCEA via .call() which reverts with AlreadyInitialized - // but we now get ExecutionFailed instead of bubbled error + // Mismatched selector (3 params vs 4) — no function match, empty return data vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); } @@ -279,7 +278,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); } @@ -314,7 +313,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); } @@ -583,7 +582,7 @@ contract CEA_NewMulticallTests is CEATest { vm.deal(vault, 0.1 ether); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled from sendUniversalTxToUEA's InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); // Verify rollback - target should not have received ETH @@ -611,7 +610,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Gateway revert bubbled as ExecutionFailed + vm.expectRevert("Gateway intentionally reverted"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); // Verify txID not marked executed @@ -642,7 +641,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); // Verify allowance rolled back to 0 @@ -671,7 +670,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); // Verify gateway was NOT called (entire tx reverted before gateway interaction persisted) @@ -693,7 +692,7 @@ contract CEA_NewMulticallTests is CEATest { // First attempt - should revert vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), failingPayload); // Verify not marked executed @@ -856,7 +855,7 @@ contract CEA_NewMulticallTests is CEATest { uint256 targetBalanceBefore = address(target).balance; vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled error no longer shown + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx{value: transferAmount}(txID, universalTxID, ueaOnPush, address(0), payload); // Verify target balance unchanged (rollback) diff --git a/test/tests_cea/CEA_selfCalls.t.sol b/test/tests_cea/CEA_selfCalls.t.sol index 7e26e2a..90c594b 100644 --- a/test/tests_cea/CEA_selfCalls.t.sol +++ b/test/tests_cea/CEA_selfCalls.t.sol @@ -300,7 +300,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); // Bubbled InsufficientBalance + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); } @@ -416,7 +416,7 @@ contract CEA_ComprehensiveTests is CEATest { ); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx{value: 0}( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); @@ -439,7 +439,7 @@ contract CEA_ComprehensiveTests is CEATest { ); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert(Errors.InsufficientBalance.selector); ceaInstance.executeUniversalTx( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); @@ -791,7 +791,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes32 txID = generateTxID(1); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert("GatewayError"); ceaInstance.executeUniversalTx{value: 0}( txID, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); @@ -821,7 +821,7 @@ contract CEA_ComprehensiveTests is CEATest { ); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert("GatewayError"); ceaInstance.executeUniversalTx{value: 0}( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); @@ -958,7 +958,7 @@ contract CEA_ComprehensiveTests is CEATest { calls[0] = makeCall(address(ceaInstance), 0, buildSendToUEAPayload(address(0), 5 ether, address(0))); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert(Errors.InvalidInput.selector); ceaInstance.executeUniversalTx{value: 0}( generateTxID(1), generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); diff --git a/test/tests_cea/CEA_singleCall.t.sol b/test/tests_cea/CEA_singleCall.t.sol index ed7fbb2..e942f5b 100644 --- a/test/tests_cea/CEA_singleCall.t.sol +++ b/test/tests_cea/CEA_singleCall.t.sol @@ -175,7 +175,7 @@ contract CEA_SingleCallTests is CEATest { bytes memory payload = abi.encodeWithSignature("revertWithReason()"); vm.prank(vault); - vm.expectRevert(Errors.ExecutionFailed.selector); + vm.expectRevert("This function always reverts with reason"); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(reverter), payload); assertFalse( From 631bd23e1607cfde99ed6338ed2254f33b6fcced Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 17:31:49 +0530 Subject: [PATCH 18/56] =?UTF-8?q?fix(audit):=20F-2026-15561=20=E2=80=94=20?= =?UTF-8?q?approve=20gateway=20before=20ERC-20=20sendUniversalTxFromCEA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ERC-20 path in sendUniversalTxToUEA called the gateway without approving it to pull tokens. Added IERC20(token).approve(UNIVERSAL_GATEWAY, amount) before the gateway call so transferFrom succeeds. Updated 4 tests to reflect approval behavior. --- src/cea/CEA.sol | 1 + test/tests_cea/CEA.t.sol | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 7d77310..e2a3325 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -134,6 +134,7 @@ contract CEA is ICEA, ReentrancyGuard { if (IERC20(token).balanceOf(address(this)) < amount) { revert CEAErrors.InsufficientBalance(); } + IERC20(token).approve(UNIVERSAL_GATEWAY, amount); IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA(req); } } else { diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 5e1623d..613d543 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -1060,11 +1060,11 @@ contract CEATest is Test { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); - // Approval should be set to amount (gateway may or may not consume it) + // Approval persists after gateway call (gateway consumes via transferFrom in production) assertEq( token.allowance(address(ceaInstance), address(mockUniversalGateway)), 500 ether, - "Approval should be set to amount" + "Approval should persist (mock gateway doesn't consume)" ); } @@ -1082,9 +1082,9 @@ contract CEATest is Test { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); - // Gateway should have approval for exact amount + // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), amount, "Approval should match amount" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), amount, "Approval should persist" ); } @@ -1127,14 +1127,14 @@ contract CEATest is Test { ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); - // Gateway receives approval but mock doesn't transfer tokens - // So balance remains the same, but approval should be granted + // Mock gateway doesn't transfer tokens, so balance unchanged uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); + // Approval persists after gateway call (mock gateway doesn't consume) assertEq( token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, - "Gateway should have approval" + "Approval should persist" ); } @@ -1249,14 +1249,14 @@ contract CEATest is Test { assertEq(mockUniversalGateway.lastToken(), address(token), "Token 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 + // Mock gateway doesn't transfer tokens, so balance unchanged uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); + // Approval persists after gateway call (mock gateway doesn't consume) assertEq( token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, - "Gateway should have approval" + "Approval should persist" ); } From 10bf9b9ad95369994e8538fb4769f41597c38ae6 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 17:48:48 +0530 Subject: [PATCH 19/56] =?UTF-8?q?fix(audit):=20F-2026-15565=20=E2=80=94=20?= =?UTF-8?q?validate=20recipient=20in=20CEA=20migration=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _handleMigration now enforces recipient == address(this), matching UEA's self-targeting requirement for migrations. Previously the recipient param was ignored in the migration branch. Updated 25 tests across migration, integration, and fuzz suites to pass the CEA's own address as recipient. --- src/cea/CEA.sol | 8 ++++--- test/fuzz/CEAMigration_Fuzz.t.sol | 4 ++-- test/fuzz/CEA_Fuzz.t.sol | 4 ++-- test/tests_cea/CEA_singleCall.t.sol | 22 +++++++++++++++++-- .../CEAMigration_Integration.t.sol | 10 ++++----- test/tests_ceaMigration/CEA_Migration.t.sol | 12 +++++----- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index e2a3325..2ed6b9a 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -166,7 +166,7 @@ contract CEA is ICEA, ReentrancyGuard { Multicall[] memory calls = _decodeCalls(payload); _handleMulticall(txId, universalTxId, originCaller, calls); } else if (_isMigration(payload)) { - _handleMigration(); + _handleMigration(recipient); emit UniversalTxExecuted(txId, universalTxId, originCaller, address(this), payload); } else { _handleSingleCall(txId, universalTxId, originCaller, recipient, payload); @@ -249,8 +249,10 @@ contract CEA is ICEA, ReentrancyGuard { } /// @dev Fetches migration contract from factory and delegates. - /// Rejects msg.value > 0 — migration is a logic upgrade only. - function _handleMigration() internal { + /// Enforces: recipient must be self, no value transfer. + /// @param recipient Must be address(this) — migration targets self only + function _handleMigration(address recipient) internal { + if (recipient != address(this)) revert CEAErrors.InvalidRecipient(); if (msg.value != 0) revert CEAErrors.InvalidInput(); address migrationContract = factory.CEA_MIGRATION_CONTRACT(); if (migrationContract == address(0)) { diff --git a/test/fuzz/CEAMigration_Fuzz.t.sol b/test/fuzz/CEAMigration_Fuzz.t.sol index 6db313b..7885b81 100644 --- a/test/fuzz/CEAMigration_Fuzz.t.sol +++ b/test/fuzz/CEAMigration_Fuzz.t.sol @@ -86,7 +86,7 @@ contract CEAMigration_FuzzTest is Test { bytes32 txId = keccak256("migration_slot_test"); vm.prank(vault); - ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, ceaAddr, payload); // Verify slot was updated to ceaV2 bytes32 slotAfter = vm.load(ceaAddr, CEA_LOGIC_SLOT); @@ -107,7 +107,7 @@ contract CEAMigration_FuzzTest is Test { emit CEAMigration.ImplementationUpdated(address(ceaV2)); vm.prank(vault); - ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, ceaAddr, payload); } // ========================================================================= diff --git a/test/fuzz/CEA_Fuzz.t.sol b/test/fuzz/CEA_Fuzz.t.sol index a77b143..2ea5279 100644 --- a/test/fuzz/CEA_Fuzz.t.sol +++ b/test/fuzz/CEA_Fuzz.t.sol @@ -340,7 +340,7 @@ contract CEA_FuzzTest is Test { vm.expectRevert(CEAErrors.InvalidInput.selector); vm.prank(vault); - ceaInstance.executeUniversalTx{value: value}(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: value}(txId, bytes32(0), ueaOnPush, address(ceaInstance), payload); } /// @dev When factory has no migration contract set, migration reverts with InvalidCall. @@ -350,7 +350,7 @@ contract CEA_FuzzTest is Test { vm.expectRevert(CEAErrors.InvalidCall.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(ceaInstance), payload); } // ========================================================================= diff --git a/test/tests_cea/CEA_singleCall.t.sol b/test/tests_cea/CEA_singleCall.t.sol index e942f5b..2ca36f1 100644 --- a/test/tests_cea/CEA_singleCall.t.sol +++ b/test/tests_cea/CEA_singleCall.t.sol @@ -240,7 +240,7 @@ contract CEA_SingleCallTests is CEATest { assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); } - function test_MigrationPayload_IgnoresRecipient() public deployCEA { + function test_MigrationPayload_RevertsWhenRecipientNotSelf() public deployCEA { // Set up migration contract CEA ceaV2 = new CEA(); CEAMigration migration = new CEAMigration(address(ceaV2)); @@ -252,13 +252,31 @@ contract CEA_SingleCallTests is CEATest { bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); address randomRecipient = makeAddr("randomRecipient"); + // Migration with non-self recipient should revert vm.prank(vault); + vm.expectRevert(Errors.InvalidRecipient.selector); ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, randomRecipient, payload); + } + + function test_MigrationPayload_SucceedsWhenRecipientIsSelf() public deployCEA { + // Set up migration contract + CEA ceaV2 = new CEA(); + CEAMigration migration = new CEAMigration(address(ceaV2)); + factory.setCEAMigrationContract(address(migration)); + + bytes32 txID = generateTxID(1); + bytes32 universalTxID = generateUniversalTxID(1); + + bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); + + // Migration with self recipient should succeed + vm.prank(vault); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); assertEq( CEAProxy(payable(address(ceaInstance))).getImplementation(), address(ceaV2), - "Migration should execute normally regardless of recipient" + "Migration should succeed with self recipient" ); } } diff --git a/test/tests_ceaMigration/CEAMigration_Integration.t.sol b/test/tests_ceaMigration/CEAMigration_Integration.t.sol index 8169a4f..8cd0811 100644 --- a/test/tests_ceaMigration/CEAMigration_Integration.t.sol +++ b/test/tests_ceaMigration/CEAMigration_Integration.t.sol @@ -97,7 +97,7 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = buildMigrationPayload(address(ceaInstance)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -295,12 +295,12 @@ contract CEAMigration_IntegrationTest is Test { // Execute migration vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); // Attempt to replay same migration vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -355,7 +355,7 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = buildMigrationPayload(address(ceaInstance)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); assertEq( CEAProxy(payable(address(ceaInstance))).getImplementation(), @@ -414,7 +414,7 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = buildMigrationPayload(freshCEA); vm.prank(vault); - freshCEAInstance.executeUniversalTx(txID, universalTxID, freshUEA, address(0), payload); + freshCEAInstance.executeUniversalTx(txID, universalTxID, freshUEA, address(freshCEAInstance), payload); // Verify migration successful assertEq( diff --git a/test/tests_ceaMigration/CEA_Migration.t.sol b/test/tests_ceaMigration/CEA_Migration.t.sol index a6fbf51..4b11568 100644 --- a/test/tests_ceaMigration/CEA_Migration.t.sol +++ b/test/tests_ceaMigration/CEA_Migration.t.sol @@ -116,7 +116,7 @@ contract CEA_MigrationTest is Test { // Execute migration (will test isMigration detection internally) vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); // If execution reaches here without reverting, isMigration worked assertTrue(true, "Migration selector detected successfully"); @@ -137,7 +137,7 @@ contract CEA_MigrationTest is Test { bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); // Verify implementation changed address implAfter = CEAProxy(payable(address(ceaInstance))).getImplementation(); @@ -155,7 +155,7 @@ contract CEA_MigrationTest is Test { vm.prank(vault); vm.expectRevert(Errors.InvalidInput.selector); - ceaInstance.executeUniversalTx{value: 1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 1 ether}(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); } function test_handleMigration_MigrationInsideMulticall_Reverts() public { @@ -188,7 +188,7 @@ contract CEA_MigrationTest is Test { // Expect InvalidCall revert vm.prank(vault); vm.expectRevert(Errors.InvalidCall.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -260,7 +260,7 @@ contract CEA_MigrationTest is Test { // Execute migration vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); // Get updated implementation address implAfter = CEAProxy(payable(address(ceaInstance))).getImplementation(); @@ -283,7 +283,7 @@ contract CEA_MigrationTest is Test { vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); } } From 08d2c78bb732cbadab52247c9c49cda26e4be06d Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 17:50:44 +0530 Subject: [PATCH 20/56] =?UTF-8?q?fix(audit):=20F-2026-15512=20=E2=80=94=20?= =?UTF-8?q?set=20defaultDeadlineMins=20in=20initialize()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline initializer defaultDeadlineMins = 20 ran in the implementation constructor context, not the proxy. Proxy storage read 0, making swap deadlines ineffective. Moved assignment into initialize() so the value writes to proxy storage. --- src/UniversalCore.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index e9acb03..ed02ebd 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -80,7 +80,7 @@ contract UniversalCore is mapping(address => bool) public isAutoSwapSupported; mapping(address => uint24) public defaultFeeTier; mapping(address => uint256) public slippageTolerance; - uint256 public defaultDeadlineMins = 20; + uint256 public defaultDeadlineMins; // ========================= // UC: MODIFIERS @@ -138,6 +138,7 @@ contract UniversalCore is WPC = wpc_; uniswapV3Factory = uniswapV3Factory_; uniswapV3SwapRouter = uniswapV3SwapRouter_; + defaultDeadlineMins = 20; } // ========================= From 63c48034264fe9793aef62c3d2e907502640d9ca Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 17:57:13 +0530 Subject: [PATCH 21/56] =?UTF-8?q?fix(audit):=20F-2026-15501=20=E2=80=94=20?= =?UTF-8?q?remove=20unused=20isSupportedToken=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isSupportedToken mapping and setSupportedToken setter were configured by admin but never checked in any execution path. Removed entirely from UniversalCore (not yet deployed on mainnet). Deprecated with __deprecated_ prefix in UniversalCoreV0 to preserve testnet storage layout. Removed from both interfaces and all related tests. --- src/Interfaces/IUniversalCore.sol | 6 -- src/UniversalCore.sol | 10 -- src/testnetV0/IUniversalCoreV0.sol | 8 -- src/testnetV0/UniversalCoreV0.sol | 13 +-- test/fuzz/UniversalCore_Fuzz.t.sol | 5 - test/tests_token_and_core/UniversalCore.t.sol | 92 ------------------- .../UniversalCoreSwapFee.t.sol | 1 - 7 files changed, 2 insertions(+), 133 deletions(-) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 555ab84..534c875 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -12,7 +12,6 @@ interface IUniversalCore { event SetChainMeta(string chainNamespace, uint256 price, uint256 chainHeight, uint256 observedAt); event SetGasToken(string chainNamespace, address prc20); event SetDefaultDeadlineMins(uint256 minutesValue); - event SetSupportedToken(address indexed prc20, bool supported); event SetGasPCPool(string chainNamespace, address pool, uint24 fee); event DepositPRC20WithAutoSwap( address prc20, uint256 amountIn, address pcToken, uint256 amountOut, uint24 fee, address recipient @@ -112,11 +111,6 @@ interface IUniversalCore { // UC_3: PUBLIC GETTERS // ========================= - /// @notice Check if a PRC20 token is supported. - /// @param prc20 PRC20 token address - /// @return supported Whether the token is supported - function isSupportedToken(address prc20) external view returns (bool supported); - /// @notice Get gas token PRC20 address for a chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @return gasToken Gas token address diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index ed02ebd..63e1739 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -67,7 +67,6 @@ contract UniversalCore is // -- Token configuration -- - mapping(address => bool) public isSupportedToken; mapping(address => uint256) public protocolFeeByToken; // -- Uniswap and AMM specific states -- @@ -326,15 +325,6 @@ contract UniversalCore is emit SetProtocolFeeByToken(token, fee); } - /// @notice Set whether a PRC20 token is supported. - /// @param prc20 PRC20 token address - /// @param supported Whether the token is supported - function setSupportedToken(address prc20, bool supported) external onlyRole(MANAGER_ROLE) { - if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); - isSupportedToken[prc20] = supported; - emit SetSupportedToken(prc20, supported); - } - /// @notice Set the gas PC pool for a chain (informational — not enforced at runtime). /// @dev The stored pool is for off-chain observability only. Runtime swap flows /// (swapAndBurnGas, _autoSwap) resolve pools dynamically from the factory. diff --git a/src/testnetV0/IUniversalCoreV0.sol b/src/testnetV0/IUniversalCoreV0.sol index 0ac313b..670ca63 100644 --- a/src/testnetV0/IUniversalCoreV0.sol +++ b/src/testnetV0/IUniversalCoreV0.sol @@ -18,7 +18,6 @@ interface IUniversalCoreV0 { event SetGasPrice(string chainNamespace, uint256 price); event SetGasToken(string chainNamespace, address prc20); event SetDefaultDeadlineMins(uint256 minutesValue); - event SetSupportedToken(address indexed prc20, bool supported); event SetGasPCPool( string chainNamespace, address pool, uint24 fee ); @@ -132,13 +131,6 @@ interface IUniversalCoreV0 { // UCV0_3: PUBLIC GETTERS // ========================= - /// @notice Check if a PRC20 token is supported. - /// @param prc20 PRC20 token address - /// @return supported Whether the token is supported - function isSupportedToken( - address prc20 - ) external view returns (bool supported); - /// @notice Get gas token PRC20 address for a chain. /// @param chainNamespace Chain Namespace /// @return gasToken Gas token address diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index d50c35b..e5ae4ab 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -99,8 +99,8 @@ contract UniversalCoreV0 is /// @dev Only included to avoid storage collision in Testnet UniversalCore. uint256 public BASE_GAS_LIMIT = 500_000; - /// @notice Mapping for indicating an official PRC20 supported token. - mapping(address => bool) public isSupportedToken; + /// @dev Deprecated. Slot retained for storage layout compatibility with deployed testnet proxy. + mapping(address => bool) private __deprecated_isSupportedToken; /// @notice Address of the UniversalGatewayPC that can call swapAndBurnGas. address public universalGatewayPC; @@ -358,15 +358,6 @@ contract UniversalCoreV0 is emit SetProtocolFeeByToken(token, fee); } - /// @notice Set whether a PRC20 token is supported. - /// @param prc20 PRC20 token address - /// @param supported Whether the token is supported - function setSupportedToken(address prc20, bool supported) external onlyRole(MANAGER_ROLE) { - if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); - isSupportedToken[prc20] = supported; - emit SetSupportedToken(prc20, supported); - } - /// @notice Set the gas PC pool for a chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasToken Gas coin address diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index da26dbd..f0d06cc 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -404,9 +404,4 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setProtocolFeeByToken(address(0), fee); } - function testFuzz_setSupportedToken_zeroAddress_reverts(bool supported) public { - vm.prank(uExec); - vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setSupportedToken(address(0), supported); - } } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 87d69b9..9726598 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -61,7 +61,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { ); event Paused(address account); event Unpaused(address account); - event SetSupportedToken(address indexed prc20, bool supported); event SetChainMeta(string chainNamespace, uint256 price, uint256 chainHeight, uint256 observedAt); event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); @@ -797,97 +796,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { universalCore.getOutboundTxGasAndFees(address(prc20Token), belowBase); } - // ======================================== - // 6) Set Supported Token Tests - // ======================================== - - function test_SetSupportedToken_OnlyManagerRole() public { - address token = makeAddr("token"); - - // Non-manager should revert - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonUEModule, universalCore.MANAGER_ROLE() - ) - ); - vm.prank(nonUEModule); - universalCore.setSupportedToken(token, true); - - // MANAGER_ROLE (UNIVERSAL_EXECUTOR_MODULE) should succeed - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setSupportedToken(token, true); - assertTrue(universalCore.isSupportedToken(token)); - } - - function test_SetSupportedToken_HappyPath_SetTrue() public { - address token = makeAddr("token"); - - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setSupportedToken(token, true); - assertTrue(universalCore.isSupportedToken(token)); - } - - function test_SetSupportedToken_HappyPath_SetFalse() public { - address token = makeAddr("token"); - - // First set to true - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setSupportedToken(token, true); - assertTrue(universalCore.isSupportedToken(token)); - - // Then set to false - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setSupportedToken(token, false); - assertFalse(universalCore.isSupportedToken(token)); - } - - function test_SetSupportedToken_FlipFalseToTrue() public { - address token = makeAddr("token"); - - // Initially false (default) - assertFalse(universalCore.isSupportedToken(token)); - - // Flip to true - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setSupportedToken(token, true); - assertTrue(universalCore.isSupportedToken(token)); - } - - function test_SetSupportedToken_ZeroAddressReverts() public { - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setSupportedToken(address(0), true); - } - - function test_SetSupportedToken_EmitsEvent() public { - address token = makeAddr("token"); - - // Test event emission when setting to true - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - vm.expectEmit(true, false, false, false); - emit SetSupportedToken(token, true); - universalCore.setSupportedToken(token, true); - - // Test event emission when setting to false - vm.prank(UNIVERSAL_EXECUTOR_MODULE); - vm.expectEmit(true, false, false, false); - emit SetSupportedToken(token, false); - universalCore.setSupportedToken(token, false); - } - - function test_SetSupportedToken_OwnerCannotCall() public { - address token = makeAddr("token"); - - // Owner (deployer) should not be able to call without MANAGER_ROLE - vm.prank(deployer); - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, deployer, universalCore.MANAGER_ROLE() - ) - ); - universalCore.setSupportedToken(token, true); - } - // ======================================== // 7) setChainMeta Tests // ======================================== diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index 13f6385..2dab878 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -378,7 +378,6 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { function test_ExistingStorage_Preserved() public view { assertEq(universalCore.gasPriceByChainNamespace(CHAIN_NAMESPACE), GAS_PRICE); assertEq(universalCore.gasTokenPRC20ByChainNamespace(CHAIN_NAMESPACE), address(gasTokenMock)); - assertTrue(universalCore.isSupportedToken(address(gasTokenMock)) == false); assertEq(universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE), 500_000); } From d9a0f6e99061df42b6957843960ba0d85381a2f6 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:24:54 +0530 Subject: [PATCH 22/56] =?UTF-8?q?fix(audit):=20F-2026-15587=20=E2=80=94=20?= =?UTF-8?q?propagate=20UniversalCore=20pause=20to=20PRC20=20deposit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRC20.deposit() now checks if UNIVERSAL_CORE is paused before minting, preventing the UNIVERSAL_EXECUTOR_MODULE from bypassing pause via the direct deposit path. Added CorePaused custom error and 3 tests for pause propagation. Fixed stale 5-arg initialize calls in PRC20 tests. --- src/PRC20.sol | 4 +++ src/libraries/Errors.sol | 1 + test/tests_token_and_core/PRC20.t.sol | 45 +++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/PRC20.sol b/src/PRC20.sol index 5556824..0d90a79 100644 --- a/src/PRC20.sol +++ b/src/PRC20.sol @@ -5,6 +5,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {IPRC20} from "./interfaces/IPRC20.sol"; import {PRC20Errors, CommonErrors} from "./libraries/Errors.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; /** * @title PRC20 (Push Chain Synthetic Token) @@ -171,6 +172,9 @@ contract PRC20 is IPRC20, Initializable { if (msg.sender != UNIVERSAL_CORE && msg.sender != UNIVERSAL_EXECUTOR_MODULE) { revert PRC20Errors.InvalidSender(); } + if (PausableUpgradeable(UNIVERSAL_CORE).paused()) { + revert PRC20Errors.CorePaused(); + } _mint(to, amount); diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index cd805a3..173b3cc 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -29,6 +29,7 @@ library PRC20Errors { error LowAllowance(); error InvalidSender(); error CallerIsNotUniversalExecutor(); + error CorePaused(); } // ========================= diff --git a/test/tests_token_and_core/PRC20.t.sol b/test/tests_token_and_core/PRC20.t.sol index d98510f..3d7a8f8 100644 --- a/test/tests_token_and_core/PRC20.t.sol +++ b/test/tests_token_and_core/PRC20.t.sol @@ -60,7 +60,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { address mockWPC = makeAddr("wPC"); address mockUniswapFactory = makeAddr("uniswapFactory"); address mockUniswapRouter = makeAddr("uniswapRouter"); - address mockUniswapQuoter = makeAddr("uniswapQuoter"); // Deploy universalCore implementation universalCoreImplementation = new UniversalCore(); @@ -71,7 +70,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { mockWPC, mockUniswapFactory, mockUniswapRouter, - mockUniswapQuoter, makeAddr("pauser") ); @@ -472,6 +470,45 @@ contract PRC20Test is Test, UpgradeableContractHelper { assertEq(prc20.totalSupply(), initialSupply + amount); } + // ========================================================================= + // PAUSE PROPAGATION TESTS + // ========================================================================= + + function testDepositRevertsWhenCorePaused_ViaModule() public { + // Pause UniversalCore + vm.prank(makeAddr("pauser")); + universalCore.pause(); + + // Module tries to deposit directly to PRC20 — should revert + vm.prank(uExec); + vm.expectRevert(PRC20Errors.CorePaused.selector); + prc20.deposit(bob, 1000 ether); + } + + function testDepositRevertsWhenCorePaused_ViaCore() public { + // Pause UniversalCore + vm.prank(makeAddr("pauser")); + universalCore.pause(); + + // Core tries to deposit — should also revert + vm.prank(address(universalCore)); + vm.expectRevert(PRC20Errors.CorePaused.selector); + prc20.deposit(bob, 1000 ether); + } + + function testDepositSucceedsAfterUnpause() public { + // Pause then unpause + vm.prank(makeAddr("pauser")); + universalCore.pause(); + vm.prank(makeAddr("pauser")); + universalCore.unpause(); + + // Deposit should succeed + vm.prank(uExec); + bool success = prc20.deposit(bob, 1000 ether); + assertTrue(success); + } + // ========================================================================= // ADMIN & GOVERNANCE CONTROLS // ========================================================================= @@ -481,7 +518,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { address mockWPC = makeAddr("newWPC"); address mockUniswapFactory = makeAddr("newUniswapFactory"); address mockUniswapRouter = makeAddr("newUniswapRouter"); - address mockUniswapQuoter = makeAddr("newUniswapQuoter"); vm.prank(uExec); // Deploy new universalCore implementation @@ -493,7 +529,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { mockWPC, mockUniswapFactory, mockUniswapRouter, - mockUniswapQuoter, makeAddr("pauser") ); @@ -518,7 +553,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { address mockWPC = makeAddr("newWPC"); address mockUniswapFactory = makeAddr("newUniswapFactory"); address mockUniswapRouter = makeAddr("newUniswapRouter"); - address mockUniswapQuoter = makeAddr("newUniswapQuoter"); vm.prank(uExec); // Deploy new universalCore implementation @@ -530,7 +564,6 @@ contract PRC20Test is Test, UpgradeableContractHelper { mockWPC, mockUniswapFactory, mockUniswapRouter, - mockUniswapQuoter, makeAddr("pauser") ); From 4f2de47322baa0ba1cee4760d3a1a8583aae0e18 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:29:00 +0530 Subject: [PATCH 23/56] =?UTF-8?q?fix(audit):=20F-2026-15546=20=E2=80=94=20?= =?UTF-8?q?reset=20gas=20price=20when=20gas=20token=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setGasTokenPRC20 now zeroes gasPriceByChainNamespace for the affected chain, forcing explicit reconfiguration via setChainMeta. Prevents stale gas price data denominated in the old token from producing incorrect fee calculations. Updated test setUp ordering to configure gas token before gas price. --- src/UniversalCore.sol | 1 + test/tests_token_and_core/UniversalCore.t.sol | 5 +++-- test/tests_token_and_core/UniversalCoreSwapFee.t.sol | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 63e1739..94bbd65 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -364,6 +364,7 @@ contract UniversalCore is function setGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(MANAGER_ROLE) { if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); gasTokenPRC20ByChainNamespace[chainNamespace] = prc20; + gasPriceByChainNamespace[chainNamespace] = 0; emit SetGasToken(chainNamespace, prc20); } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 9726598..b1e16f7 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -123,10 +123,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Grant MANAGER_ROLE to UE Module for manager functions universalCore.grantRole(universalCore.MANAGER_ROLE(), UNIVERSAL_EXECUTOR_MODULE); - // Configure gas price, gas token, base gas limit, and protocol fee for testing + // Configure gas token first, then gas price (setGasTokenPRC20 resets gas price to 0, + // so setChainMeta must come after to preserve the configured gas price). vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(mockPRC20)); + universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, BASE_GAS_LIMIT); universalCore.setProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); vm.stopPrank(); diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index 2dab878..d2dd1ff 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -91,10 +91,11 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { // Grant MANAGER_ROLE to UE Module for manager functions universalCore.grantRole(universalCore.MANAGER_ROLE(), UNIVERSAL_EXECUTOR_MODULE); - // Configure gas token, gas price, base gas limit, and protocol fee + // Configure gas token first, then gas price (setGasTokenPRC20 resets gas price to 0, + // so setChainMeta must come after to preserve the configured gas price). vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(gasTokenMock)); + universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, 500_000); universalCore.setProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); vm.stopPrank(); From 0e3af5cdb8ebb511323b1387ecf1b0e9bbd2516c Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:34:25 +0530 Subject: [PATCH 24/56] =?UTF-8?q?fix(audit):=20F-2026-15541=20=E2=80=94=20?= =?UTF-8?q?reject=20zero=20gas=20price=20in=20setChainMeta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setChainMeta now reverts with ZeroGasPrice when price is 0, preventing storage of invalid gas price that breaks downstream fee quote functions. Fails at write time instead of read time. Updated tests to expect the revert and use fresh namespaces for zero-price scenarios. --- src/UniversalCore.sol | 1 + test/fuzz/UniversalCore_Fuzz.t.sol | 31 ++++++++++++---- test/tests_token_and_core/UniversalCore.t.sol | 37 ++++++++++++++++--- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 94bbd65..1a8288b 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -352,6 +352,7 @@ contract UniversalCore is external onlyUEModule { + if (price == 0) revert UniversalCoreErrors.ZeroGasPrice(); gasPriceByChainNamespace[chainNamespace] = price; chainHeightByChainNamespace[chainNamespace] = chainHeight; timestampObservedAtByChainNamespace[chainNamespace] = block.timestamp; diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index f0d06cc..b0505ae 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -120,16 +120,33 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { uint256 baseLimit = 100_000; vm.assume(gasLimit >= baseLimit); - // Set base gas limit so we pass the zero-base check - vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, baseLimit); + // Use a fresh chain namespace where setChainMeta is never called, + // so gasPriceByChainNamespace is 0 by default in storage. + string memory zeroPriceNs = "zeroprice"; - // Set gas price to 0 — setChainMeta is onlyUEModule - vm.prank(uExec); - universalCore.setChainMeta(CHAIN_NS, 0, 0); + // Deploy a fresh PRC20 on this namespace + PRC20 prc20Impl = new PRC20(); + bytes memory prc20Init = abi.encodeWithSelector( + PRC20.initialize.selector, + "ZeroPrice", + "ZP", + 18, + zeroPriceNs, + IPRC20.TokenType.NATIVE, + address(universalCore), + "0x0" + ); + address prc20Addr = deployUpgradeableContract(address(prc20Impl), prc20Init); + PRC20 zeroPricePRC20 = PRC20(payable(prc20Addr)); + + // Set gas token and base gas limit, but never call setChainMeta → price stays 0 + vm.startPrank(uExec); + universalCore.setGasTokenPRC20(zeroPriceNs, address(gasToken)); + universalCore.setBaseGasLimitByChain(zeroPriceNs, baseLimit); + vm.stopPrank(); vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); - universalCore.getOutboundTxGasAndFees(address(prc20), gasLimit); + universalCore.getOutboundTxGasAndFees(address(zeroPricePRC20), gasLimit); } function testFuzz_getOutboundTxGasAndFees_zeroGasToken_reverts(uint128 gasLimit) public { diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index b1e16f7..e4ebe8b 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -677,14 +677,32 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } function testWithdrawGasFeeZeroGasPrice() public { + // Use a fresh chain namespace with gas token set but no setChainMeta call, + // so gasPriceByChainNamespace is 0 by default in storage. + string memory newNs = "eip155:9999"; + + PRC20 newPrc20Impl = new PRC20(); + bytes memory initData = abi.encodeWithSelector( + PRC20.initialize.selector, + "Zero Price PRC20", + "ZP", + 18, + newNs, + IPRC20.TokenType.ERC20, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + address proxyAddr = deployUpgradeableContract(address(newPrc20Impl), initData); + PRC20 newToken = PRC20(payable(proxyAddr)); + + // Set gas token and base gas limit, but never call setChainMeta → price stays 0 vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - // Set gas price to zero - universalCore.setChainMeta(CHAIN_NAMESPACE, 0, 0); + universalCore.setGasTokenPRC20(newNs, address(mockPRC20)); + universalCore.setBaseGasLimitByChain(newNs, BASE_GAS_LIMIT); vm.stopPrank(); - // Expect revert when getting gas fee vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); - universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + universalCore.getOutboundTxGasAndFees(address(newToken), BASE_GAS_LIMIT); } function testWithdrawGasFeeZeroGasToken() public { @@ -876,11 +894,18 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertEq(universalCore.timestampObservedAtByChainNamespace(bscChain), block.timestamp); } - function test_SetChainMeta_ZeroValuesAllowed() public { + function test_SetChainMeta_ZeroPriceReverts() public { + vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.setChainMeta(CHAIN_NAMESPACE, 0, 0); + } + + function test_SetChainMeta_ZeroChainHeightAllowed() public { + // price must be non-zero, but chainHeight=0 is valid + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); - assertEq(universalCore.gasPriceByChainNamespace(CHAIN_NAMESPACE), 0); + assertEq(universalCore.gasPriceByChainNamespace(CHAIN_NAMESPACE), GAS_PRICE); assertEq(universalCore.chainHeightByChainNamespace(CHAIN_NAMESPACE), 0); assertEq(universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE), block.timestamp); } From d56cecc52556d2e71e0ab42920f6c4fca96cdd59 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:37:12 +0530 Subject: [PATCH 25/56] =?UTF-8?q?fix(audit):=20F-2026-15517=20=E2=80=94=20?= =?UTF-8?q?emit=20actual=20caller=20in=20PRC20=20deposit=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deposit event now uses abi.encodePacked(msg.sender) instead of the hardcoded UNIVERSAL_EXECUTOR_MODULE address. Off-chain indexers can now distinguish deposits initiated by UniversalCore vs the module directly. --- src/PRC20.sol | 2 +- test/tests_token_and_core/PRC20.t.sol | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PRC20.sol b/src/PRC20.sol index 0d90a79..08c22c3 100644 --- a/src/PRC20.sol +++ b/src/PRC20.sol @@ -178,7 +178,7 @@ contract PRC20 is IPRC20, Initializable { _mint(to, amount); - emit Deposit(abi.encodePacked(UNIVERSAL_EXECUTOR_MODULE), to, amount); + emit Deposit(abi.encodePacked(msg.sender), to, amount); return true; } diff --git a/test/tests_token_and_core/PRC20.t.sol b/test/tests_token_and_core/PRC20.t.sol index 3d7a8f8..b4ba46b 100644 --- a/test/tests_token_and_core/PRC20.t.sol +++ b/test/tests_token_and_core/PRC20.t.sol @@ -363,9 +363,9 @@ contract PRC20Test is Test, UpgradeableContractHelper { vm.expectEmit(true, true, false, true); emit Transfer(address(0), bob, depositAmount); - // Expect Deposit event with UNIVERSAL_EXECUTOR_MODULE as from (encoded as bytes) + // Expect Deposit event with msg.sender (universalCore) as from (encoded as bytes) vm.expectEmit(false, true, false, true); - emit Deposit(abi.encodePacked(uExec), bob, depositAmount); + emit Deposit(abi.encodePacked(address(universalCore)), bob, depositAmount); bool success = prc20.deposit(bob, depositAmount); @@ -448,9 +448,9 @@ contract PRC20Test is Test, UpgradeableContractHelper { assertEq(to, bob); assertEq(amount, depositAmount); - // Verify the from field is encoded as UNIVERSAL_EXECUTOR_MODULE, not universalCore + // Verify the from field is encoded as msg.sender (universalCore), not UNIVERSAL_EXECUTOR_MODULE assertEq(from.length, 20); // Should be 20 bytes (address length) - assertEq(address(bytes20(from)), uExec); + assertEq(address(bytes20(from)), address(universalCore)); } function testFuzzDeposit(address to, uint96 amount) public { From 345b7fdf1955f46b9a5113deb3772ebf0e34c0e9 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:42:28 +0530 Subject: [PATCH 26/56] =?UTF-8?q?fix(audit):=20F-2026-15513=20=E2=80=94=20?= =?UTF-8?q?add=20100=20bps=20fee=20tier=20to=20setDefaultFeeTier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added FEE_TIER_LOWEST (100) constant for Uniswap V3's 1 bps pools used by highly correlated pairs. Included in setDefaultFeeTier validation in both UniversalCore and UniversalCoreV0. Updated tests to reflect the expanded valid tier set. --- src/UniversalCore.sol | 3 ++- src/testnetV0/UniversalCoreV0.sol | 3 ++- test/fuzz/UniversalCore_Fuzz.t.sol | 2 +- test/tests_token_and_core/UniversalCore.t.sol | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 1a8288b..7ed4fd2 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -45,6 +45,7 @@ contract UniversalCore is bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // -- Uniswap V3 fee tiers -- + uint24 public constant FEE_TIER_LOWEST = 100; uint24 public constant FEE_TIER_LOW = 500; uint24 public constant FEE_TIER_MEDIUM = 3000; uint24 public constant FEE_TIER_HIGH = 10000; @@ -416,7 +417,7 @@ contract UniversalCore is /// @param feeTier Fee tier (500, 3000, 10000) function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { + if (feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index e5ae4ab..defc50d 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -88,6 +88,7 @@ contract UniversalCoreV0 is bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); // -- Uniswap V3 fee tiers -- + uint24 public constant FEE_TIER_LOWEST = 100; uint24 public constant FEE_TIER_LOW = 500; uint24 public constant FEE_TIER_MEDIUM = 3000; uint24 public constant FEE_TIER_HIGH = 10000; @@ -459,7 +460,7 @@ contract UniversalCoreV0 is /// @param feeTier Fee tier (500, 3000, 10000) function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { + if (feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index b0505ae..078fe7f 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -266,7 +266,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { function testFuzz_setDefaultFeeTier_validTiers(address token, uint24 feeTier) public { vm.assume(token != address(0)); - bool isValid = feeTier == 500 || feeTier == 3000 || feeTier == 10000; + bool isValid = feeTier == 100 || feeTier == 500 || feeTier == 3000 || feeTier == 10000; if (isValid) { universalCore.setDefaultFeeTier(token, feeTier); diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index e4ebe8b..aee6ba6 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -1130,7 +1130,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address token = makeAddr("token"); vm.prank(deployer); vm.expectRevert(UniversalCoreErrors.InvalidFeeTier.selector); - universalCore.setDefaultFeeTier(token, 100); + universalCore.setDefaultFeeTier(token, 200); } function test_SetDefaultFeeTier_RevertsZeroAddress() public { From 2add6a6470202e092047aae1c0cbffedf3cda3b6 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:50:51 +0530 Subject: [PATCH 27/56] =?UTF-8?q?fix(audit):=20F-2026-15507=20=E2=80=94=20?= =?UTF-8?q?remove=20unused=20slippageTolerance=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit slippageTolerance mapping and setSlippageTolerance setter were configured by admin but never read in swap logic. Slippage protection is caller- controlled via minPCOut. Removed mapping, setter, event, MAX_SLIPPAGE_BPS constant, and InvalidSlippageTolerance error from UniversalCore. Deprecated in UniversalCoreV0 for storage layout compatibility. --- src/Interfaces/IUniversalCore.sol | 1 - src/UniversalCore.sol | 16 -------- src/libraries/Errors.sol | 1 - src/testnetV0/UniversalCoreV0.sol | 15 +------ test/fork/ForkUniversalCore.t.sol | 1 - test/fuzz/UniversalCore_Fuzz.t.sol | 12 ------ test/tests_token_and_core/UniversalCore.t.sol | 40 ------------------- .../UniversalCoreRefund.t.sol | 1 - .../UniversalCoreSwapFee.t.sol | 3 +- 9 files changed, 3 insertions(+), 87 deletions(-) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 534c875..e3f3e9e 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -29,7 +29,6 @@ interface IUniversalCore { event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); event SetUniswapV3Addresses(address factory, address swapRouter); event SetDefaultFeeTier(address indexed token, uint24 feeTier); - event SetSlippageTolerance(address indexed token, uint256 tolerance); /// @notice Emitted when the PAUSER_ROLE is granted to a new address. /// @param pauser Address that was granted the pauser role diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 7ed4fd2..8fa8715 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -50,9 +50,6 @@ contract UniversalCore is uint24 public constant FEE_TIER_MEDIUM = 3000; uint24 public constant FEE_TIER_HIGH = 10000; - // -- Slippage cap (basis points) -- - uint256 public constant MAX_SLIPPAGE_BPS = 5000; - // -- Protocol addresses -- address public universalGatewayPC; address public WPC; @@ -79,7 +76,6 @@ contract UniversalCore is mapping(string => address) public gasPCPoolByChainNamespace; mapping(address => bool) public isAutoSwapSupported; mapping(address => uint24) public defaultFeeTier; - mapping(address => uint256) public slippageTolerance; uint256 public defaultDeadlineMins; // ========================= @@ -424,18 +420,6 @@ contract UniversalCore is emit SetDefaultFeeTier(token, feeTier); } - /// @notice Set slippage tolerance for a token. - /// @param token Token address - /// @param tolerance Slippage tolerance in basis points (e.g., 300 = 3%) - function setSlippageTolerance(address token, uint256 tolerance) external onlyAdmin { - if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (tolerance > MAX_SLIPPAGE_BPS) { - revert UniversalCoreErrors.InvalidSlippageTolerance(); - } - slippageTolerance[token] = tolerance; - emit SetSlippageTolerance(token, tolerance); - } - /// @notice Set default deadline in minutes. /// @param minutesValue Default deadline in minutes function setDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 173b3cc..3daeccd 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -45,7 +45,6 @@ library UniversalCoreErrors { error CallerIsNotUEModule(); error CallerIsNotGatewayPC(); error AutoSwapNotSupported(); - error InvalidSlippageTolerance(); error MinPCOutRequired(); error GasLimitBelowBase(uint256 provided, uint256 minimum); error ZeroBaseGasLimit(); diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index defc50d..43ffb63 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -54,8 +54,8 @@ contract UniversalCoreV0 is /// @notice Default fee tier for each token (0 = not set). mapping(address => uint24) public defaultFeeTier; - /// @notice Slippage tolerance for each token in basis points (e.g., 300 = 3%). - mapping(address => uint256) public slippageTolerance; + /// @dev Deprecated. Slot retained for storage layout compatibility with deployed testnet proxy. + mapping(address => uint256) private __deprecated_slippageTolerance; /// @notice Default deadline in minutes for swaps. uint256 public defaultDeadlineMins = 20; @@ -466,17 +466,6 @@ contract UniversalCoreV0 is defaultFeeTier[token] = feeTier; } - /// @notice Set slippage tolerance for a token. - /// @param token Token address - /// @param tolerance Slippage tolerance in basis points (e.g., 300 = 3%) - function setSlippageTolerance(address token, uint256 tolerance) external onlyAdmin { - if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (tolerance > MAX_SLIPPAGE_BPS) { - revert UniversalCoreErrors.InvalidSlippageTolerance(); - } - slippageTolerance[token] = tolerance; - } - /// @notice Set default deadline in minutes. /// @param minutesValue Default deadline in minutes function setDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { diff --git a/test/fork/ForkUniversalCore.t.sol b/test/fork/ForkUniversalCore.t.sol index eee466d..924e1ea 100644 --- a/test/fork/ForkUniversalCore.t.sol +++ b/test/fork/ForkUniversalCore.t.sol @@ -104,7 +104,6 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr function _configureToken(address token, uint24 fee) private { universalCore.setAutoSwapSupported(token, true); universalCore.setDefaultFeeTier(token, fee); - universalCore.setSlippageTolerance(token, 500); // 5% slippage } function _updatePRC20UniversalCore(address token) private { diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index 078fe7f..2cc2c00 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -277,18 +277,6 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { } } - function testFuzz_setSlippageTolerance_boundary(address token, uint256 tolerance) public { - vm.assume(token != address(0)); - - if (tolerance <= 5000) { - universalCore.setSlippageTolerance(token, tolerance); - assertEq(universalCore.slippageTolerance(token), tolerance); - } else { - vm.expectRevert(UniversalCoreErrors.InvalidSlippageTolerance.selector); - universalCore.setSlippageTolerance(token, tolerance); - } - } - // ============================================= // 12.5 Deadline Validation Properties // ============================================= diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index aee6ba6..fa8350c 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -48,7 +48,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); event SetUniswapV3Addresses(address factory, address swapRouter); event SetDefaultFeeTier(address indexed token, uint24 feeTier); - event SetSlippageTolerance(address indexed token, uint256 tolerance); event SetGasPCPool(string indexed chainId, address indexed pool, uint24 fee); event SetGasToken(string indexed chainId, address indexed prc20); event DepositPRC20WithAutoSwap( @@ -1145,45 +1144,6 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { universalCore.setDefaultFeeTier(makeAddr("token"), 3000); } - // ======================================== - // 13) setSlippageTolerance Tests - // ======================================== - - function test_SetSlippageTolerance_HappyPath() public { - address token = makeAddr("token"); - vm.prank(deployer); - vm.expectEmit(true, false, false, true); - emit SetSlippageTolerance(token, 300); - universalCore.setSlippageTolerance(token, 300); - assertEq(universalCore.slippageTolerance(token), 300); - } - - function test_SetSlippageTolerance_RevertsExceeds5000() public { - address token = makeAddr("token"); - vm.prank(deployer); - vm.expectRevert(UniversalCoreErrors.InvalidSlippageTolerance.selector); - universalCore.setSlippageTolerance(token, 5001); - } - - function test_SetSlippageTolerance_RevertsZeroAddress() public { - vm.prank(deployer); - vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setSlippageTolerance(address(0), 300); - } - - function test_SetSlippageTolerance_OnlyAdmin() public { - vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setSlippageTolerance(makeAddr("token"), 300); - } - - function test_SetSlippageTolerance_BoundaryAt5000() public { - address token = makeAddr("token"); - vm.prank(deployer); - universalCore.setSlippageTolerance(token, 5000); - assertEq(universalCore.slippageTolerance(token), 5000); - } - // ======================================== // 14) Rescue Funds Gas Limit (continued) // ======================================== diff --git a/test/tests_token_and_core/UniversalCoreRefund.t.sol b/test/tests_token_and_core/UniversalCoreRefund.t.sol index 7353162..3268f12 100644 --- a/test/tests_token_and_core/UniversalCoreRefund.t.sol +++ b/test/tests_token_and_core/UniversalCoreRefund.t.sol @@ -99,7 +99,6 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { // Configure auto-swap support universalCore.setAutoSwapSupported(address(gasTokenMock), true); universalCore.setDefaultFeeTier(address(gasTokenMock), FEE_TIER); - universalCore.setSlippageTolerance(address(gasTokenMock), 300); // Setup mock pool (gasToken <-> wPC) address pool = makeAddr("mockPool"); diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index d2dd1ff..9b9a193 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -100,9 +100,8 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { universalCore.setProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); vm.stopPrank(); - // Set default fee tier and slippage for gas token + // Set default fee tier for gas token universalCore.setDefaultFeeTier(address(gasTokenMock), FEE_TIER); - universalCore.setSlippageTolerance(address(gasTokenMock), 300); // Setup mock pool (wPC <-> gasToken) address pool = makeAddr("mockPool"); From 468185685d8ead1c9fbe715946453e22197369fe Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 19:55:30 +0530 Subject: [PATCH 28/56] =?UTF-8?q?fix(audit):=20F-2026-15504=20=E2=80=94=20?= =?UTF-8?q?replace=20.transfer=20with=20call=20in=20WPC=20withdraw?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .transfer forwards only 2300 gas, blocking smart contract recipients with non-trivial receive logic. Replaced with low-level call that forwards all available gas. State is updated before the external call so reentrancy is not a concern. Added TransferFailed custom error. --- src/WPC.sol | 3 ++- src/libraries/Errors.sol | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/WPC.sol b/src/WPC.sol index 514e647..58aa353 100644 --- a/src/WPC.sol +++ b/src/WPC.sol @@ -37,7 +37,8 @@ contract WPC is IWPC { if (balanceOf[msg.sender] < wad) revert WPCErrors.InsufficientBalance(); balanceOf[msg.sender] -= wad; _totalSupply -= wad; - payable(msg.sender).transfer(wad); + (bool ok,) = msg.sender.call{value: wad}(""); + if (!ok) revert WPCErrors.TransferFailed(); emit Withdrawal(msg.sender, wad); } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 3daeccd..1d27b00 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -58,6 +58,7 @@ library UniversalCoreErrors { library WPCErrors { error InsufficientBalance(); error InsufficientAllowance(); + error TransferFailed(); } // ========================= From 58b3554a232b4c06126d668c194f132525302344 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 20:03:06 +0530 Subject: [PATCH 29/56] =?UTF-8?q?fix(audit):=20F-2026-15619=20=E2=80=94=20?= =?UTF-8?q?replace=20require=20strings=20with=20custom=20errors=20in=20Uti?= =?UTF-8?q?ls.sol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced string revert messages in stringToExactUInt256 with custom errors EmptyString and NonDigitCharacter for gas efficiency and structured error data. WPC was already fixed in batch 1. --- src/libraries/Errors.sol | 5 +++++ src/libraries/Utils.sol | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 1d27b00..ed5a6c6 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -55,6 +55,11 @@ library UniversalCoreErrors { // WPC-Specific ERRORS // ========================= +library StringUtilsErrors { + error EmptyString(); + error NonDigitCharacter(); +} + library WPCErrors { error InsufficientBalance(); error InsufficientAllowance(); diff --git a/src/libraries/Utils.sol b/src/libraries/Utils.sol index c802100..a7d4070 100644 --- a/src/libraries/Utils.sol +++ b/src/libraries/Utils.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {StringUtilsErrors} from "./Errors.sol"; + /// @title StringUtils /// @notice Utility library for string-to-number conversion. library StringUtils { @@ -15,11 +17,11 @@ library StringUtils { bytes memory b = bytes(s); uint256 len = b.length; - require(len > 0, "Empty string cannot be converted."); + if (len == 0) revert StringUtilsErrors.EmptyString(); for (uint256 i = 0; i < len; ++i) { uint8 c = uint8(b[i]); - require(c >= 48 && c <= 57, "Non-digit character found."); + if (c < 48 || c > 57) revert StringUtilsErrors.NonDigitCharacter(); result = result * 10 + (c - 48); } From 0a257acc374cd5bff58b179a697a7bc0231e9a86 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 20:05:43 +0530 Subject: [PATCH 30/56] =?UTF-8?q?fix(audit):=20F-2026-15618=20=E2=80=94=20?= =?UTF-8?q?extract=20=5FregisterUEA=20to=20avoid=20redundant=20role=20chec?= =?UTF-8?q?ks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved registration logic into internal _registerUEA. Both registerUEA (single) and registerMultipleUEA (batch) now delegate to it. Batch operations no longer repeat the onlyRole(DEFAULT_ADMIN_ROLE) check per iteration, saving gas proportional to batch size. --- src/uea/UEAFactory.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index 99fd195..a422edc 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -251,12 +251,17 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea } for (uint256 i = 0; i < _UEA.length; i++) { - registerUEA(_chainHashes[i], _vmHashes[i], _UEA[i]); + _registerUEA(_chainHashes[i], _vmHashes[i], _UEA[i]); } } /// @inheritdoc IUEAFactory function registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) public onlyRole(DEFAULT_ADMIN_ROLE) { + _registerUEA(_chainHash, _vmHash, _UEA); + } + + /// @dev Internal registration logic — no role check. + function _registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) internal { if (_UEA == address(0)) { revert UEAErrors.InvalidInputArgs(); } From fd12011d4579da76fe9d1d73cc470514afd03ea0 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 1 Apr 2026 20:08:12 +0530 Subject: [PATCH 31/56] =?UTF-8?q?fix(audit):=20F-2026-15616=20=E2=80=94=20?= =?UTF-8?q?fix=20initializeCEA=20docs=20to=20match=204-arg=20signature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation showed 3 arguments for initializeCEA but the implementation takes 4 (pushAccount, vault, universalGateway, factory). Updated 3_CEA.md to match. --- docs/3_CEA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/3_CEA.md b/docs/3_CEA.md index c3170bb..058342f 100644 --- a/docs/3_CEA.md +++ b/docs/3_CEA.md @@ -106,7 +106,7 @@ Each user has exactly one CEA per external chain (v1). A CEA is deployed via `CE **Deployment flow:** 1. `CEAFactory` clones `CEAProxy` template using `cloneDeterministic(salt)` where `salt = keccak256(abi.encode(pushAccount))`. 2. `CEAFactory` calls `CEAProxy.initializeCEAProxy(CEA_IMPLEMENTATION)` to set the implementation. -3. `CEAFactory` calls `CEA.initializeCEA(pushAccount, VAULT, UNIVERSAL_GATEWAY)` through the proxy. +3. `CEAFactory` calls `CEA.initializeCEA(pushAccount, VAULT, UNIVERSAL_GATEWAY, factory)` through the proxy. In practice: ``` From c3196937c03bac2b65a7ea2c1bb30ab72a94027f Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 20 Apr 2026 18:59:47 +0530 Subject: [PATCH 32/56] chore: apply forge fmt to UEA_EVM and UEA_SVM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formatting-only pass — no logic changes. Isolated here so upcoming audit-fix commits contain only surgical code changes. --- src/uea/UEA_EVM.sol | 115 ++++++++++++---------------------------- src/uea/UEA_SVM.sol | 126 ++++++++++++++------------------------------ 2 files changed, 73 insertions(+), 168 deletions(-) diff --git a/src/uea/UEA_EVM.sol b/src/uea/UEA_EVM.sol index f13f92f..2320ab4 100644 --- a/src/uea/UEA_EVM.sol +++ b/src/uea/UEA_EVM.sol @@ -43,8 +43,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { string public constant VERSION = "1.0.0"; /// @notice Universal Executor Module — authorized to execute without signature. - address public constant UNIVERSAL_EXECUTOR_MODULE = - 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; + address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; /// @notice EIP-712 domain separator typehash. /// keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract)") @@ -59,10 +58,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { // ========================= /// @inheritdoc IUEA - function initialize( - UniversalAccountId memory _id, - address _factory - ) external { + function initialize(UniversalAccountId memory _id, address _factory) external { if (_initialized) { revert UEAErrors.AccountAlreadyExists(); } @@ -78,43 +74,24 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @inheritdoc IUEA function domainSeparator() public view returns (bytes32) { - uint256 chainId = StringUtils.stringToExactUInt256( - _universalAccountId.chainId - ); + uint256 chainId = StringUtils.stringToExactUInt256(_universalAccountId.chainId); - return keccak256( - abi.encode( - DOMAIN_SEPARATOR_TYPEHASH, - keccak256(bytes(VERSION)), - chainId, - address(this) - ) - ); + return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes(VERSION)), chainId, address(this))); } /// @inheritdoc IUEA - function universalAccount() - public - view - returns (UniversalAccountId memory) - { + function universalAccount() public view returns (UniversalAccountId memory) { return _universalAccountId; } /// @inheritdoc IUEA - function verifyUniversalPayloadSignature( - bytes32 payloadHash, - bytes memory signature - ) public view returns (bool) { + function verifyUniversalPayloadSignature(bytes32 payloadHash, bytes memory signature) public view returns (bool) { address recoveredSigner = payloadHash.recover(signature); - return recoveredSigner - == address(bytes20(_universalAccountId.owner)); + return recoveredSigner == address(bytes20(_universalAccountId.owner)); } /// @inheritdoc IUEA - function getUniversalPayloadHash( - UniversalPayload memory payload - ) public view returns (bytes32) { + function getUniversalPayloadHash(UniversalPayload memory payload) public view returns (bytes32) { bytes32 structHash = keccak256( abi.encode( UNIVERSAL_PAYLOAD_TYPEHASH, @@ -132,9 +109,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { bytes32 domainSep = domainSeparator(); - return keccak256( - abi.encodePacked("\x19\x01", domainSep, structHash) - ); + return keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); } // ========================= @@ -142,18 +117,10 @@ contract UEA_EVM is ReentrancyGuard, IUEA { // ========================= /// @inheritdoc IUEA - function executeUniversalTx( - UniversalPayload calldata payload, - bytes calldata signature - ) external nonReentrant { + function executeUniversalTx(UniversalPayload calldata payload, bytes calldata signature) external nonReentrant { if (msg.sender != UNIVERSAL_EXECUTOR_MODULE) { - bytes32 payloadHash = - getUniversalPayloadHash(payload); - if ( - !verifyUniversalPayloadSignature( - payloadHash, signature - ) - ) { + bytes32 payloadHash = getUniversalPayloadHash(payload); + if (!verifyUniversalPayloadSignature(payloadHash, signature)) { revert UEAErrors.InvalidEVMSignature(); } } @@ -167,13 +134,8 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @dev Handles nonce increment, selector-based dispatch, and event emission. /// @param payload The UniversalPayload to execute - function _handleExecution( - UniversalPayload memory payload - ) internal { - if ( - payload.deadline > 0 - && block.timestamp > payload.deadline - ) { + function _handleExecution(UniversalPayload memory payload) internal { + if (payload.deadline > 0 && block.timestamp > payload.deadline) { revert UEAErrors.ExpiredDeadline(); } @@ -210,15 +172,14 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @param payload The UniversalPayload containing multicall data /// @return success Whether all calls succeeded /// @return returnData Return data from the last or first failed call - function _handleMulticall( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { + function _handleMulticall(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { Multicall[] memory calls = _decodeCalls(payload.data); for (uint256 i = 0; i < calls.length; i++) { - (success, returnData) = calls[i].to.call{ - value: calls[i].value - }(calls[i].data); + (success, returnData) = calls[i].to.call{value: calls[i].value}(calls[i].data); if (!success) { return (success, returnData); } @@ -232,9 +193,10 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @param payload The UniversalPayload containing migration data /// @return success Whether the migration succeeded /// @return returnData Return data from the delegatecall - function _handleMigration( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { + function _handleMigration(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { if (payload.to != address(this)) { revert UEAErrors.InvalidCall(); } @@ -243,29 +205,26 @@ contract UEA_EVM is ReentrancyGuard, IUEA { revert UEAErrors.InvalidCall(); } - address migrationContract = - ueaFactory.UEA_MIGRATION_CONTRACT(); + address migrationContract = ueaFactory.UEA_MIGRATION_CONTRACT(); if (migrationContract == address(0)) { revert UEAErrors.InvalidCall(); } - bytes memory migrateCallData = - abi.encodeWithSignature("migrateUEAEVM()"); + bytes memory migrateCallData = abi.encodeWithSignature("migrateUEAEVM()"); - (success, returnData) = - migrationContract.delegatecall(migrateCallData); + (success, returnData) = migrationContract.delegatecall(migrateCallData); } /// @dev Executes a single call to the target address. /// @param payload The UniversalPayload containing call data /// @return success Whether the call succeeded /// @return returnData Return data from the call - function _handleSingleCall( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { - (success, returnData) = - payload.to.call{value: payload.value}(payload.data); + function _handleSingleCall(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { + (success, returnData) = payload.to.call{value: payload.value}(payload.data); } // ========================= @@ -275,9 +234,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @dev Checks whether the payload data starts with MULTICALL_SELECTOR. /// @param data Raw data from the UniversalPayload /// @return True if multicall format - function _isMulticall( - bytes memory data - ) private pure returns (bool) { + function _isMulticall(bytes memory data) private pure returns (bool) { if (data.length < 4) return false; bytes4 selector; assembly { @@ -289,9 +246,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @dev Checks whether the payload data starts with MIGRATION_SELECTOR. /// @param data Raw data from the UniversalPayload /// @return True if migration format - function _isMigration( - bytes memory data - ) private pure returns (bool) { + function _isMigration(bytes memory data) private pure returns (bool) { if (data.length < 4) return false; bytes4 selector; assembly { @@ -303,9 +258,7 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @dev Strips MULTICALL_SELECTOR prefix and decodes as Multicall[]. /// @param data Raw data containing selector + ABI-encoded Multicall[] /// @return Decoded Multicall array - function _decodeCalls( - bytes memory data - ) private pure returns (Multicall[] memory) { + function _decodeCalls(bytes memory data) private pure returns (Multicall[] memory) { bytes memory strippedData = new bytes(data.length - 4); for (uint256 i = 0; i < strippedData.length; i++) { strippedData[i] = data[i + 4]; diff --git a/src/uea/UEA_SVM.sol b/src/uea/UEA_SVM.sol index 5a2b956..a002db7 100644 --- a/src/uea/UEA_SVM.sol +++ b/src/uea/UEA_SVM.sol @@ -39,12 +39,10 @@ contract UEA_SVM is ReentrancyGuard, IUEA { string public constant VERSION = "1.0.0"; /// @notice Ed25519 verifier precompile address. - address public constant VERIFIER_PRECOMPILE = - 0x00000000000000000000000000000000000000ca; + address public constant VERIFIER_PRECOMPILE = 0x00000000000000000000000000000000000000ca; /// @notice Universal Executor Module — authorized to execute without signature. - address public constant UNIVERSAL_EXECUTOR_MODULE = - 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; + address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; /// @notice EIP-712 domain separator typehash for SVM. /// keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract)") @@ -59,10 +57,7 @@ contract UEA_SVM is ReentrancyGuard, IUEA { // ========================= /// @inheritdoc IUEA - function initialize( - UniversalAccountId memory _id, - address _factory - ) external { + function initialize(UniversalAccountId memory _id, address _factory) external { if (_initialized) { revert UEAErrors.AccountAlreadyExists(); } @@ -80,35 +75,23 @@ contract UEA_SVM is ReentrancyGuard, IUEA { function domainSeparator() public view returns (bytes32) { return keccak256( abi.encode( - DOMAIN_SEPARATOR_TYPEHASH_SVM, - keccak256(bytes(VERSION)), - _universalAccountId.chainId, - address(this) + DOMAIN_SEPARATOR_TYPEHASH_SVM, keccak256(bytes(VERSION)), _universalAccountId.chainId, address(this) ) ); } /// @inheritdoc IUEA - function universalAccount() - public - view - returns (UniversalAccountId memory) - { + function universalAccount() public view returns (UniversalAccountId memory) { return _universalAccountId; } /// @inheritdoc IUEA - function verifyUniversalPayloadSignature( - bytes32 payloadHash, - bytes memory signature - ) public view returns (bool) { + function verifyUniversalPayloadSignature(bytes32 payloadHash, bytes memory signature) public view returns (bool) { return _verifySignatureSVM(payloadHash, signature); } /// @inheritdoc IUEA - function getUniversalPayloadHash( - UniversalPayload memory payload - ) public view returns (bytes32) { + function getUniversalPayloadHash(UniversalPayload memory payload) public view returns (bytes32) { bytes32 structHash = keccak256( abi.encode( UNIVERSAL_PAYLOAD_TYPEHASH, @@ -126,9 +109,7 @@ contract UEA_SVM is ReentrancyGuard, IUEA { bytes32 domainSep = domainSeparator(); - return keccak256( - abi.encodePacked("\x19\x01", domainSep, structHash) - ); + return keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); } // ========================= @@ -136,18 +117,10 @@ contract UEA_SVM is ReentrancyGuard, IUEA { // ========================= /// @inheritdoc IUEA - function executeUniversalTx( - UniversalPayload calldata payload, - bytes calldata signature - ) external nonReentrant { + function executeUniversalTx(UniversalPayload calldata payload, bytes calldata signature) external nonReentrant { if (msg.sender != UNIVERSAL_EXECUTOR_MODULE) { - bytes32 payloadHash = - getUniversalPayloadHash(payload); - if ( - !verifyUniversalPayloadSignature( - payloadHash, signature - ) - ) { + bytes32 payloadHash = getUniversalPayloadHash(payload); + if (!verifyUniversalPayloadSignature(payloadHash, signature)) { revert UEAErrors.InvalidSVMSignature(); } } @@ -163,19 +136,12 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @param payloadHash Payload hash to verify /// @param signature Ed25519 signature bytes /// @return True if the signature is valid - function _verifySignatureSVM( - bytes32 payloadHash, - bytes memory signature - ) internal view returns (bool) { - (bool success, bytes memory result) = - VERIFIER_PRECOMPILE.staticcall( - abi.encodeWithSignature( - "verifyEd25519(bytes,bytes32,bytes)", - _universalAccountId.owner, - payloadHash, - signature - ) - ); + function _verifySignatureSVM(bytes32 payloadHash, bytes memory signature) internal view returns (bool) { + (bool success, bytes memory result) = VERIFIER_PRECOMPILE.staticcall( + abi.encodeWithSignature( + "verifyEd25519(bytes,bytes32,bytes)", _universalAccountId.owner, payloadHash, signature + ) + ); if (!success) { revert UEAErrors.PrecompileCallFailed(); } @@ -185,13 +151,8 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @dev Handles nonce increment, selector-based dispatch, and event emission. /// @param payload The UniversalPayload to execute - function _handleExecution( - UniversalPayload memory payload - ) internal { - if ( - payload.deadline > 0 - && block.timestamp > payload.deadline - ) { + function _handleExecution(UniversalPayload memory payload) internal { + if (payload.deadline > 0 && block.timestamp > payload.deadline) { revert UEAErrors.ExpiredDeadline(); } @@ -228,15 +189,14 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @param payload The UniversalPayload containing multicall data /// @return success Whether all calls succeeded /// @return returnData Return data from the last or first failed call - function _handleMulticall( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { + function _handleMulticall(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { Multicall[] memory calls = _decodeCalls(payload.data); for (uint256 i = 0; i < calls.length; i++) { - (success, returnData) = calls[i].to.call{ - value: calls[i].value - }(calls[i].data); + (success, returnData) = calls[i].to.call{value: calls[i].value}(calls[i].data); if (!success) { return (success, returnData); } @@ -250,9 +210,10 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @param payload The UniversalPayload containing migration data /// @return success Whether the migration succeeded /// @return returnData Return data from the delegatecall - function _handleMigration( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { + function _handleMigration(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { if (payload.to != address(this)) { revert UEAErrors.InvalidCall(); } @@ -260,29 +221,26 @@ contract UEA_SVM is ReentrancyGuard, IUEA { revert UEAErrors.InvalidCall(); } - address migrationContract = - ueaFactory.UEA_MIGRATION_CONTRACT(); + address migrationContract = ueaFactory.UEA_MIGRATION_CONTRACT(); if (migrationContract == address(0)) { revert UEAErrors.InvalidCall(); } - bytes memory migrateCallData = - abi.encodeWithSignature("migrateUEASVM()"); + bytes memory migrateCallData = abi.encodeWithSignature("migrateUEASVM()"); - (success, returnData) = - migrationContract.delegatecall(migrateCallData); + (success, returnData) = migrationContract.delegatecall(migrateCallData); } /// @dev Executes a single call to the target address. /// @param payload The UniversalPayload containing call data /// @return success Whether the call succeeded /// @return returnData Return data from the call - function _handleSingleCall( - UniversalPayload memory payload - ) internal returns (bool success, bytes memory returnData) { - (success, returnData) = - payload.to.call{value: payload.value}(payload.data); + function _handleSingleCall(UniversalPayload memory payload) + internal + returns (bool success, bytes memory returnData) + { + (success, returnData) = payload.to.call{value: payload.value}(payload.data); } // ========================= @@ -292,9 +250,7 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @dev Checks whether the payload data starts with MULTICALL_SELECTOR. /// @param data Raw data from the UniversalPayload /// @return True if multicall format - function _isMulticall( - bytes memory data - ) private pure returns (bool) { + function _isMulticall(bytes memory data) private pure returns (bool) { if (data.length < 4) return false; bytes4 selector; assembly { @@ -306,9 +262,7 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @dev Checks whether the payload data starts with MIGRATION_SELECTOR. /// @param data Raw data from the UniversalPayload /// @return True if migration format - function _isMigration( - bytes memory data - ) private pure returns (bool) { + function _isMigration(bytes memory data) private pure returns (bool) { if (data.length < 4) return false; bytes4 selector; assembly { @@ -320,9 +274,7 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @dev Strips MULTICALL_SELECTOR prefix and decodes as Multicall[]. /// @param data Raw data containing selector + ABI-encoded Multicall[] /// @return Decoded Multicall array - function _decodeCalls( - bytes memory data - ) private pure returns (Multicall[] memory) { + function _decodeCalls(bytes memory data) private pure returns (Multicall[] memory) { bytes memory strippedData = new bytes(data.length - 4); for (uint256 i = 0; i < strippedData.length; i++) { strippedData[i] = data[i + 4]; From 62d47ccdfefa8e4ca4a02be0921881da543a642b Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 20 Apr 2026 19:06:42 +0530 Subject: [PATCH 33/56] fix: bind UEA domain separators to Push Chain deployment via EIP-712 salt Added Push Chain's block.chainid as the canonical EIP-712 `salt` field in both UEA_EVM and UEA_SVM domain separators. Prevents cross-deployment signature replay across Push Chain forks or parallel deployments. Uses the canonical 5-field EIP712Domain shape (version, chainId, verifyingContract, salt) for maximum compatibility with standard wallet signers and EIP-712 tooling. Updates tests to reflect the new typehashes and domain separator reconstruction. --- src/uea/UEA_EVM.sol | 23 ++++++++++++++++++--- src/uea/UEA_SVM.sol | 26 +++++++++++++++++++++--- test/tests_uea_and_factory/UEA_EVM.t.sol | 3 ++- test/tests_uea_and_factory/UEA_SVM.t.sol | 7 +++++-- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/uea/UEA_EVM.sol b/src/uea/UEA_EVM.sol index 2320ab4..5501bff 100644 --- a/src/uea/UEA_EVM.sol +++ b/src/uea/UEA_EVM.sol @@ -46,9 +46,22 @@ contract UEA_EVM is ReentrancyGuard, IUEA { address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; /// @notice EIP-712 domain separator typehash. - /// keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract)") + /// keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract,bytes32 salt)") + /// @dev Uses only the canonical EIP-712 `EIP712Domain` fields (`version`, `chainId`, + /// `verifyingContract`, `salt`) for maximum compatibility with standard wallets. + /// + /// Field semantics in this protocol: + /// - `version` — UEA implementation version string. + /// - `chainId` — the *source* chain's numeric ID (e.g. Ethereum + /// mainnet = 1), derived from `UniversalAccountId`. + /// Binds the signature to the origin chain identity. + /// - `verifyingContract` — this UEA proxy address. + /// - `salt` — `bytes32(block.chainid)` of Push Chain at execution + /// time. Binds the signature to the specific Push Chain + /// deployment and prevents cross-deployment replay + /// across forks or parallel deployments. bytes32 public constant DOMAIN_SEPARATOR_TYPEHASH = - 0x2aef22f9d7df5f9d21c56d14029233f3fdaa91917727e1eb68e504d27072d6cd; + 0xb90aaffa4b0fc25d6056f438f2c06198968eaf6723d182f5f928441117424b8e; /// @notice UEAFactory reference for fetching migration contract. IUEAFactory public ueaFactory; @@ -76,7 +89,11 @@ contract UEA_EVM is ReentrancyGuard, IUEA { function domainSeparator() public view returns (bytes32) { uint256 chainId = StringUtils.stringToExactUInt256(_universalAccountId.chainId); - return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes(VERSION)), chainId, address(this))); + return keccak256( + abi.encode( + DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes(VERSION)), chainId, address(this), bytes32(block.chainid) + ) + ); } /// @inheritdoc IUEA diff --git a/src/uea/UEA_SVM.sol b/src/uea/UEA_SVM.sol index a002db7..9fc4fed 100644 --- a/src/uea/UEA_SVM.sol +++ b/src/uea/UEA_SVM.sol @@ -45,9 +45,25 @@ contract UEA_SVM is ReentrancyGuard, IUEA { address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; /// @notice EIP-712 domain separator typehash for SVM. - /// keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract)") + /// keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)") + /// @dev Uses only the canonical EIP-712 `EIP712Domain` fields (`version`, `chainId`, + /// `verifyingContract`, `salt`) for maximum compatibility with standard signers + /// and EIP-712 tooling. The `_SVM` suffix in the type name disambiguates this + /// domain from the EVM variant (which encodes `chainId` as `uint256`); here + /// `chainId` is a `string` to accommodate Solana cluster identifiers. + /// + /// Field semantics in this protocol: + /// - `version` — UEA implementation version string. + /// - `chainId` — the *source* Solana cluster identifier string, + /// derived from `UniversalAccountId`. Binds the + /// signature to the origin chain identity. + /// - `verifyingContract` — this UEA proxy address. + /// - `salt` — `bytes32(block.chainid)` of Push Chain at execution + /// time. Binds the signature to the specific Push Chain + /// deployment and prevents cross-deployment replay + /// across forks or parallel deployments. bytes32 public constant DOMAIN_SEPARATOR_TYPEHASH_SVM = - 0x3aefc31558906b9b2c54de94f82a9b2455c24b4ba2b642ebb545ea2cc64a1e4b; + 0x038a4fd0ee5950f0ea6d28f116a885fc5e376a8d1a939f7a9bea48f4f13fabb1; /// @notice UEAFactory reference for fetching migration contract. IUEAFactory public ueaFactory; @@ -75,7 +91,11 @@ contract UEA_SVM is ReentrancyGuard, IUEA { function domainSeparator() public view returns (bytes32) { return keccak256( abi.encode( - DOMAIN_SEPARATOR_TYPEHASH_SVM, keccak256(bytes(VERSION)), _universalAccountId.chainId, address(this) + DOMAIN_SEPARATOR_TYPEHASH_SVM, + keccak256(bytes(VERSION)), + _universalAccountId.chainId, + address(this), + bytes32(block.chainid) ) ); } diff --git a/test/tests_uea_and_factory/UEA_EVM.t.sol b/test/tests_uea_and_factory/UEA_EVM.t.sol index 19db0e1..2876b9c 100644 --- a/test/tests_uea_and_factory/UEA_EVM.t.sol +++ b/test/tests_uea_and_factory/UEA_EVM.t.sol @@ -1011,7 +1011,8 @@ contract UEA_EVMTest is Test { // This test verifies that the DOMAIN_SEPARATOR_TYPEHASH constant matches the expected hash // If the EIP712Domain struct definition changes, this test will fail - bytes32 expectedHash = keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract)"); + bytes32 expectedHash = + keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract,bytes32 salt)"); // Access the constant from the deployed instance bytes32 actualHash = evmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH(); diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index c00851f..469864c 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -946,7 +946,8 @@ contract UEASVMTest is Test { svmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH_SVM(), keccak256(bytes(svmSmartAccountInstance.VERSION())), "101", - address(svmSmartAccountInstance) + address(svmSmartAccountInstance), + bytes32(block.chainid) ) ); @@ -957,7 +958,9 @@ contract UEASVMTest is Test { // This test verifies that the DOMAIN_SEPARATOR_TYPEHASH_SVM constant matches the expected hash // If the EIP712Domain_SVM struct definition changes, this test will fail - bytes32 expectedHash = keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract)"); + bytes32 expectedHash = keccak256( + "EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)" + ); // Access the constant from the deployed instance bytes32 actualHash = svmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH_SVM(); From 5d97ff4299e29dff294eee3e0ce1698dee155b2c Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 20 Apr 2026 19:18:24 +0530 Subject: [PATCH 34/56] fix: hash SVM domain separator chainId per EIP-712 Per EIP-712, dynamic types (string, bytes) must be keccak256-hashed when included in the domain/struct hash. The SVM domain separator declared string chainId in the typehash but passed the raw string into abi.encode, a spec violation. Now wraps with keccak256(bytes(...)) to comply. Typehash constant is unchanged; only the encoding is fixed. --- src/uea/UEA_SVM.sol | 5 ++++- test/tests_uea_and_factory/UEA_SVM.t.sol | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/uea/UEA_SVM.sol b/src/uea/UEA_SVM.sol index 9fc4fed..b60d566 100644 --- a/src/uea/UEA_SVM.sol +++ b/src/uea/UEA_SVM.sol @@ -88,12 +88,15 @@ contract UEA_SVM is ReentrancyGuard, IUEA { // ========================= /// @inheritdoc IUEA + /// @dev Per EIP-712, dynamic types (`string`, `bytes`) must be encoded as their + /// `keccak256` hash when included in the domain/struct hash. Both `version` and + /// `chainId` are declared as `string` in the typehash, so both are hashed here. function domainSeparator() public view returns (bytes32) { return keccak256( abi.encode( DOMAIN_SEPARATOR_TYPEHASH_SVM, keccak256(bytes(VERSION)), - _universalAccountId.chainId, + keccak256(bytes(_universalAccountId.chainId)), address(this), bytes32(block.chainid) ) diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index 469864c..e2a439f 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -945,7 +945,7 @@ contract UEASVMTest is Test { abi.encode( svmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH_SVM(), keccak256(bytes(svmSmartAccountInstance.VERSION())), - "101", + keccak256(bytes("101")), address(svmSmartAccountInstance), bytes32(block.chainid) ) From 4f579026e347d08fe70337c491fa8ed5b711dda6 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 20 Apr 2026 19:35:41 +0530 Subject: [PATCH 35/56] F-2026-15560: require explicit zero recipient for fund-parking mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fund-parking in _handleSingleCall now requires BOTH an empty payload AND a zero recipient. Previously, empty payload alone was sufficient, which made the park-vs-forward outcome ambiguous when a recipient was set. Requiring the vault to explicitly signal parking with address(0) removes that implicit dual meaning. The unified single-call entrypoint (vault dispatches one interface for both park and forward outcomes based on input shape) is intentional and retained — only the parking condition is tightened. --- src/cea/CEA.sol | 11 ++++++----- test/tests_cea/CEA_singleCall.t.sol | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 2ed6b9a..b53469b 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -208,13 +208,14 @@ contract CEA is ICEA, ReentrancyGuard { } /// @dev Handles single-call execution or funds parking. - /// Empty payload = park funds. Non-empty = execute call. - /// Self-calls blocked (use multicall path instead). + /// Note: Funds-parking mode is explicitly signalled by BOTH an empty payload AND a + /// zero `recipient`. + /// /// @param txId Transaction identifier for event emission /// @param universalTxId Universal tx identifier for event emission /// @param originCaller Origin caller for event emission - /// @param recipient Target contract for execution - /// @param payload Raw calldata to forward (empty = park funds) + /// @param recipient Target contract for execution (zero + empty payload = park funds) + /// @param payload Raw calldata to forward (empty + zero recipient = park funds) function _handleSingleCall( bytes32 txId, bytes32 universalTxId, @@ -222,7 +223,7 @@ contract CEA is ICEA, ReentrancyGuard { address recipient, bytes calldata payload ) internal { - if (payload.length == 0) { + if (payload.length == 0 && recipient == address(0)) { emit UniversalTxExecuted(txId, universalTxId, originCaller, address(this), payload); return; } diff --git a/test/tests_cea/CEA_singleCall.t.sol b/test/tests_cea/CEA_singleCall.t.sol index 2ca36f1..cd60053 100644 --- a/test/tests_cea/CEA_singleCall.t.sol +++ b/test/tests_cea/CEA_singleCall.t.sol @@ -52,7 +52,7 @@ contract CEA_SingleCallTests is CEATest { assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); } - function test_ParkFunds_EmptyPayload_NonZeroRecipient_Ignored() public deployCEA { + function test_EmptyPayload_NonZeroRecipient_ForwardsNative() public deployCEA { bytes32 txID = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -64,9 +64,10 @@ contract CEA_SingleCallTests is CEATest { vm.prank(vault); ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, someRecipient, ""); - // Funds should park in CEA regardless of recipient value - assertEq(address(ceaInstance).balance, amount, "CEA should hold native funds"); - assertEq(address(someRecipient).balance, 0, "Recipient should not receive anything"); + // Empty payload + non-zero recipient is a plain native send to recipient, not fund-parking. + // Fund-parking requires BOTH empty payload AND address(0) recipient. + assertEq(address(someRecipient).balance, amount, "Recipient should receive the native funds"); + assertEq(address(ceaInstance).balance, 0, "CEA should not hold funds when recipient is non-zero"); assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); } From 9c3b78af6ea6917e35dcd82357e221e605c5c79a Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 10:49:29 +0530 Subject: [PATCH 36/56] F-2026-15577: block silent overwrite in registerUEA, add explicit update path registerUEA now reverts with UEAAlreadyRegistered if an implementation is already set for the given _vmHash. A new updateUEAImplementation external function (DEFAULT_ADMIN_ROLE) provides the explicit replace path and emits UEAImplementationUpdated(vmHash, previous, new) so off-chain systems can reconstruct implementation history. Tests updated to use fresh VM hashes for first-time registrations and updateUEAImplementation for the replace path. 6 new tests cover the guard and the new function. --- src/Interfaces/IUEAFactory.sol | 6 ++ src/libraries/Errors.sol | 1 + src/uea/UEAFactory.sol | 28 ++++++ test/tests_uea_and_factory/UEAFactory.t.sol | 96 +++++++++++++++++---- 4 files changed, 115 insertions(+), 16 deletions(-) diff --git a/src/Interfaces/IUEAFactory.sol b/src/Interfaces/IUEAFactory.sol index 5402469..a7d2545 100644 --- a/src/Interfaces/IUEAFactory.sol +++ b/src/Interfaces/IUEAFactory.sol @@ -30,6 +30,12 @@ interface IUEAFactory { /// @param vmHash VM type hash event UEARegistered(bytes32 indexed chainHash, address ueaLogic, bytes32 vmHash); + /// @notice Emitted when an existing UEA implementation is replaced. + /// @param vmHash VM hash whose implementation is being updated + /// @param previousUEA Previous UEA implementation address + /// @param newUEA New UEA implementation address + event UEAImplementationUpdated(bytes32 indexed vmHash, address previousUEA, address newUEA); + /// @notice Emitted when the PAUSER_ROLE is granted to a new address. /// @param pauser Address that was granted the pauser role event PauserRoleGranted(address indexed pauser); diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index ed5a6c6..305e8f4 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -78,6 +78,7 @@ library UEAErrors { error InvalidSVMSignature(); error PrecompileCallFailed(); error AccountAlreadyExists(); + error UEAAlreadyRegistered(); } library CEAErrors { diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index a422edc..8bf5643 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -261,6 +261,10 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea } /// @dev Internal registration logic — no role check. + /// Treats registration as a one-time operation per VM hash. If an implementation + /// is already registered for this `_vmHash`, callers must use + /// `updateUEAImplementation` instead. This prevents silent replacement of the + /// VM implementation that all future UEAs of that type delegate to. function _registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) internal { if (_UEA == address(0)) { revert UEAErrors.InvalidInputArgs(); @@ -271,10 +275,34 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea revert UEAErrors.InvalidInputArgs(); } + if (UEA_VM[_vmHash] != address(0)) { + revert UEAErrors.UEAAlreadyRegistered(); + } + UEA_VM[_vmHash] = _UEA; emit UEARegistered(_chainHash, _UEA, _vmHash); } + /// @notice Replace the registered UEA implementation for a VM hash. + /// @dev Explicit update path distinct from `registerUEA`, which only + /// performs first-time registration. Emits `UEAImplementationUpdated` + /// with both previous and new addresses so off-chain systems can + /// reconstruct the implementation history. + /// @param _vmHash VM hash whose implementation is being updated + /// @param _newUEA New UEA implementation address (must be non-zero) + function updateUEAImplementation(bytes32 _vmHash, address _newUEA) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_newUEA == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + address previous = UEA_VM[_vmHash]; + if (previous == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + + UEA_VM[_vmHash] = _newUEA; + emit UEAImplementationUpdated(_vmHash, previous, _newUEA); + } + // ========================= // UF_4: PUBLIC HELPERS // ========================= diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index 3fa4806..b73e3ba 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -11,6 +11,7 @@ import {UEA_SVM} from "../../src/uea/UEA_SVM.sol"; import {UEAMigration} from "../../src/uea/UEAMigration.sol"; import {UEAErrors as Errors} from "../../src/libraries/Errors.sol"; import {IUEA} from "../../src/interfaces/IUEA.sol"; +import {IUEAFactory} from "../../src/Interfaces/IUEAFactory.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -111,12 +112,15 @@ contract UEAFactoryTest is Test { } function testRegisterUEA() public { - bytes32 chainHash = keccak256(abi.encode("KOVAN", "42")); - factory.registerNewChain(chainHash, EVM_HASH); - factory.registerUEA(chainHash, EVM_HASH, address(ueaEVMImpl)); + // Use a fresh VM hash so there is no prior implementation registered for it. + bytes32 moveChainHash = keccak256(abi.encode("APTOS", "1")); + factory.registerNewChain(moveChainHash, MOVE_VM_HASH); + + UEA_EVM moveImpl = new UEA_EVM(); + factory.registerUEA(moveChainHash, MOVE_VM_HASH, address(moveImpl)); // Check that the UEA implementation is registered - assertEq(factory.getUEA(chainHash), address(ueaEVMImpl)); + assertEq(factory.getUEA(moveChainHash), address(moveImpl)); } function testSetUEAMigrationContractOnlyOwner() public { @@ -145,15 +149,19 @@ contract UEAFactoryTest is Test { bytes32[] memory vmHashes = new bytes32[](2); address[] memory implementations = new address[](2); - // Use different chains than those in setUp - chainHashes[0] = keccak256(abi.encode("KOVAN", "42")); - chainHashes[1] = keccak256(abi.encode("METIS", "1088")); + // Use distinct VM hashes that have no prior implementation registered. + // MOVE_VM_HASH and WASM_VM_HASH are never registered in setUp. + chainHashes[0] = keccak256(abi.encode("APTOS", "1")); + chainHashes[1] = keccak256(abi.encode("NEAR", "mainnet")); + + vmHashes[0] = MOVE_VM_HASH; + vmHashes[1] = WASM_VM_HASH; - vmHashes[0] = EVM_HASH; - vmHashes[1] = EVM_HASH; + UEA_EVM moveImpl = new UEA_EVM(); + UEA_EVM wasmImpl = new UEA_EVM(); - implementations[0] = address(ueaEVMImpl); - implementations[1] = address(ueaEVMImpl); + implementations[0] = address(moveImpl); + implementations[1] = address(wasmImpl); // Register chains first factory.registerNewChain(chainHashes[0], vmHashes[0]); @@ -314,9 +322,9 @@ contract UEAFactoryTest is Test { // Use eip155 chain which is already registered in setUp address initialImpl = factory.getUEA(ethereumChainHash); - // Deploy a new implementation + // Deploy a new implementation and update via the dedicated update path. UEA_EVM newImpl = new UEA_EVM(); - factory.registerUEA(ethereumChainHash, EVM_HASH, address(newImpl)); + factory.updateUEAImplementation(EVM_HASH, address(newImpl)); // Check that the implementation was updated assertNotEq(factory.getUEA(ethereumChainHash), initialImpl); @@ -606,9 +614,8 @@ contract UEAFactoryTest is Test { // Deploy a new implementation UEA_EVM newImpl = new UEA_EVM(); - // Change implementation for EVM type - bytes32 evmChainHash = keccak256(abi.encode("eip155", "1")); - factory.registerUEA(evmChainHash, EVM_HASH, address(newImpl)); + // Change implementation for EVM type via the dedicated update path. + factory.updateUEAImplementation(EVM_HASH, address(newImpl)); // Verify implementation has changed address updatedImpl = factory.getUEA(chainHash); @@ -1088,6 +1095,63 @@ contract UEAFactoryTest is Test { UEAFactory(address(newProxy)).initialize(deployer, address(0)); } + // ========================================================================= + // updateUEAImplementation Tests + // ========================================================================= + + function testUpdateUEAImplementation_HappyPath() public { + address previousImpl = factory.UEA_VM(EVM_HASH); + assertTrue(previousImpl != address(0)); + + UEA_EVM newImpl = new UEA_EVM(); + vm.expectEmit(true, false, false, true, address(factory)); + emit IUEAFactory.UEAImplementationUpdated(EVM_HASH, previousImpl, address(newImpl)); + + factory.updateUEAImplementation(EVM_HASH, address(newImpl)); + + assertEq(factory.UEA_VM(EVM_HASH), address(newImpl)); + assertNotEq(factory.UEA_VM(EVM_HASH), previousImpl); + } + + function testUpdateUEAImplementation_UpdatesSVMImpl() public { + address previousImpl = factory.UEA_VM(SVM_HASH); + assertTrue(previousImpl != address(0)); + + UEA_SVM newImpl = new UEA_SVM(); + factory.updateUEAImplementation(SVM_HASH, address(newImpl)); + + assertEq(factory.UEA_VM(SVM_HASH), address(newImpl)); + } + + function testUpdateUEAImplementation_OnlyAdmin() public { + UEA_EVM newImpl = new UEA_EVM(); + bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + ); + vm.prank(nonOwner); + factory.updateUEAImplementation(EVM_HASH, address(newImpl)); + } + + function testUpdateUEAImplementation_ZeroAddressReverts() public { + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.updateUEAImplementation(EVM_HASH, address(0)); + } + + function testUpdateUEAImplementation_UnregisteredVmHashReverts() public { + // CAIRO_VM_HASH has never had an implementation registered — no prior entry. + UEA_EVM newImpl = new UEA_EVM(); + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.updateUEAImplementation(CAIRO_VM_HASH, address(newImpl)); + } + + function testRegisterUEA_AlreadyRegisteredReverts() public { + // EVM_HASH already has an implementation from setUp — a second registerUEA must revert. + vm.expectRevert(Errors.UEAAlreadyRegistered.selector); + factory.registerUEA(ethereumChainHash, EVM_HASH, address(ueaEVMImpl)); + } + // Test for the case where getOriginForUEA is called with an address that has an owner function testGetOriginForUEAWithOwner() public { // Create and deploy a UEA From bb2178a293077b8e22754baee0d59d0bec38c037 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 17:06:11 +0530 Subject: [PATCH 37/56] F-2026-15538: add opt-in per-chain staleness check for gas data UniversalCore previously wrote timestampObservedAtByChainNamespace on every setChainMeta but never read it. Fee-quote functions could therefore return results derived from arbitrarily old gas data if the update pipeline stalled. This adds an opt-in per-chain staleness guard: - New mapping maxStalenessByChainNamespace (MANAGER_ROLE setter, 0 disables) - New event SetMaxStalenessByChain - New error StaleGasData(observedAt, nowTimestamp, maxAge) - Both getOutboundTxGasAndFees and getRescueFundsGasLimit call a private _validateGasDataFreshness helper after the ZeroGasPrice check; fail-closed when block.timestamp > observedAt + maxAge Feature is strictly opt-in: maxStaleness defaults to 0 which skips the check. V0 testnet contract is not modified. Coverage: 13 unit tests + 3 fuzz tests. --- src/Interfaces/IUniversalCore.sol | 1 + src/UniversalCore.sol | 40 +++ src/libraries/Errors.sol | 1 + test/fuzz/UniversalCore_Fuzz.t.sol | 59 ++++ test/tests_token_and_core/UniversalCore.t.sol | 287 ++++++++++++++++++ 5 files changed, 388 insertions(+) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index e3f3e9e..84275a8 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -20,6 +20,7 @@ interface IUniversalCore { event SetProtocolFeeByToken(address indexed token, uint256 fee); event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); + event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); event RefundUnusedGas( address indexed gasToken, uint256 amount, address indexed recipient, bool swapped, uint256 pcOut ); diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 8fa8715..dd88900 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -63,6 +63,12 @@ contract UniversalCore is mapping(string => uint256) public chainHeightByChainNamespace; mapping(string => uint256) public timestampObservedAtByChainNamespace; + /// @notice Maximum acceptable age (seconds) of `timestampObservedAtByChainNamespace` + /// before gas fee quotes for that chain are rejected as stale. + /// @dev `0` disables the check for that chain (opt-in). Set per chain by + /// MANAGER_ROLE via `setMaxStalenessByChain`. + mapping(string => uint256) public maxStalenessByChainNamespace; + // -- Token configuration -- mapping(address => uint256) public protocolFeeByToken; @@ -277,6 +283,8 @@ contract UniversalCore is gasPrice = gasPriceByChainNamespace[chainNamespace]; if (gasPrice == 0) revert UniversalCoreErrors.ZeroGasPrice(); + _validateGasDataFreshness(chainNamespace); + gasFee = gasPrice * gasLimitWithBaseLimit; protocolFee = protocolFeeByToken[_prc20]; } @@ -306,6 +314,8 @@ contract UniversalCore is gasPrice = gasPriceByChainNamespace[chainNamespace]; if (gasPrice == 0) revert UniversalCoreErrors.ZeroGasPrice(); + _validateGasDataFreshness(chainNamespace); + gasFee = gasPrice * rescueGasLimit; } @@ -446,6 +456,21 @@ contract UniversalCore is emit SetRescueFundsGasLimitByChain(chainNamespace, gasLimit); } + /// @notice Set the maximum acceptable age (seconds) of gas data for a chain. + /// @dev A value of `0` disables the staleness check for that chain (opt-in). + /// When set, `getOutboundTxGasAndFees` and `getRescueFundsGasLimit` + /// revert with `StaleGasData` if the chain's observed timestamp is + /// older than `block.timestamp - maxStaleness`. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param maxStaleness Maximum acceptable age of gas data in seconds (0 disables) + function setMaxStalenessByChain(string memory chainNamespace, uint256 maxStaleness) + external + onlyRole(MANAGER_ROLE) + { + maxStalenessByChainNamespace[chainNamespace] = maxStaleness; + emit SetMaxStalenessByChain(chainNamespace, maxStaleness); + } + /// @notice Pause the contract - stops all deposit functions. Only callable by PAUSER_ROLE. function pause() external onlyRole(PAUSER_ROLE) { _pause(); @@ -481,6 +506,21 @@ contract UniversalCore is if (amount == 0) revert CommonErrors.ZeroAmount(); } + /// @dev Enforces that gas data for `chainNamespace` is within the configured freshness + /// window. No-op when `maxStalenessByChainNamespace[chainNamespace]` is `0` + /// (check disabled). Reverts with `StaleGasData` when the data is older than + /// the configured max age, carrying the observed timestamp, current timestamp, + /// and max age in the revert data. + /// @param chainNamespace Chain namespace whose freshness is being validated + function _validateGasDataFreshness(string memory chainNamespace) private view { + uint256 maxAge = maxStalenessByChainNamespace[chainNamespace]; + if (maxAge == 0) return; + uint256 observedAt = timestampObservedAtByChainNamespace[chainNamespace]; + if (block.timestamp > observedAt + maxAge) { + revert UniversalCoreErrors.StaleGasData(observedAt, block.timestamp, maxAge); + } + } + /// @dev Swap PRC20 to native PC via Uniswap V3 and send to recipient. /// @param prc20 PRC20 token address to swap /// @param amount Amount of PRC20 to swap diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 305e8f4..ce06e6c 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -49,6 +49,7 @@ library UniversalCoreErrors { error GasLimitBelowBase(uint256 provided, uint256 minimum); error ZeroBaseGasLimit(); error ZeroRescueGasLimit(); + error StaleGasData(uint256 observedAt, uint256 nowTimestamp, uint256 maxAge); } // ========================= diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index 2cc2c00..ea34791 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -409,4 +409,63 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setProtocolFeeByToken(address(0), fee); } + // ============================================= + // 12.X Gas Data Staleness Properties + // ============================================= + + function testFuzz_staleness_revertsWhenPastWindow(uint128 maxAge, uint128 timePast) public { + vm.assume(maxAge > 0); + vm.assume(timePast > maxAge); + + // Establish gas price + base limit so the quote call reaches the staleness check. + vm.prank(uExec); + universalCore.setChainMeta(CHAIN_NS, 50 gwei, 0); + vm.prank(uExec); + universalCore.setBaseGasLimitByChain(CHAIN_NS, 100_000); + + vm.prank(uExec); + universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NS); + vm.warp(uint256(observedAt) + uint256(timePast)); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(maxAge) + ) + ); + universalCore.getOutboundTxGasAndFees(address(prc20), 0); + } + + function testFuzz_staleness_okWithinWindow(uint128 maxAge, uint128 timePast) public { + vm.assume(maxAge > 0); + vm.assume(timePast <= maxAge); + + vm.prank(uExec); + universalCore.setChainMeta(CHAIN_NS, 50 gwei, 0); + vm.prank(uExec); + universalCore.setBaseGasLimitByChain(CHAIN_NS, 100_000); + + vm.prank(uExec); + universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NS); + vm.warp(uint256(observedAt) + uint256(timePast)); + + (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); + assertGt(gasFee, 0, "should succeed within the staleness window"); + } + + function testFuzz_setMaxStalenessByChain_nonManager_reverts(address caller, uint256 maxAge) public { + // Only uExec was granted MANAGER_ROLE in setUp; any other address must revert. + vm.assume(caller != uExec); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, caller, universalCore.MANAGER_ROLE() + ) + ); + vm.prank(caller); + universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + } } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index fa8350c..e93b8d9 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -63,6 +63,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SetChainMeta(string chainNamespace, uint256 price, uint256 chainHeight, uint256 observedAt); event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); + event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); function setUp() public { // Setup accounts @@ -1166,4 +1167,290 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { (, uint256 gasFee2,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); assertEq(gasFee2, GAS_PRICE * updatedLimit); } + + // ======================================== + // Gas Data Staleness Tests + // ======================================== + + // --- Setter tests --- + + function test_SetMaxStalenessByChain_HappyPath() public { + uint256 maxStaleness = 3600; // 1 hour + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + vm.expectEmit(false, false, false, true); + emit SetMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + + assertEq(universalCore.maxStalenessByChainNamespace(CHAIN_NAMESPACE), maxStaleness); + } + + function test_SetMaxStalenessByChain_OnlyManagerRole() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.MANAGER_ROLE() + ) + ); + vm.prank(nonOwner); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 3600); + } + + function test_SetMaxStalenessByChain_ZeroDisablesCheck() public { + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 0); + + assertEq(universalCore.maxStalenessByChainNamespace(CHAIN_NAMESPACE), 0); + } + + // --- Default-off behaviour --- + + function test_StalenessDisabledByDefault_NoRevertEvenAfterLongWarp() public { + // No setMaxStalenessByChain call — staleness check is off for this namespace. + // Configure rescue limit so getRescueFundsGasLimit doesn't revert on + // ZeroRescueGasLimit before reaching the (disabled) staleness check. + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + + vm.warp(block.timestamp + 365 days); + + (, uint256 outboundFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + assertGt(outboundFee, 0, "outbound fee should quote even after a year when check is disabled"); + + (, uint256 rescueFee,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); + assertGt(rescueFee, 0, "rescue fee should quote even after a year when check is disabled"); + } + + // --- getOutboundTxGasAndFees staleness --- + + function test_StalenessCheck_GetOutboundTxGasAndFees_Reverts() public { + uint256 maxStaleness = 300; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + maxStaleness + 1); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness + ) + ); + universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + } + + function test_StalenessCheck_GetOutboundTxGasAndFees_BoundaryAtEdge_OK() public { + uint256 maxStaleness = 300; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + + // Warp to exactly observedAt + maxStaleness — still within window (strict >). + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + maxStaleness); + + (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + assertGt(gasFee, 0, "should succeed exactly at the boundary"); + } + + function test_StalenessCheck_GetOutboundTxGasAndFees_OneSecondPastEdge_Reverts() public { + uint256 maxStaleness = 300; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + maxStaleness + 1); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness + ) + ); + universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + } + + // --- getRescueFundsGasLimit staleness --- + + function test_StalenessCheck_GetRescueFundsGasLimit_Reverts() public { + uint256 maxStaleness = 300; + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + vm.stopPrank(); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + maxStaleness + 1); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness + ) + ); + universalCore.getRescueFundsGasLimit(address(prc20Token)); + } + + function test_StalenessCheck_GetRescueFundsGasLimit_BoundaryAtEdge_OK() public { + uint256 maxStaleness = 300; + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + vm.stopPrank(); + + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + maxStaleness); + + (, uint256 gasFee,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); + assertGt(gasFee, 0, "rescue should succeed exactly at the boundary"); + } + + // --- Recovery / refresh --- + + function test_StalenessCheck_RefreshingObservedAtClearsStaleness() public { + uint256 maxStaleness = 300; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + + // Warp past window — call should revert + uint256 firstObservedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(firstObservedAt + maxStaleness + 100); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, firstObservedAt, block.timestamp, maxStaleness + ) + ); + universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + + // Refresh chain meta — observedAt resets to current block.timestamp. + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); + + (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + assertGt(gasFee, 0, "should succeed after refresh"); + } + + // --- Chain-halt edge case: observedAt never set --- + + function test_StalenessCheck_RevertsWhenObservedAtIsZero() public { + // Set up a fresh chain namespace with gas token + base limit, but NEVER call setChainMeta. + string memory freshNs = "never-observed"; + + PRC20 freshImpl = new PRC20(); + bytes memory initData = abi.encodeWithSelector( + PRC20.initialize.selector, + "Fresh PRC20", + "FPRC20", + 18, + freshNs, + IPRC20.TokenType.ERC20, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + address proxyAddr = deployUpgradeableContract(address(freshImpl), initData); + PRC20 freshPRC20 = PRC20(payable(proxyAddr)); + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setGasTokenPRC20(freshNs, address(mockPRC20)); + universalCore.setBaseGasLimitByChain(freshNs, 100_000); + // Deliberately skip setChainMeta — gasPrice stays 0. + // We need gasPrice > 0 to reach the staleness check. Work around by calling + // setChainMeta once to establish a price, then test the "observedAt is in the + // distant past" case which is the same fail-closed behaviour. + universalCore.setChainMeta(freshNs, GAS_PRICE, 0); + universalCore.setMaxStalenessByChain(freshNs, 60); + vm.stopPrank(); + + // Warp far past the observed window. observedAt is now in the past relative to block.timestamp. + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(freshNs); + vm.warp(observedAt + 1 days); + + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(60) + ) + ); + universalCore.getOutboundTxGasAndFees(address(freshPRC20), 0); + } + + // --- Multi-chain isolation --- + + function test_StalenessCheck_PerChainIsolation() public { + // Chain A is the default CHAIN_NAMESPACE with full config from setUp. + // Chain B is a fresh namespace we fully configure but never set maxStaleness on. + string memory chainBNs = "eip155:999"; + + PRC20 bImpl = new PRC20(); + bytes memory initData = abi.encodeWithSelector( + PRC20.initialize.selector, + "B PRC20", + "BPRC20", + 18, + chainBNs, + IPRC20.TokenType.ERC20, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + address proxyAddr = deployUpgradeableContract(address(bImpl), initData); + PRC20 bPRC20 = PRC20(payable(proxyAddr)); + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setGasTokenPRC20(chainBNs, address(mockPRC20)); + universalCore.setChainMeta(chainBNs, GAS_PRICE, 0); + universalCore.setBaseGasLimitByChain(chainBNs, BASE_GAS_LIMIT); + // Configure maxStaleness only on chain A. + universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 300); + vm.stopPrank(); + + // Warp past A's window. + uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); + vm.warp(observedAt + 300 + 1); + + // Chain A reverts (maxStaleness enforced). + vm.expectRevert( + abi.encodeWithSelector( + UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(300) + ) + ); + universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + + // Chain B succeeds (no maxStaleness set for chainBNs). + (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(bPRC20), 0); + assertGt(gasFee, 0, "chain B should not be affected by chain A's staleness config"); + } + + // --- Regression: revert ordering (staleness is last) --- + + function test_StalenessCheck_DoesNotAffectExistingRevertPaths() public { + // Fresh namespace with maxStaleness set but no gas price. The ZeroGasPrice + // revert must fire before the staleness check is reached. + string memory freshNs = "revert-order-test"; + + PRC20 freshImpl = new PRC20(); + bytes memory initData = abi.encodeWithSelector( + PRC20.initialize.selector, + "Fresh PRC20", + "FPRC20", + 18, + freshNs, + IPRC20.TokenType.ERC20, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + address proxyAddr = deployUpgradeableContract(address(freshImpl), initData); + PRC20 freshPRC20 = PRC20(payable(proxyAddr)); + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setGasTokenPRC20(freshNs, address(mockPRC20)); + universalCore.setBaseGasLimitByChain(freshNs, 100_000); + universalCore.setMaxStalenessByChain(freshNs, 60); + // No setChainMeta → gasPrice is 0 → ZeroGasPrice revert must come before staleness. + vm.stopPrank(); + + vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); + universalCore.getOutboundTxGasAndFees(address(freshPRC20), 0); + } } From 8517898990f9b2f1f5ee2ed8eb51395c161b47ae Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 17:25:25 +0530 Subject: [PATCH 38/56] F-2026-15576: make Push Chain ID configurable in getOriginForUEA fallback Replaces the hardcoded "42101" chainId in UEAFactory.getOriginForUEA's synthetic fallback branch with a configurable state variable pushChainId. Mainnet UEAFactory seeds it via initialize (new 3rd param); testnet UEAFactoryV0 seeds it via the setPushChainId admin setter. Adds short NatSpec on getOriginForUEA clarifying that the fallback returns a synthetic identity (not a registered origin) and callers must check isUEA before trusting the returned account. --- src/Interfaces/IUEAFactory.sol | 3 + src/testnetV0/UEAFactoryV0.sol | 14 +++- src/uea/UEAFactory.sol | 19 ++++- test/fuzz/UEAFactory_Fuzz.t.sol | 34 +++++++- test/fuzz/UEA_EVM_Fuzz.t.sol | 2 +- test/fuzz/UEA_SVM_Fuzz.t.sol | 2 +- test/tests_ueaMigration/BaseTest.t.sol | 2 +- test/tests_uea_and_factory/UEAFactory.t.sol | 83 +++++++++++++++++-- .../tests_uea_and_factory/UEAProxyCalls.t.sol | 2 +- test/tests_uea_and_factory/UEA_EVM.t.sol | 2 +- test/tests_uea_and_factory/UEA_SVM.t.sol | 2 +- 11 files changed, 149 insertions(+), 16 deletions(-) diff --git a/src/Interfaces/IUEAFactory.sol b/src/Interfaces/IUEAFactory.sol index a7d2545..41679ec 100644 --- a/src/Interfaces/IUEAFactory.sol +++ b/src/Interfaces/IUEAFactory.sol @@ -78,6 +78,9 @@ interface IUEAFactory { /// @return Migration contract address function UEA_MIGRATION_CONTRACT() external view returns (address); + /// @notice Push Chain ID used in the `getOriginForUEA` synthetic fallback. + function pushChainId() external view returns (string memory); + // ========================= // UF_2: DEPLOYMENT // ========================= diff --git a/src/testnetV0/UEAFactoryV0.sol b/src/testnetV0/UEAFactoryV0.sol index 2df8e05..d24d9a5 100644 --- a/src/testnetV0/UEAFactoryV0.sol +++ b/src/testnetV0/UEAFactoryV0.sol @@ -52,6 +52,9 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, /// @notice The current UEA migration contract address. address public UEA_MIGRATION_CONTRACT; + /// @notice Push Chain numeric identifier used in the `getOriginForUEA` synthetic fallback. + string public pushChainId; + // ========================= // UF: CONSTRUCTOR // ========================= @@ -130,6 +133,9 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, } /// @inheritdoc IUEAFactory + /// @dev When `isUEA` is false, `account` is a synthetic fallback built from + /// `"eip155"` + `pushChainId` + `addr` — NOT a registered origin. + /// Callers MUST check `isUEA` before trusting `account`. function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA) { account = UEA_to_UOA[addr]; @@ -137,7 +143,7 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, isUEA = true; } else { account = - UniversalAccountId({chainNamespace: "eip155", chainId: "42101", owner: bytes(abi.encodePacked(addr))}); + UniversalAccountId({chainNamespace: "eip155", chainId: pushChainId, owner: bytes(abi.encodePacked(addr))}); } return (account, isUEA); @@ -236,6 +242,12 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, UEA_MIGRATION_CONTRACT = ueaMigrationContract; } + /// @notice Update `pushChainId`. Reverts on empty string. + function setPushChainId(string memory _pushChainId) external onlyOwner { + if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); + pushChainId = _pushChainId; + } + /// @inheritdoc IUEAFactory function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyOwner { (, bool isRegistered) = getVMType(_chainHash); diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index 8bf5643..adeaf25 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -54,6 +54,9 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea /// @notice The current UEA migration contract address. address public UEA_MIGRATION_CONTRACT; + /// @notice Push Chain numeric identifier used in the `getOriginForUEA` synthetic fallback. + string public pushChainId; + // ========================= // UF: CONSTRUCTOR // ========================= @@ -70,12 +73,15 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea /// @dev Initializer for the upgradeable UEAFactory. /// @param initialAdmin Initial admin — granted DEFAULT_ADMIN_ROLE (governance) /// @param initialPauser Address granted the PAUSER_ROLE - function initialize(address initialAdmin, address initialPauser) public initializer { + /// @param _pushChainId Push Chain numeric identifier (e.g. "42101") + function initialize(address initialAdmin, address initialPauser, string memory _pushChainId) public initializer { if (initialAdmin == address(0) || initialPauser == address(0)) revert UEAErrors.InvalidInputArgs(); + if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); __AccessControl_init(); __Pausable_init(); _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); _grantRole(PAUSER_ROLE, initialPauser); + pushChainId = _pushChainId; emit PauserRoleGranted(initialPauser); } @@ -124,6 +130,9 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea } /// @inheritdoc IUEAFactory + /// @dev When `isUEA` is false, `account` is a synthetic fallback built from + /// `"eip155"` + `pushChainId` + `addr` — NOT a registered origin. + /// Callers MUST check `isUEA` before trusting `account`. function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA) { account = UEA_to_UOA[addr]; @@ -131,7 +140,7 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea isUEA = true; } else { account = - UniversalAccountId({chainNamespace: "eip155", chainId: "42101", owner: bytes(abi.encodePacked(addr))}); + UniversalAccountId({chainNamespace: "eip155", chainId: pushChainId, owner: bytes(abi.encodePacked(addr))}); } return (account, isUEA); @@ -230,6 +239,12 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea UEA_MIGRATION_CONTRACT = ueaMigrationContract; } + /// @notice Update `pushChainId`. Reverts on empty string. + function setPushChainId(string memory _pushChainId) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); + pushChainId = _pushChainId; + } + /// @inheritdoc IUEAFactory function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyRole(DEFAULT_ADMIN_ROLE) { (, bool isRegistered) = getVMType(_chainHash); diff --git a/test/fuzz/UEAFactory_Fuzz.t.sol b/test/fuzz/UEAFactory_Fuzz.t.sol index 1c8851d..d18f082 100644 --- a/test/fuzz/UEAFactory_Fuzz.t.sol +++ b/test/fuzz/UEAFactory_Fuzz.t.sol @@ -24,7 +24,7 @@ contract UEAFactory_Fuzz is Test { ueaProxyImpl = new UEAProxy(); UEAFactory factoryImpl = new UEAFactory(); bytes memory initData = - abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser")); + abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); factory.setUEAProxyImplementation(address(ueaProxyImpl)); @@ -203,4 +203,36 @@ contract UEAFactory_Fuzz is Test { vm.expectRevert(UEAErrors.InvalidInputArgs.selector); factory.deployUEA(id); } + + // ============================================= + // 5.3 Configurable pushChainId Properties + // ============================================= + + function testFuzz_setPushChainId_nonAdmin_reverts(address caller) public { + vm.assume(caller != address(this)); + vm.assume(caller != address(0)); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, caller, factory.DEFAULT_ADMIN_ROLE() + ) + ); + vm.prank(caller); + factory.setPushChainId("1"); + } + + function testFuzz_getOriginForUEA_fallbackMatchesConfiguredChainId(address addr, string memory chainId) public { + vm.assume(bytes(chainId).length > 0); + // Ensure addr is not a UEA by using an address that cannot collide with deployed UEAs + vm.assume(addr != address(0)); + + factory.setPushChainId(chainId); + + (UniversalAccountId memory account, bool isUEA) = factory.getOriginForUEA(addr); + + assertFalse(isUEA, "random address should not be a registered UEA"); + assertEq(account.chainNamespace, "eip155", "namespace is hardcoded eip155"); + assertEq(account.chainId, chainId, "chainId matches configured pushChainId"); + assertEq(account.owner, bytes(abi.encodePacked(addr))); + } } diff --git a/test/fuzz/UEA_EVM_Fuzz.t.sol b/test/fuzz/UEA_EVM_Fuzz.t.sol index ceb812a..cf2423c 100644 --- a/test/fuzz/UEA_EVM_Fuzz.t.sol +++ b/test/fuzz/UEA_EVM_Fuzz.t.sol @@ -42,7 +42,7 @@ contract UEA_EVM_FuzzTest is Test { UEAFactory factoryImpl = new UEAFactory(); bytes memory initData = - abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser")); + abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); factory.setUEAProxyImplementation(address(ueaProxyImpl)); diff --git a/test/fuzz/UEA_SVM_Fuzz.t.sol b/test/fuzz/UEA_SVM_Fuzz.t.sol index 12fc74c..a4ef47f 100644 --- a/test/fuzz/UEA_SVM_Fuzz.t.sol +++ b/test/fuzz/UEA_SVM_Fuzz.t.sol @@ -39,7 +39,7 @@ contract UEA_SVM_FuzzTest is Test { UEAFactory factoryImpl = new UEAFactory(); bytes memory initData = - abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser")); + abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); factory.setUEAProxyImplementation(address(ueaProxyImpl)); diff --git a/test/tests_ueaMigration/BaseTest.t.sol b/test/tests_ueaMigration/BaseTest.t.sol index 79b747b..c9fa457 100644 --- a/test/tests_ueaMigration/BaseTest.t.sol +++ b/test/tests_ueaMigration/BaseTest.t.sol @@ -174,7 +174,7 @@ contract BaseTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser")); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser"), "42101"); ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(factoryProxy)); diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index b73e3ba..86e17a0 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -59,8 +59,8 @@ contract UEAFactoryTest is Test { // Deploy the factory implementation UEAFactory factoryImpl = new UEAFactory(); - // Deploy and initialize the proxy with initialOwner and initialPauser - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, pauser); + // Deploy and initialize the proxy with initialOwner, initialPauser, and pushChainId + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, pauser, "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); @@ -825,7 +825,7 @@ contract UEAFactoryTest is Test { function testComputeUEA_RevertsWhenNoProxyImplementation() public { // Deploy a fresh factory without proxy implementation UEAFactory freshFactoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), pauser); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), pauser, "42101"); ERC1967Proxy freshProxy = new ERC1967Proxy(address(freshFactoryImpl), initData); UEAFactory freshFactory = UEAFactory(address(freshProxy)); @@ -845,7 +845,7 @@ contract UEAFactoryTest is Test { function testDeployUEA_RevertsWhenNoProxyImplementation() public { // Deploy a fresh factory without proxy implementation UEAFactory freshFactoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), pauser); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), pauser, "42101"); ERC1967Proxy freshProxy = new ERC1967Proxy(address(freshFactoryImpl), initData); UEAFactory freshFactory = UEAFactory(address(freshProxy)); @@ -1088,11 +1088,10 @@ contract UEAFactoryTest is Test { function testInitialize_ZeroPauserReverts() public { UEAFactory newImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, address(0)); ERC1967Proxy newProxy = new ERC1967Proxy(address(newImpl), ""); vm.expectRevert(Errors.InvalidInputArgs.selector); - UEAFactory(address(newProxy)).initialize(deployer, address(0)); + UEAFactory(address(newProxy)).initialize(deployer, address(0), "42101"); } // ========================================================================= @@ -1167,4 +1166,76 @@ contract UEAFactoryTest is Test { assertTrue(isUEA); assertTrue(account.owner.length > 0); } + + // ========================================================================= + // F-2026-15576: Configurable pushChainId in getOriginForUEA fallback + // ========================================================================= + + function test_Initialize_SeedsPushChainId() public { + // Fresh proxy deployed with pushChainId seeded via initialize + UEAFactory freshImpl = new UEAFactory(); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, pauser, "9000"); + ERC1967Proxy proxy = new ERC1967Proxy(address(freshImpl), initData); + UEAFactory freshFactory = UEAFactory(address(proxy)); + + assertEq(freshFactory.pushChainId(), "9000"); + } + + function test_Initialize_RevertsOnEmptyPushChainId() public { + UEAFactory freshImpl = new UEAFactory(); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, pauser, ""); + vm.expectRevert(); + new ERC1967Proxy(address(freshImpl), initData); + } + + function test_SetPushChainId_HappyPath() public { + factory.setPushChainId("9999"); + assertEq(factory.pushChainId(), "9999"); + } + + function test_SetPushChainId_OnlyAdmin() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, factory.DEFAULT_ADMIN_ROLE() + ) + ); + vm.prank(nonOwner); + factory.setPushChainId("9999"); + } + + function test_SetPushChainId_RevertsOnEmptyString() public { + vm.expectRevert(Errors.InvalidInputArgs.selector); + factory.setPushChainId(""); + } + + function test_GetOriginForUEA_FallbackUsesConfiguredChainId() public { + address randomAddr = makeAddr("random_fallback"); + + (UniversalAccountId memory account, bool isUEA) = factory.getOriginForUEA(randomAddr); + + assertFalse(isUEA); + assertEq(account.chainNamespace, "eip155"); + assertEq(account.chainId, factory.pushChainId()); + assertEq(account.chainId, "42101", "Default seeded value should match setUp"); + assertEq(account.owner, bytes(abi.encodePacked(randomAddr))); + } + + function test_GetOriginForUEA_FallbackUpdatesAfterSetter() public { + address randomAddr = makeAddr("random_update"); + + // Initial: chainId should be "42101" (seeded in setUp) + (UniversalAccountId memory beforeAcc, bool beforeIsUEA) = factory.getOriginForUEA(randomAddr); + assertFalse(beforeIsUEA); + assertEq(beforeAcc.chainId, "42101"); + + // Update pushChainId + factory.setPushChainId("1"); + + // After update: fallback returns new chainId + (UniversalAccountId memory afterAcc, bool afterIsUEA) = factory.getOriginForUEA(randomAddr); + assertFalse(afterIsUEA); + assertEq(afterAcc.chainNamespace, "eip155", "namespace stays hardcoded eip155"); + assertEq(afterAcc.chainId, "1"); + assertEq(afterAcc.owner, bytes(abi.encodePacked(randomAddr))); + } } diff --git a/test/tests_uea_and_factory/UEAProxyCalls.t.sol b/test/tests_uea_and_factory/UEAProxyCalls.t.sol index 466f8d5..f365855 100644 --- a/test/tests_uea_and_factory/UEAProxyCalls.t.sol +++ b/test/tests_uea_and_factory/UEAProxyCalls.t.sol @@ -52,7 +52,7 @@ contract ProxyCallTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser")); + bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); diff --git a/test/tests_uea_and_factory/UEA_EVM.t.sol b/test/tests_uea_and_factory/UEA_EVM.t.sol index 2876b9c..ad023f7 100644 --- a/test/tests_uea_and_factory/UEA_EVM.t.sol +++ b/test/tests_uea_and_factory/UEA_EVM.t.sol @@ -54,7 +54,7 @@ contract UEA_EVMTest is Test { // Deploy and initialize the proxy with initialOwner bytes memory initData = - abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser")); + abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index e2a439f..0a1cc21 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -45,7 +45,7 @@ contract UEASVMTest is Test { // Deploy and initialize the proxy with initialOwner bytes memory initData = - abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser")); + abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); From 6ae229701353b43cb2e6caf3423e4f6cdaf89d0c Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 18:17:24 +0530 Subject: [PATCH 39/56] txID to subTxId --- docs/CEA_migration_flow.md | 212 ++++---- docs/THREAT_MODELLING_DOC.md | 468 +++++++++--------- src/Interfaces/ICEA.sol | 14 +- src/cea/CEA.sol | 32 +- test/fuzz/CEAMigration_Fuzz.t.sol | 8 +- test/fuzz/CEA_Fuzz.t.sol | 100 ++-- test/tests_cea/CEA.t.sol | 329 ++++++------ test/tests_cea/CEA_multicalls.t.sol | 214 ++++---- test/tests_cea/CEA_selfCalls.t.sol | 74 +-- test/tests_cea/CEA_singleCall.t.sol | 92 ++-- .../CEAMigration_Integration.t.sol | 44 +- test/tests_ceaMigration/CEA_Migration.t.sol | 38 +- 12 files changed, 816 insertions(+), 809 deletions(-) diff --git a/docs/CEA_migration_flow.md b/docs/CEA_migration_flow.md index 9f746a1..d2f5266 100644 --- a/docs/CEA_migration_flow.md +++ b/docs/CEA_migration_flow.md @@ -67,14 +67,14 @@ function initializeCEAProxy(address _logic) external initializer { **Current Execution Flow:** ``` -executeUniversalTx(txID, universalTxID, originCaller, payload) +executeUniversalTx(subTxId, universalTxID, originCaller, payload) → _handleExecution(...) → if isMulticall(payload): _handleMulticall(...) → else: _handleSingleCall(...) // backwards compat ``` **Key Functions:** -- `executeUniversalTx()` (line 101): Entry point, validates txID and originCaller +- `executeUniversalTx()` (line 101): Entry point, validates subTxId and originCaller - `_handleExecution()` (line 168): Routes based on payload type - `_handleMulticall()` (line 193): Executes Multicall[] array via `.call()` - `sendUniversalTxToUEA()` (line 123): Self-call only function for withdrawals @@ -183,7 +183,7 @@ function migrateCEA() external onlyDelegateCall { │ ─────────────────────────────────────────────────────────────────────── │ │ Vault.executeUniversalTx() │ │ → Calls CEA.executeUniversalTx( │ -│ txID, │ +│ subTxId, │ │ universalTxID, │ │ originCaller = UEA address, │ │ payload = MULTICALL_SELECTOR + Multicall[{...}] │ @@ -195,9 +195,9 @@ function migrateCEA() external onlyDelegateCall { │ ─────────────────────────────────────────────────────────────────────── │ │ CEA.executeUniversalTx() [NEW LOGIC] │ │ → Validates: msg.sender == VAULT ✓ │ -│ → Validates: !isExecuted[txID] ✓ │ +│ → Validates: !isExecuted[subTxId] ✓ │ │ → Validates: originCaller == UEA ✓ │ -│ → Sets: isExecuted[txID] = true ✓ │ +│ → Sets: isExecuted[subTxId] = true ✓ │ │ → Calls: _handleExecution(...) │ │ │ │ CEA._handleExecution() [NEW LOGIC] │ @@ -242,7 +242,7 @@ function migrateCEA() external onlyDelegateCall { │ ─────────────────────────────────────────────────────────────────────── │ │ CEA._handleMigration() [returned from delegatecall] │ │ → Checks: success == true ✓ │ -│ → Emits: UniversalTxExecuted(txID, universalTxID, originCaller, ...) │ +│ → Emits: UniversalTxExecuted(subTxId, universalTxID, originCaller, ...) │ │ → Returns to caller │ │ │ │ Result: CEAProxy now points to CEA v2 implementation │ @@ -290,7 +290,7 @@ if (isMulticall(payload)) { } // Normal multicall execution - _handleMulticall(txID, universalTxID, originCaller, calls); + _handleMulticall(subTxId, universalTxID, originCaller, calls); } ``` @@ -443,16 +443,16 @@ function _handleMigration(Multicall memory call) internal { **Before:** ```solidity function _handleExecution( - bytes32 txID, + bytes32 subTxId, bytes32 universalTxID, address originCaller, bytes calldata payload ) internal { if (isMulticall(payload)) { Multicall[] memory calls = decodeCalls(payload); - _handleMulticall(txID, universalTxID, originCaller, calls); + _handleMulticall(subTxId, universalTxID, originCaller, calls); } else { - _handleSingleCall(txID, universalTxID, originCaller, payload); + _handleSingleCall(subTxId, universalTxID, originCaller, payload); } } ``` @@ -460,7 +460,7 @@ function _handleExecution( **After:** ```solidity function _handleExecution( - bytes32 txID, + bytes32 subTxId, bytes32 universalTxID, address originCaller, bytes calldata payload @@ -472,14 +472,14 @@ function _handleExecution( 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); + emit UniversalTxExecuted(subTxId, universalTxID, originCaller, address(this), calls[0].data); return; } // Normal multicall execution - _handleMulticall(txID, universalTxID, originCaller, calls); + _handleMulticall(subTxId, universalTxID, originCaller, calls); } else { - _handleSingleCall(txID, universalTxID, originCaller, payload); + _handleSingleCall(subTxId, universalTxID, originCaller, payload); } } ``` @@ -682,19 +682,19 @@ function initializeCEA(address _uea, address _vault, address _universalGateway, All migration executions MUST satisfy these constraints (enforced by `_handleMigration()`): -| Constraint | Validation | Error | Rationale | -|------------|-----------|-------|-----------| -| **Standalone execution** | `calls.length == 1` | `InvalidCall` | Prevents migration buried in complex batch | -| **Self-targeted** | `call.to == address(this)` | `InvalidTarget` | Migration must target own proxy | -| **Zero value** | `call.value == 0` | `InvalidInput` | No funds sent with migration | -| **Migration contract set** | `factory.CEA_MIGRATION_CONTRACT() != address(0)` | `InvalidCall` | Prevents uninitialized migration | -| **Delegatecall context** | Enforced by CEAMigration.`onlyDelegateCall()` | `Unauthorized` | Prevents direct calls to migration | -| **Valid implementation** | CEAMigration constructor validates `hasCode()` | `InvalidInput` | Prevents bricking proxy | +| Constraint | Validation | Error | Rationale | +| -------------------------- | ------------------------------------------------ | --------------- | ------------------------------------------ | +| **Standalone execution** | `calls.length == 1` | `InvalidCall` | Prevents migration buried in complex batch | +| **Self-targeted** | `call.to == address(this)` | `InvalidTarget` | Migration must target own proxy | +| **Zero value** | `call.value == 0` | `InvalidInput` | No funds sent with migration | +| **Migration contract set** | `factory.CEA_MIGRATION_CONTRACT() != address(0)` | `InvalidCall` | Prevents uninitialized migration | +| **Delegatecall context** | Enforced by CEAMigration.`onlyDelegateCall()` | `Unauthorized` | Prevents direct calls to migration | +| **Valid implementation** | CEAMigration constructor validates `hasCode()` | `InvalidInput` | Prevents bricking proxy | **Additional existing protections:** - `onlyVault` modifier (line 52): Only Vault can call `executeUniversalTx()` - `originCaller == UEA` check (line 109): Transaction must originate from correct UEA -- `!isExecuted[txID]` check (line 108): Prevents replay attacks +- `!isExecuted[subTxId]` check (line 108): Prevents replay attacks - `nonReentrant` modifier (line 106): Prevents reentrancy --- @@ -801,22 +801,22 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig ### 6.4 Replay Attack -**Threat:** Same txID executed twice → double spend or double migration +**Threat:** Same subTxId executed twice → double spend or double migration **Impact:** High - unauthorized execution or wasted gas **Mitigation:** - **Existing protection (line 108):** ```solidity - if (isExecuted[txID]) revert CEAErrors.PayloadExecuted(); - isExecuted[txID] = true; + if (isExecuted[subTxId]) revert CEAErrors.PayloadExecuted(); + isExecuted[subTxId] = true; ``` - Executed BEFORE routing to migration - Preserved across migration (storage not touched) **Test coverage:** -- Execute migration with txID = keccak256("migration1") -- Attempt to execute same txID again → expect `PayloadExecuted` revert +- Execute migration with subTxId = keccak256("migration1") +- Attempt to execute same subTxId again → expect `PayloadExecuted` revert - Verify isExecuted mapping preserved after migration --- @@ -1007,17 +1007,17 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_ceaMigration/CEAMigration.t.sol` -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `test_Constructor_ValidImplementation` | Deploy with valid CEA v2 | Success, immutables set correctly | -| `test_Constructor_RevertZeroAddress` | Deploy with address(0) | Revert with `InvalidInput` | -| `test_Constructor_RevertEOA` | Deploy with EOA address | Revert with `InvalidInput` | -| `test_migrateCEA_DirectCall` | Call `migrateCEA()` directly | Revert with `Unauthorized` | -| `test_migrateCEA_Delegatecall` | Delegatecall from mock proxy | Success, slot written, event emitted | -| `test_migrateCEA_SlotWrite` | Verify CEA_LOGIC_SLOT updated | Slot contains new implementation address | -| `test_migrateCEA_EventEmission` | Check event emission | `ImplementationUpdated` emitted with correct address | -| `test_hasCode_Contract` | Check contract address | Returns true | -| `test_hasCode_EOA` | Check EOA address | Returns false | +| Test | Description | Expected Result | +| -------------------------------------- | ----------------------------- | ---------------------------------------------------- | +| `test_Constructor_ValidImplementation` | Deploy with valid CEA v2 | Success, immutables set correctly | +| `test_Constructor_RevertZeroAddress` | Deploy with address(0) | Revert with `InvalidInput` | +| `test_Constructor_RevertEOA` | Deploy with EOA address | Revert with `InvalidInput` | +| `test_migrateCEA_DirectCall` | Call `migrateCEA()` directly | Revert with `Unauthorized` | +| `test_migrateCEA_Delegatecall` | Delegatecall from mock proxy | Success, slot written, event emitted | +| `test_migrateCEA_SlotWrite` | Verify CEA_LOGIC_SLOT updated | Slot contains new implementation address | +| `test_migrateCEA_EventEmission` | Check event emission | `ImplementationUpdated` emitted with correct address | +| `test_hasCode_Contract` | Check contract address | Returns true | +| `test_hasCode_EOA` | Check EOA address | Returns false | --- @@ -1025,14 +1025,14 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_cea/CEAFactory.t.sol` (add to existing test file) -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `test_setCEAMigrationContract_Success` | Owner sets migration contract | Success, event emitted | -| `test_setCEAMigrationContract_ZeroAddress` | Set to address(0) | Revert with `ZeroAddress` | -| `test_setCEAMigrationContract_NonOwner` | Non-owner attempts to set | Revert with `OwnableUnauthorizedAccount` | -| `test_setCEAMigrationContract_Event` | Verify event emission | `CEAMigrationContractUpdated` with old/new addresses | -| `test_initialize_WithMigrationContract` | Initialize factory with migration contract | Success (if optional param added) | -| `test_deployCEA_PassesFactoryAddress` | Verify factory address passed to CEA | CEA.factory == factory address | +| Test | Description | Expected Result | +| ------------------------------------------ | ------------------------------------------ | ---------------------------------------------------- | +| `test_setCEAMigrationContract_Success` | Owner sets migration contract | Success, event emitted | +| `test_setCEAMigrationContract_ZeroAddress` | Set to address(0) | Revert with `ZeroAddress` | +| `test_setCEAMigrationContract_NonOwner` | Non-owner attempts to set | Revert with `OwnableUnauthorizedAccount` | +| `test_setCEAMigrationContract_Event` | Verify event emission | `CEAMigrationContractUpdated` with old/new addresses | +| `test_initialize_WithMigrationContract` | Initialize factory with migration contract | Success (if optional param added) | +| `test_deployCEA_PassesFactoryAddress` | Verify factory address passed to CEA | CEA.factory == factory address | --- @@ -1040,19 +1040,19 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_cea/CEA_Migration.t.sol` (new test file) -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `test_initializeCEA_WithFactory` | Initialize with factory address | Success, factory set | -| `test_initializeCEA_ZeroFactory` | Initialize with address(0) factory | Revert with `ZeroAddress` | -| `test_isMigration_True` | Check MIGRATION_SELECTOR | Returns true | -| `test_isMigration_False` | Check other selector | Returns false | -| `test_isMigration_ShortData` | Check data < 4 bytes | Returns false | -| `test_handleMigration_WrongTarget` | Migration with `to != address(this)` | Revert with `InvalidTarget` | -| `test_handleMigration_NonZeroValue` | Migration with `value > 0` | Revert with `InvalidInput` | -| `test_handleMigration_NoMigrationContract` | Factory returns address(0) | Revert with `InvalidCall` | -| `test_handleMigration_DelegatecallFailure` | Migration contract reverts | Revert bubbles up | -| `test_handleMulticall_MigrationInBatch` | Batch with migration selector | Revert with `InvalidCall` | -| `test_handleExecution_StandaloneMigration` | Single-element migration multicall | Routes to `_handleMigration()` | +| Test | Description | Expected Result | +| ------------------------------------------ | ------------------------------------ | ------------------------------ | +| `test_initializeCEA_WithFactory` | Initialize with factory address | Success, factory set | +| `test_initializeCEA_ZeroFactory` | Initialize with address(0) factory | Revert with `ZeroAddress` | +| `test_isMigration_True` | Check MIGRATION_SELECTOR | Returns true | +| `test_isMigration_False` | Check other selector | Returns false | +| `test_isMigration_ShortData` | Check data < 4 bytes | Returns false | +| `test_handleMigration_WrongTarget` | Migration with `to != address(this)` | Revert with `InvalidTarget` | +| `test_handleMigration_NonZeroValue` | Migration with `value > 0` | Revert with `InvalidInput` | +| `test_handleMigration_NoMigrationContract` | Factory returns address(0) | Revert with `InvalidCall` | +| `test_handleMigration_DelegatecallFailure` | Migration contract reverts | Revert bubbles up | +| `test_handleMulticall_MigrationInBatch` | Batch with migration selector | Revert with `InvalidCall` | +| `test_handleExecution_StandaloneMigration` | Single-element migration multicall | Routes to `_handleMigration()` | --- @@ -1060,19 +1060,19 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_ceaMigration/CEAMigration_Integration.t.sol` -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `test_FullMigrationFlow` | Complete Vault → CEA → Migration flow | Success, implementation upgraded | -| `test_StatePersistence_UEA` | Verify UEA unchanged after migration | `cea.UEA()` == original value | -| `test_StatePersistence_VAULT` | Verify VAULT unchanged | `cea.VAULT()` == original value | -| `test_StatePersistence_UNIVERSAL_GATEWAY` | Verify gateway unchanged | `cea.UNIVERSAL_GATEWAY()` == original value | -| `test_StatePersistence_isExecuted` | Verify executed tx records preserved | `cea.isExecuted(oldTxID)` == true | -| `test_FundPersistence_Native` | Native balance preserved | Balance unchanged before/after | -| `test_FundPersistence_ERC20` | ERC20 balance preserved | Balance unchanged before/after | -| `test_PostMigration_Withdraw` | Withdraw funds after migration | `sendUniversalTxToUEA()` succeeds | -| `test_PostMigration_Execute` | Execute new tx after migration | `executeUniversalTx()` succeeds with new logic | -| `test_PostMigration_Multicall` | Multicall after migration | Works normally | -| `test_MultipleProxies_IndependentMigration` | Migrate multiple CEAs independently | Each migrates without affecting others | +| Test | Description | Expected Result | +| ------------------------------------------- | ------------------------------------- | ---------------------------------------------- | +| `test_FullMigrationFlow` | Complete Vault → CEA → Migration flow | Success, implementation upgraded | +| `test_StatePersistence_UEA` | Verify UEA unchanged after migration | `cea.UEA()` == original value | +| `test_StatePersistence_VAULT` | Verify VAULT unchanged | `cea.VAULT()` == original value | +| `test_StatePersistence_UNIVERSAL_GATEWAY` | Verify gateway unchanged | `cea.UNIVERSAL_GATEWAY()` == original value | +| `test_StatePersistence_isExecuted` | Verify executed tx records preserved | `cea.isExecuted(oldTxID)` == true | +| `test_FundPersistence_Native` | Native balance preserved | Balance unchanged before/after | +| `test_FundPersistence_ERC20` | ERC20 balance preserved | Balance unchanged before/after | +| `test_PostMigration_Withdraw` | Withdraw funds after migration | `sendUniversalTxToUEA()` succeeds | +| `test_PostMigration_Execute` | Execute new tx after migration | `executeUniversalTx()` succeeds with new logic | +| `test_PostMigration_Multicall` | Multicall after migration | Works normally | +| `test_MultipleProxies_IndependentMigration` | Migrate multiple CEAs independently | Each migrates without affecting others | --- @@ -1080,16 +1080,16 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_ceaMigration/CEAMigration_Negative.t.sol` -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `testRevert_NotVault` | Non-Vault calls executeUniversalTx with migration | Revert with `NotVault` | -| `testRevert_WrongOriginCaller` | Wrong originCaller in migration payload | Revert with `InvalidUEA` | -| `testRevert_ReplayedTxID` | Attempt to execute same migration txID twice | Revert with `PayloadExecuted` | -| `testRevert_WrongTarget` | Migration with `to != address(this)` | Revert with `InvalidTarget` | -| `testRevert_NonZeroValue` | Migration with `value > 0` | Revert with `InvalidInput` | -| `testRevert_BatchedMigration` | Migration in multi-call batch | Revert with `InvalidCall` | -| `testRevert_UnsetMigrationContract` | Migration before factory.CEA_MIGRATION_CONTRACT() set | Revert with `InvalidCall` | -| `testRevert_InvalidImplementation` | Migration contract points to invalid address | Revert (caught at migration deploy) | +| Test | Description | Expected Result | +| ----------------------------------- | ----------------------------------------------------- | ----------------------------------- | +| `testRevert_NotVault` | Non-Vault calls executeUniversalTx with migration | Revert with `NotVault` | +| `testRevert_WrongOriginCaller` | Wrong originCaller in migration payload | Revert with `InvalidUEA` | +| `testRevert_ReplayedTxID` | Attempt to execute same migration subTxId twice | Revert with `PayloadExecuted` | +| `testRevert_WrongTarget` | Migration with `to != address(this)` | Revert with `InvalidTarget` | +| `testRevert_NonZeroValue` | Migration with `value > 0` | Revert with `InvalidInput` | +| `testRevert_BatchedMigration` | Migration in multi-call batch | Revert with `InvalidCall` | +| `testRevert_UnsetMigrationContract` | Migration before factory.CEA_MIGRATION_CONTRACT() set | Revert with `InvalidCall` | +| `testRevert_InvalidImplementation` | Migration contract points to invalid address | Revert (caught at migration deploy) | --- @@ -1097,14 +1097,14 @@ All migration executions MUST satisfy these constraints (enforced by `_handleMig **File:** `test/tests_ceaMigration/CEAMigration_EdgeCases.t.sol` -| Test | Description | Expected Result | -|------|-------------|-----------------| -| `test_MigrationV1toV2toV3` | Chain migrations: v1 → v2 → v3 | All succeed, state preserved through both | -| `test_MigrationAfterManyExecutions` | Migrate CEA with 1000+ executed txs | Success, all isExecuted entries preserved | -| `test_MigrationWithMaxBalances` | Migrate CEA holding max uint256 token amounts | Balances preserved | -| `test_MigrationEmptyState` | Migrate brand new CEA (no executions yet) | Success, ready for use | -| `test_MigrationImmediateReuse` | Execute normal tx immediately after migration | Works with new implementation | -| `test_MigrationDuringHighLoad` | Migrate while other CEAs executing | No interference, isolated state | +| Test | Description | Expected Result | +| ----------------------------------- | --------------------------------------------- | ----------------------------------------- | +| `test_MigrationV1toV2toV3` | Chain migrations: v1 → v2 → v3 | All succeed, state preserved through both | +| `test_MigrationAfterManyExecutions` | Migrate CEA with 1000+ executed txs | Success, all isExecuted entries preserved | +| `test_MigrationWithMaxBalances` | Migrate CEA holding max uint256 token amounts | Balances preserved | +| `test_MigrationEmptyState` | Migrate brand new CEA (no executions yet) | Success, ready for use | +| `test_MigrationImmediateReuse` | Execute normal tx immediately after migration | Works with new implementation | +| `test_MigrationDuringHighLoad` | Migrate while other CEAs executing | No interference, isolated state | --- @@ -1119,7 +1119,7 @@ function testFuzz_MigrationPreservesState(uint256 executionCount) public { // Execute random txs for (uint256 i = 0; i < executionCount; i++) { - bytes32 txID = keccak256(abi.encode(i)); + bytes32 subTxId = keccak256(abi.encode(i)); // ... execute normal tx } @@ -1517,7 +1517,7 @@ event CEAMigrationContractUpdated(address indexed oldContract, address indexed n **CEA (existing, line 219):** ```solidity event UniversalTxExecuted( - bytes32 indexed txID, + bytes32 indexed subTxId, bytes32 indexed universalTxID, address indexed originCaller, address to, @@ -1527,7 +1527,7 @@ event UniversalTxExecuted( **Emitted during migration:** 1. `CEAMigration.ImplementationUpdated(ceaV2Address)` - inside delegatecall -2. `CEA.UniversalTxExecuted(txID, universalTxID, UEA, ceaProxy, migrationSelector)` - in _handleExecution +2. `CEA.UniversalTxExecuted(subTxId, universalTxID, UEA, ceaProxy, migrationSelector)` - in _handleExecution --- @@ -1549,18 +1549,18 @@ event UniversalTxExecuted( ### 10.6 Comparison: UEA vs CEA Migration -| Aspect | UEA Migration | CEA Migration | -|--------|--------------|---------------| -| **Initiator** | User (signs UniversalPayload) | User (via UEA on Push Chain) | -| **Entry point** | `UEA_EVM.executePayload()` | `CEA.executeUniversalTx()` | -| **Caller** | Direct call (or UE_MODULE) | Vault only | -| **Authorization** | Signature verification | originCaller == UEA | -| **Payload format** | `UniversalPayload` struct | `Multicall[]` array | -| **Selector detection** | `isMigration(payload.data)` | `isMigration(call.data)` inside multicall | -| **Migration contract fetch** | `factory.UEA_MIGRATION_CONTRACT()` | `factory.CEA_MIGRATION_CONTRACT()` | -| **Delegatecall target** | `migrateUEAEVM()` | `migrateCEA()` | -| **Storage slot** | `UEA_LOGIC_SLOT` (0x868a771a...) | `CEA_LOGIC_SLOT` (0x8b2ae8ee...) | -| **Cross-chain** | No (UEA lives on Push Chain) | Yes (CEA on external chain, initiated from Push) | +| Aspect | UEA Migration | CEA Migration | +| ---------------------------- | ---------------------------------- | ------------------------------------------------ | +| **Initiator** | User (signs UniversalPayload) | User (via UEA on Push Chain) | +| **Entry point** | `UEA_EVM.executePayload()` | `CEA.executeUniversalTx()` | +| **Caller** | Direct call (or UE_MODULE) | Vault only | +| **Authorization** | Signature verification | originCaller == UEA | +| **Payload format** | `UniversalPayload` struct | `Multicall[]` array | +| **Selector detection** | `isMigration(payload.data)` | `isMigration(call.data)` inside multicall | +| **Migration contract fetch** | `factory.UEA_MIGRATION_CONTRACT()` | `factory.CEA_MIGRATION_CONTRACT()` | +| **Delegatecall target** | `migrateUEAEVM()` | `migrateCEA()` | +| **Storage slot** | `UEA_LOGIC_SLOT` (0x868a771a...) | `CEA_LOGIC_SLOT` (0x8b2ae8ee...) | +| **Cross-chain** | No (UEA lives on Push Chain) | Yes (CEA on external chain, initiated from Push) | --- diff --git a/docs/THREAT_MODELLING_DOC.md b/docs/THREAT_MODELLING_DOC.md index b1583d2..b836ffb 100644 --- a/docs/THREAT_MODELLING_DOC.md +++ b/docs/THREAT_MODELLING_DOC.md @@ -80,20 +80,20 @@ protocol compromise with no on-chain recovery path. ## Scope -| Contract | File | Chain | Upgradeable | -|---|---|---|---| -| UniversalCore | `src/UniversalCore.sol` | Push Chain | Yes (OZ ERC1967) | -| PRC20 | `src/PRC20.sol` | Push Chain | Yes (OZ Initializable) | -| WPC | `src/WPC.sol` | Push Chain | No | -| UEA_EVM | `src/uea/UEA_EVM.sol` | Push Chain | No (logic; proxy is upgradeable via migration) | -| UEA_SVM | `src/uea/UEA_SVM.sol` | Push Chain | No (logic; proxy is upgradeable via migration) | -| UEAFactory | `src/uea/UEAFactory.sol` | Push Chain | Yes (OZ ERC1967) | -| UEAProxy | `src/uea/UEAProxy.sol` | Push Chain | No (upgraded via UEAMigration delegatecall) | -| UEAMigration | `src/uea/UEAMigration.sol` | Push Chain | No | -| CEA | `src/cea/CEA.sol` | External Chain | No (logic; proxy is upgradeable via migration) | -| CEAFactory | `src/cea/CEAFactory.sol` | External Chain | Yes (OZ ERC1967) | -| CEAProxy | `src/cea/CEAProxy.sol` | External Chain | No (upgraded via CEAMigration delegatecall) | -| CEAMigration | `src/cea/CEAMigration.sol` | External Chain | No | +| Contract | File | Chain | Upgradeable | +| ------------- | -------------------------- | -------------- | ---------------------------------------------- | +| UniversalCore | `src/UniversalCore.sol` | Push Chain | Yes (OZ ERC1967) | +| PRC20 | `src/PRC20.sol` | Push Chain | Yes (OZ Initializable) | +| WPC | `src/WPC.sol` | Push Chain | No | +| UEA_EVM | `src/uea/UEA_EVM.sol` | Push Chain | No (logic; proxy is upgradeable via migration) | +| UEA_SVM | `src/uea/UEA_SVM.sol` | Push Chain | No (logic; proxy is upgradeable via migration) | +| UEAFactory | `src/uea/UEAFactory.sol` | Push Chain | Yes (OZ ERC1967) | +| UEAProxy | `src/uea/UEAProxy.sol` | Push Chain | No (upgraded via UEAMigration delegatecall) | +| UEAMigration | `src/uea/UEAMigration.sol` | Push Chain | No | +| CEA | `src/cea/CEA.sol` | External Chain | No (logic; proxy is upgradeable via migration) | +| CEAFactory | `src/cea/CEAFactory.sol` | External Chain | Yes (OZ ERC1967) | +| CEAProxy | `src/cea/CEAProxy.sol` | External Chain | No (upgraded via CEAMigration delegatecall) | +| CEAMigration | `src/cea/CEAMigration.sol` | External Chain | No | > **Excluded:** `src/mocks/` (test helpers) and `src/testnetV0/` (deprecated v0 contracts) > are out of scope for this threat model. @@ -116,14 +116,14 @@ not analysed here: ## Privilege Hierarchy -| Principal | Type | Contracts Affected | Powers | -|---|---|---|---| -| `UNIVERSAL_EXECUTOR_MODULE` (`0x14191...`) | Hardcoded address | `UniversalCore`, `PRC20`, `UEA_EVM`, `UEA_SVM` | Mint PRC20 tokens; deposit/refund/setChainMeta in UniversalCore; bypass all UEA signature checks and execute arbitrary multicall payloads through any UEA | -| `DEFAULT_ADMIN_ROLE` | OZ role (address assigned at init) | `UniversalCore`, `UEAFactory`, `CEAFactory` | Set all protocol config addresses (Uniswap, WPC, gateway, migration contracts); grant/revoke all other roles; upgrade proxy implementations | -| `MANAGER_ROLE` | OZ role | `UniversalCore` | Set per-chain and per-token operational parameters (gas limits, fee tiers, supported tokens, pool addresses) | -| `PAUSER_ROLE` | OZ role | `UniversalCore`, `UEAFactory`, `CEAFactory` | Call `pause()` and `unpause()` only | -| `universalGatewayPC` | Mutable address (admin-settable) | `UniversalCore` | Sole caller of `swapAndBurnGas`; can send arbitrary native value | -| `VAULT` | Mutable address (admin-settable); immutable per-CEA | `CEAFactory`, `CEA` | Sole deployer of CEAs; sole caller of `executeUniversalTx` on every CEA | +| Principal | Type | Contracts Affected | Powers | +| ------------------------------------------ | --------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UNIVERSAL_EXECUTOR_MODULE` (`0x14191...`) | Hardcoded address | `UniversalCore`, `PRC20`, `UEA_EVM`, `UEA_SVM` | Mint PRC20 tokens; deposit/refund/setChainMeta in UniversalCore; bypass all UEA signature checks and execute arbitrary multicall payloads through any UEA | +| `DEFAULT_ADMIN_ROLE` | OZ role (address assigned at init) | `UniversalCore`, `UEAFactory`, `CEAFactory` | Set all protocol config addresses (Uniswap, WPC, gateway, migration contracts); grant/revoke all other roles; upgrade proxy implementations | +| `MANAGER_ROLE` | OZ role | `UniversalCore` | Set per-chain and per-token operational parameters (gas limits, fee tiers, supported tokens, pool addresses) | +| `PAUSER_ROLE` | OZ role | `UniversalCore`, `UEAFactory`, `CEAFactory` | Call `pause()` and `unpause()` only | +| `universalGatewayPC` | Mutable address (admin-settable) | `UniversalCore` | Sole caller of `swapAndBurnGas`; can send arbitrary native value | +| `VAULT` | Mutable address (admin-settable); immutable per-CEA | `CEAFactory`, `CEA` | Sole deployer of CEAs; sole caller of `executeUniversalTx` on every CEA | --- @@ -140,55 +140,55 @@ Uniswap V3 pool infrastructure. ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `depositPRC20Token` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused` | -| `depositPRC20WithAutoSwap` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused`, `nonReentrant` | -| `refundUnusedGas` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused`, `nonReentrant` | -| `setChainMeta` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule` (**no** `whenNotPaused`) | -| `swapAndBurnGas` | `universalGatewayPC` | `onlyGatewayPC`, `whenNotPaused`, `nonReentrant`, `payable` | -| `setProtocolFeeByToken` | `MANAGER_ROLE` | `onlyManager` | -| `setSupportedToken` | `MANAGER_ROLE` | `onlyManager` | -| `setGasPCPool` | `MANAGER_ROLE` | `onlyManager` | -| `setGasTokenPRC20` | `MANAGER_ROLE` | `onlyManager` | -| `setBaseGasLimitByChain` | `MANAGER_ROLE` | `onlyManager` | -| `setRescueFundsGasLimitByChain` | `MANAGER_ROLE` | `onlyManager` | -| `setAutoSwapSupported` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setWPC` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUniversalGatewayPC` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUniswapV3Addresses` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setDefaultFeeTier` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setSlippageTolerance` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setDefaultDeadlineMins` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `pause` | `PAUSER_ROLE` | OZ `Pausable` | -| `unpause` | `PAUSER_ROLE` | OZ `Pausable` | -| `receive()` | Anyone | `payable` | +| Function | Caller | Guard | +| ------------------------------- | --------------------------- | ----------------------------------------------------------- | +| `depositPRC20Token` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused` | +| `depositPRC20WithAutoSwap` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused`, `nonReentrant` | +| `refundUnusedGas` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule`, `whenNotPaused`, `nonReentrant` | +| `setChainMeta` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUEModule` (**no** `whenNotPaused`) | +| `swapAndBurnGas` | `universalGatewayPC` | `onlyGatewayPC`, `whenNotPaused`, `nonReentrant`, `payable` | +| `setProtocolFeeByToken` | `MANAGER_ROLE` | `onlyManager` | +| `setSupportedToken` | `MANAGER_ROLE` | `onlyManager` | +| `setGasPCPool` | `MANAGER_ROLE` | `onlyManager` | +| `setGasTokenPRC20` | `MANAGER_ROLE` | `onlyManager` | +| `setBaseGasLimitByChain` | `MANAGER_ROLE` | `onlyManager` | +| `setRescueFundsGasLimitByChain` | `MANAGER_ROLE` | `onlyManager` | +| `setAutoSwapSupported` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setWPC` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setUniversalGatewayPC` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setUniswapV3Addresses` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setDefaultFeeTier` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setSlippageTolerance` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setDefaultDeadlineMins` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `pause` | `PAUSER_ROLE` | OZ `Pausable` | +| `unpause` | `PAUSER_ROLE` | OZ `Pausable` | +| `receive()` | Anyone | `payable` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| UC-T1 | Tampering | `setChainMeta` has no `whenNotPaused` guard — oracle/chain metadata is mutable even while the contract is paused, bypassing the intent of a pause | -| UC-T2 | Elevation of Privilege | `universalGatewayPC` is admin-mutable with no timelock; an attacker who compromises admin can point it to an attacker-controlled address that calls `swapAndBurnGas` with arbitrary native value | -| UC-T3 | Tampering | Uniswap V3 addresses (factory, router, quoter) are admin-mutable; replacement with malicious contracts enables fund diversion in `_autoSwap` and `swapAndBurnGas` | -| UC-T4 | Tampering | `WPC` address is admin-mutable; a malicious WETH-style contract at the new address can redirect or steal native PC during wrap/unwrap operations | -| UC-T5 | Denial of Service | `swapAndBurnGas` sends the native PC refund via `caller.call{value: refund}("")`; if `caller` reverts on ETH receive, the entire swap transaction reverts | -| UC-T6 | Denial of Service | `defaultDeadlineMins` is settable to `0` by admin; `deadline = block.timestamp + 0` makes all new swap transactions immediately expire at the EVM level | -| UC-T7 | Spoofing | `_validateParams` blocks `recipient == UNIVERSAL_EXECUTOR_MODULE` and `recipient == address(this)` but does not block other sensitive addresses (e.g., `universalGatewayPC`) | -| UC-T8 | Information Disclosure | `slippageTolerance` is stored on-chain but `minPCOut` is caller-supplied by the UE Module at call time; auditor should verify that the on-chain tolerance is enforced against the call-time value and not silently ignored | -| UC-T9 | Tampering | `defaultDeadlineMins == 0` path: `deadline = block.timestamp + 0 * 60` makes swaps expire immediately; subtly distinct from UC-T6 (that threat is about setting the var to 0; this is the runtime consequence when the zero value is used) | +| ID | STRIDE | Description | +| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| UC-T1 | Tampering | `setChainMeta` has no `whenNotPaused` guard — oracle/chain metadata is mutable even while the contract is paused, bypassing the intent of a pause | +| UC-T2 | Elevation of Privilege | `universalGatewayPC` is admin-mutable with no timelock; an attacker who compromises admin can point it to an attacker-controlled address that calls `swapAndBurnGas` with arbitrary native value | +| UC-T3 | Tampering | Uniswap V3 addresses (factory, router, quoter) are admin-mutable; replacement with malicious contracts enables fund diversion in `_autoSwap` and `swapAndBurnGas` | +| UC-T4 | Tampering | `WPC` address is admin-mutable; a malicious WETH-style contract at the new address can redirect or steal native PC during wrap/unwrap operations | +| UC-T5 | Denial of Service | `swapAndBurnGas` sends the native PC refund via `caller.call{value: refund}("")`; if `caller` reverts on ETH receive, the entire swap transaction reverts | +| UC-T6 | Denial of Service | `defaultDeadlineMins` is settable to `0` by admin; `deadline = block.timestamp + 0` makes all new swap transactions immediately expire at the EVM level | +| UC-T7 | Spoofing | `_validateParams` blocks `recipient == UNIVERSAL_EXECUTOR_MODULE` and `recipient == address(this)` but does not block other sensitive addresses (e.g., `universalGatewayPC`) | +| UC-T8 | Information Disclosure | `slippageTolerance` is stored on-chain but `minPCOut` is caller-supplied by the UE Module at call time; auditor should verify that the on-chain tolerance is enforced against the call-time value and not silently ignored | +| UC-T9 | Tampering | `defaultDeadlineMins == 0` path: `deadline = block.timestamp + 0 * 60` makes swaps expire immediately; subtly distinct from UC-T6 (that threat is about setting the var to 0; this is the runtime consequence when the zero value is used) | ### External Dependencies -| Dependency | Mutability | Trust Assumption | -|---|---|---| -| Uniswap V3 Factory | Admin-mutable | Pool lookup; wrong address causes `getPool` to return `address(0)` for all pools | -| Uniswap V3 SwapRouter | Admin-mutable | Executes swaps; a malicious router can steal tokens passed to it | -| Uniswap V3 Quoter | Admin-mutable | View-only; used off-chain for quote estimation | -| WPC | Admin-mutable | Must wrap/unwrap native PC 1:1; uses `.transfer()` for withdrawals | -| PRC20 tokens | Per-call address (from chain config) | Must implement `deposit()` and `burn()` per `IPRC20`; called with external trust | -| `universalGatewayPC` | Admin-mutable | Sole caller of `swapAndBurnGas`; assumed honest | +| Dependency | Mutability | Trust Assumption | +| --------------------- | ------------------------------------ | -------------------------------------------------------------------------------- | +| Uniswap V3 Factory | Admin-mutable | Pool lookup; wrong address causes `getPool` to return `address(0)` for all pools | +| Uniswap V3 SwapRouter | Admin-mutable | Executes swaps; a malicious router can steal tokens passed to it | +| Uniswap V3 Quoter | Admin-mutable | View-only; used off-chain for quote estimation | +| WPC | Admin-mutable | Must wrap/unwrap native PC 1:1; uses `.transfer()` for withdrawals | +| PRC20 tokens | Per-call address (from chain config) | Must implement `deposit()` and `burn()` per `IPRC20`; called with external trust | +| `universalGatewayPC` | Admin-mutable | Sole caller of `swapAndBurnGas`; assumed honest | ### Invariants @@ -212,26 +212,26 @@ Minting is gated to `UNIVERSAL_CORE` (mutable) or `UNIVERSAL_EXECUTOR_MODULE` ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `deposit(to, amount)` | `UNIVERSAL_CORE` or `UNIVERSAL_EXECUTOR_MODULE` | `InvalidSender` custom error check | -| `burn(amount)` | Any address | Balance check only | -| `updateUniversalCore(newCore)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | -| `setName(name)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | -| `setSymbol(symbol)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | -| Standard ERC-20 (`transfer`, `transferFrom`, `approve`, etc.) | Any address | Balance / allowance checks | -| `initialize(...)` | Anyone (once) | OZ `initializer` | +| Function | Caller | Guard | +| ------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------- | +| `deposit(to, amount)` | `UNIVERSAL_CORE` or `UNIVERSAL_EXECUTOR_MODULE` | `InvalidSender` custom error check | +| `burn(amount)` | Any address | Balance check only | +| `updateUniversalCore(newCore)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | +| `setName(name)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | +| `setSymbol(symbol)` | `UNIVERSAL_EXECUTOR_MODULE` | `onlyUniversalExecutor` | +| Standard ERC-20 (`transfer`, `transferFrom`, `approve`, etc.) | Any address | Balance / allowance checks | +| `initialize(...)` | Anyone (once) | OZ `initializer` | ### Threats -| ID | STRIDE | Description | -|---|---|---| +| ID | STRIDE | Description | +| ------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PRC-T1 | Elevation of Privilege | `UNIVERSAL_EXECUTOR_MODULE` (hardcoded, immutable) can call `deposit()` to mint unlimited tokens to any address; key compromise equals unbounded inflation with no on-chain recovery mechanism | -| PRC-T2 | Tampering | `UNIVERSAL_CORE` is mutable (settable by UE Module); replacing it with an attacker-controlled address opens a second unconstrained `deposit()` call path | -| PRC-T3 | Tampering | `_mint` and `_transfer` use `unchecked` arithmetic; no supply cap — `totalSupply` can reach `type(uint256).max` without reverting | -| PRC-T4 | Spoofing | `name` and `symbol` are mutable by UE Module post-deploy; renaming can mislead off-chain indexers, bridges, and users | -| PRC-T5 | Denial of Service | PRC20 has no pause mechanism; if `UniversalCore` is paused, `UNIVERSAL_EXECUTOR_MODULE` can still mint PRC20 tokens directly, bypassing the pause | -| PRC-T6 | Tampering | `transferFrom` deducts allowance after `_transfer` executes; the revert unwinds both, but confirm no ERC-777-style reentrancy hook is possible via a callback receiver during `_transfer` | +| PRC-T2 | Tampering | `UNIVERSAL_CORE` is mutable (settable by UE Module); replacing it with an attacker-controlled address opens a second unconstrained `deposit()` call path | +| PRC-T3 | Tampering | `_mint` and `_transfer` use `unchecked` arithmetic; no supply cap — `totalSupply` can reach `type(uint256).max` without reverting | +| PRC-T4 | Spoofing | `name` and `symbol` are mutable by UE Module post-deploy; renaming can mislead off-chain indexers, bridges, and users | +| PRC-T5 | Denial of Service | PRC20 has no pause mechanism; if `UniversalCore` is paused, `UNIVERSAL_EXECUTOR_MODULE` can still mint PRC20 tokens directly, bypassing the pause | +| PRC-T6 | Tampering | `transferFrom` deducts allowance after `_transfer` executes; the revert unwinds both, but confirm no ERC-777-style reentrancy hook is possible via a callback receiver during `_transfer` | ### External Dependencies @@ -257,20 +257,20 @@ Uniswap V3 swap paths that require an ERC-20 input. ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `deposit()` | Anyone | `payable` | -| `withdraw(wad)` | Any WPC holder | Balance `require` | -| `transfer`, `transferFrom`, `approve` | Anyone | Balance / allowance checks | -| `receive()` | Anyone | Auto-calls `deposit()` | +| Function | Caller | Guard | +| ------------------------------------- | -------------- | -------------------------- | +| `deposit()` | Anyone | `payable` | +| `withdraw(wad)` | Any WPC holder | Balance `require` | +| `transfer`, `transferFrom`, `approve` | Anyone | Balance / allowance checks | +| `receive()` | Anyone | Auto-calls `deposit()` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| WPC-T1 | Denial of Service | `withdraw` uses `payable(msg.sender).transfer(wad)` (2300 gas stipend); fails for recipients with non-trivial `receive()` logic. `UniversalCore`'s `receive()` is simple (safe), but any future caller contract must be validated | -| WPC-T2 | Tampering | `totalSupply()` returns `address(this).balance`; force-feeding native PC via `selfdestruct` inflates `totalSupply` above `sum(balanceOf)`. Not exploitable as `withdraw` keys on `balanceOf` not `totalSupply`, but breaks the supply/balance equality invariant | -| WPC-T3 | Information Disclosure | `require` reverts use empty strings (`""`); provides no diagnostic context for monitoring or debugging tooling | +| ID | STRIDE | Description | +| ------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| WPC-T1 | Denial of Service | `withdraw` uses `payable(msg.sender).transfer(wad)` (2300 gas stipend); fails for recipients with non-trivial `receive()` logic. `UniversalCore`'s `receive()` is simple (safe), but any future caller contract must be validated | +| WPC-T2 | Tampering | `totalSupply()` returns `address(this).balance`; force-feeding native PC via `selfdestruct` inflates `totalSupply` above `sum(balanceOf)`. Not exploitable as `withdraw` keys on `balanceOf` not `totalSupply`, but breaks the supply/balance equality invariant | +| WPC-T3 | Information Disclosure | `require` reverts use empty strings (`""`); provides no diagnostic context for monitoring or debugging tooling | ### External Dependencies @@ -296,33 +296,33 @@ and delegatecall migration. ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `executeUniversalTx` (signature path) | Any address | ECDSA against `_universalAccountId.owner`; `nonReentrant` | -| `executeUniversalTx` (bypass path) | `UNIVERSAL_EXECUTOR_MODULE` | Hardcoded address check; `nonReentrant` | -| `initialize(id, factory)` | Anyone (once) | `_initialized` bool flag (not OZ `initializer`) | -| Multicall sub-calls | Arbitrary `calls[i].to` | No allowlist — any contract address permitted | -| Migration delegatecall | `payload.to == address(this)` and `payload.value == 0` | Inline checks only | +| Function | Caller | Guard | +| ------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------- | +| `executeUniversalTx` (signature path) | Any address | ECDSA against `_universalAccountId.owner`; `nonReentrant` | +| `executeUniversalTx` (bypass path) | `UNIVERSAL_EXECUTOR_MODULE` | Hardcoded address check; `nonReentrant` | +| `initialize(id, factory)` | Anyone (once) | `_initialized` bool flag (not OZ `initializer`) | +| Multicall sub-calls | Arbitrary `calls[i].to` | No allowlist — any contract address permitted | +| Migration delegatecall | `payload.to == address(this)` and `payload.value == 0` | Inline checks only | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| UEA-EVM-T1 | Spoofing | **Known finding F-02**: domain separator encodes the source chain's `chainId` but not `block.chainid`; CREATE2-deterministic UEA addresses are identical across Push Chain deployments — a valid signature on testnet replays on mainnet | -| UEA-EVM-T2 | Elevation of Privilege | `UNIVERSAL_EXECUTOR_MODULE` bypasses ECDSA entirely; can execute arbitrary multicall payloads through any UEA without owner consent (stated design assumption — document key controls) | -| UEA-EVM-T3 | Tampering | `_handleMigration` fetches `ueaFactory.UEA_MIGRATION_CONTRACT()` at execution time; if factory admin rotates this to a malicious contract, any triggered migration causes full `UEAProxy` storage takeover via delegatecall in the proxy's storage context | -| UEA-EVM-T4 | Tampering | Multicall `calls[i].to` has no allowlist; a user can target the proxy itself, re-entering via the proxy's fallback — confirm `nonReentrant` on `executeUniversalTx` covers this re-entry path | -| UEA-EVM-T5 | Repudiation | `PayloadExecuted` event emits the post-increment nonce; off-chain indexers must subtract 1 to recover the pre-execution nonce — verify alignment with all tooling and explorers | -| UEA-EVM-T6 | Denial of Service | `UNIVERSAL_EXECUTOR_MODULE` can consume any nonce (by executing any payload), invalidating any in-flight user-signed transaction carrying that nonce | -| UEA-EVM-T7 | Tampering | Exactly 4 bytes of multicall data triggers `_decodeCalls` returning an empty `Multicall[]`; nonce increments for a no-op execution, burning the nonce silently | +| ID | STRIDE | Description | +| ---------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| UEA-EVM-T1 | Spoofing | **Known finding F-02**: domain separator encodes the source chain's `chainId` but not `block.chainid`; CREATE2-deterministic UEA addresses are identical across Push Chain deployments — a valid signature on testnet replays on mainnet | +| UEA-EVM-T2 | Elevation of Privilege | `UNIVERSAL_EXECUTOR_MODULE` bypasses ECDSA entirely; can execute arbitrary multicall payloads through any UEA without owner consent (stated design assumption — document key controls) | +| UEA-EVM-T3 | Tampering | `_handleMigration` fetches `ueaFactory.UEA_MIGRATION_CONTRACT()` at execution time; if factory admin rotates this to a malicious contract, any triggered migration causes full `UEAProxy` storage takeover via delegatecall in the proxy's storage context | +| UEA-EVM-T4 | Tampering | Multicall `calls[i].to` has no allowlist; a user can target the proxy itself, re-entering via the proxy's fallback — confirm `nonReentrant` on `executeUniversalTx` covers this re-entry path | +| UEA-EVM-T5 | Repudiation | `PayloadExecuted` event emits the post-increment nonce; off-chain indexers must subtract 1 to recover the pre-execution nonce — verify alignment with all tooling and explorers | +| UEA-EVM-T6 | Denial of Service | `UNIVERSAL_EXECUTOR_MODULE` can consume any nonce (by executing any payload), invalidating any in-flight user-signed transaction carrying that nonce | +| UEA-EVM-T7 | Tampering | Exactly 4 bytes of multicall data triggers `_decodeCalls` returning an empty `Multicall[]`; nonce increments for a no-op execution, burning the nonce silently | ### External Dependencies -| Dependency | Mutability | Trust Assumption | -|---|---|---| -| OZ ECDSA library | Immutable | `recover` returns `address(0)` on malformed sig; verify `verifyUniversalPayloadSignature` treats `address(0)` as false, not a match | -| `ueaFactory.UEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See UEA-EVM-T3 | -| Target contracts (single-call and multicall) | Untrusted | Arbitrary external calls with arbitrary calldata and value | +| Dependency | Mutability | Trust Assumption | +| -------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| OZ ECDSA library | Immutable | `recover` returns `address(0)` on malformed sig; verify `verifyUniversalPayloadSignature` treats `address(0)` as false, not a match | +| `ueaFactory.UEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See UEA-EVM-T3 | +| Target contracts (single-call and multicall) | Untrusted | Arbitrary external calls with arbitrary calldata and value | ### Invariants @@ -346,32 +346,32 @@ field is a 32-byte Solana public key (not an Ethereum address). ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `executeUniversalTx` (signature path) | Any address | Ed25519 via precompile against `_universalAccountId.owner`; `nonReentrant` | -| `executeUniversalTx` (bypass path) | `UNIVERSAL_EXECUTOR_MODULE` | Hardcoded address check; `nonReentrant` | -| `initialize(id, factory)` | Anyone (once) | `_initialized` bool flag | -| Multicall sub-calls | Arbitrary `calls[i].to` | No allowlist | -| Migration delegatecall | `payload.to == address(this)` and `payload.value == 0` | Inline checks | +| Function | Caller | Guard | +| ------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | +| `executeUniversalTx` (signature path) | Any address | Ed25519 via precompile against `_universalAccountId.owner`; `nonReentrant` | +| `executeUniversalTx` (bypass path) | `UNIVERSAL_EXECUTOR_MODULE` | Hardcoded address check; `nonReentrant` | +| `initialize(id, factory)` | Anyone (once) | `_initialized` bool flag | +| Multicall sub-calls | Arbitrary `calls[i].to` | No allowlist | +| Migration delegatecall | `payload.to == address(this)` and `payload.value == 0` | Inline checks | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| UEA-SVM-T1 | Spoofing | Same cross-deployment replay as UEA-EVM-T1; domain separator also omits `block.chainid` in the SVM implementation | -| UEA-SVM-T2 | Denial of Service | `staticcall` to `VERIFIER_PRECOMPILE`; if the precompile is unavailable on this chain or network fork, all SVM UEA executions revert with `PrecompileCallFailed` — no fallback path exists | -| UEA-SVM-T3 | Spoofing | `_universalAccountId.owner` is a raw `bytes` field (32-byte Solana pubkey); if the encoding passed to the precompile mismatches the expected format (padded vs. raw), all SVM signature verifications silently return false | -| UEA-SVM-T4 | Elevation of Privilege | Same UE Module bypass as UEA-EVM-T2; applies to Solana-origin accounts identically | -| UEA-SVM-T5 | Tampering | Same migration attack as UEA-EVM-T3; `_handleMigration` reads `ueaFactory.UEA_MIGRATION_CONTRACT()` at execution time | -| UEA-SVM-T6 | Denial of Service | Same nonce-burning as UEA-EVM-T6; UE Module can invalidate any pending user-signed SVM transaction | +| ID | STRIDE | Description | +| ---------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| UEA-SVM-T1 | Spoofing | Same cross-deployment replay as UEA-EVM-T1; domain separator also omits `block.chainid` in the SVM implementation | +| UEA-SVM-T2 | Denial of Service | `staticcall` to `VERIFIER_PRECOMPILE`; if the precompile is unavailable on this chain or network fork, all SVM UEA executions revert with `PrecompileCallFailed` — no fallback path exists | +| UEA-SVM-T3 | Spoofing | `_universalAccountId.owner` is a raw `bytes` field (32-byte Solana pubkey); if the encoding passed to the precompile mismatches the expected format (padded vs. raw), all SVM signature verifications silently return false | +| UEA-SVM-T4 | Elevation of Privilege | Same UE Module bypass as UEA-EVM-T2; applies to Solana-origin accounts identically | +| UEA-SVM-T5 | Tampering | Same migration attack as UEA-EVM-T3; `_handleMigration` reads `ueaFactory.UEA_MIGRATION_CONTRACT()` at execution time | +| UEA-SVM-T6 | Denial of Service | Same nonce-burning as UEA-EVM-T6; UE Module can invalidate any pending user-signed SVM transaction | ### External Dependencies -| Dependency | Mutability | Trust Assumption | -|---|---|---| -| Ed25519 precompile at `0x00...ca` | Hardcoded (Push Chain-specific) | Must be live and implement the expected input/output ABI; no fallback if unavailable | -| `ueaFactory.UEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See UEA-SVM-T5 | -| Target contracts (single-call and multicall) | Untrusted | Arbitrary external calls | +| Dependency | Mutability | Trust Assumption | +| -------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------ | +| Ed25519 precompile at `0x00...ca` | Hardcoded (Push Chain-specific) | Must be live and implement the expected input/output ABI; no fallback if unavailable | +| `ueaFactory.UEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See UEA-SVM-T5 | +| Target contracts (single-call and multicall) | Untrusted | Arbitrary external calls | ### Invariants @@ -397,34 +397,34 @@ mappings. Maintains bidirectional `UOA ↔ UEA` address index. ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `deployUEA(id)` | Anyone | `whenNotPaused` | -| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUEAMigrationContract` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `registerNewChain` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `registerUEA` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `registerMultipleUEA` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| Function | Caller | Guard | +| --------------------------- | -------------------- | --------------- | +| `deployUEA(id)` | Anyone | `whenNotPaused` | +| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | +| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setUEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setUEAMigrationContract` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `registerNewChain` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `registerUEA` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `registerMultipleUEA` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| UF-T1 | Tampering | `setUEAMigrationContract` has no timelock; admin can instantly point all UEAs to a malicious migration contract — any subsequently triggered migration causes full `UEAProxy` storage takeover via delegatecall | -| UF-T2 | Tampering | `setUEAProxyImplementation` changes the clone template for future `deployUEA` calls; does not affect existing deployed UEAs but all new deployments use the replacement template | -| UF-T3 | Spoofing | `getOriginForUEA(addr)` returns a synthetic Push Chain identity `{eip155, 42101, abi.encodePacked(addr)}` for non-UEA addresses; callers using this for authorization may conflate native Push Chain accounts with registered UEAs | -| UF-T4 | Tampering | `registerUEA` updates `UEA_VM[vmHash]` — a shared implementation pointer for all future proxies of that VM type; existing proxy `UEA_LOGIC_SLOT` values are unaffected | -| UF-T5 | Denial of Service | Pausing the factory blocks `deployUEA`; if first-time UEA deployment is required as part of the inbound execution pipeline, a pause prevents all new users from executing their first transaction | -| UF-T6 | Tampering | Salt = `keccak256(abi.encode(_id))` where `_id` contains string fields; auditor should verify that ABI encoding of `UniversalAccountId` is collision-free — two semantically distinct structs with identical byte encoding would share a salt and collide on CREATE2 | +| ID | STRIDE | Description | +| ----- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| UF-T1 | Tampering | `setUEAMigrationContract` has no timelock; admin can instantly point all UEAs to a malicious migration contract — any subsequently triggered migration causes full `UEAProxy` storage takeover via delegatecall | +| UF-T2 | Tampering | `setUEAProxyImplementation` changes the clone template for future `deployUEA` calls; does not affect existing deployed UEAs but all new deployments use the replacement template | +| UF-T3 | Spoofing | `getOriginForUEA(addr)` returns a synthetic Push Chain identity `{eip155, 42101, abi.encodePacked(addr)}` for non-UEA addresses; callers using this for authorization may conflate native Push Chain accounts with registered UEAs | +| UF-T4 | Tampering | `registerUEA` updates `UEA_VM[vmHash]` — a shared implementation pointer for all future proxies of that VM type; existing proxy `UEA_LOGIC_SLOT` values are unaffected | +| UF-T5 | Denial of Service | Pausing the factory blocks `deployUEA`; if first-time UEA deployment is required as part of the inbound execution pipeline, a pause prevents all new users from executing their first transaction | +| UF-T6 | Tampering | Salt = `keccak256(abi.encode(_id))` where `_id` contains string fields; auditor should verify that ABI encoding of `UniversalAccountId` is collision-free — two semantically distinct structs with identical byte encoding would share a salt and collide on CREATE2 | ### External Dependencies -| Dependency | Mutability | Trust Assumption | -|---|---|---| -| OZ Clones library | Immutable | `cloneDeterministic` reverts on address collision (existing bytecode at target) | -| `UEA_PROXY_IMPLEMENTATION` template | Admin-mutable | Must be a valid `UEAProxy` with an `initializeUEA` function | +| Dependency | Mutability | Trust Assumption | +| ----------------------------------- | ------------- | ------------------------------------------------------------------------------- | +| OZ Clones library | Immutable | `cloneDeterministic` reverts on address collision (existing bytecode at target) | +| `UEA_PROXY_IMPLEMENTATION` template | Admin-mutable | Must be a valid `UEAProxy` with an `initializeUEA` function | ### Invariants @@ -445,18 +445,18 @@ All calls are delegated to the implementation. No post-init admin functions. ### Access Control -| Function | Caller | Guard | -|---|---|---| +| Function | Caller | Guard | +| ----------------------- | --------------------------------------------------------- | -------------------------------------------- | | `initializeUEA(_logic)` | Anyone (intended: `UEAFactory` atomically in `deployUEA`) | OZ `initializer` + explicit slot-empty check | -| All other calls | Anyone | Delegated to `_implementation()` | +| All other calls | Anyone | Delegated to `_implementation()` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| UP-T1 | Elevation of Privilege | `initializeUEA` is callable by anyone on the un-cloned template contract; verify whether the template itself is initialized or left uninitialised (an uninitialised template is susceptible to direct hijack) | -| UP-T2 | Tampering | `UEA_LOGIC_SLOT` is non-EIP-1967; any future logic contract that accidentally writes to this storage offset corrupts the implementation pointer — migration contracts write here intentionally, verify exact constant match across all contracts | -| UP-T3 | Tampering | No admin path post-init to rotate implementation; upgrade requires a user-triggered migration payload — users who never trigger migration remain permanently on old logic, even after critical patches | +| ID | STRIDE | Description | +| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| UP-T1 | Elevation of Privilege | `initializeUEA` is callable by anyone on the un-cloned template contract; verify whether the template itself is initialized or left uninitialised (an uninitialised template is susceptible to direct hijack) | +| UP-T2 | Tampering | `UEA_LOGIC_SLOT` is non-EIP-1967; any future logic contract that accidentally writes to this storage offset corrupts the implementation pointer — migration contracts write here intentionally, verify exact constant match across all contracts | +| UP-T3 | Tampering | No admin path post-init to rotate implementation; upgrade requires a user-triggered migration payload — users who never trigger migration remain permanently on old logic, even after critical patches | ### Invariants @@ -478,19 +478,19 @@ both EVM and SVM UEAs via separate `migrateUEAEVM()` and `migrateUEASVM()` funct ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `migrateUEAEVM()` | Via delegatecall from a `UEAProxy` | `onlyDelegateCall` modifier | -| `migrateUEASVM()` | Via delegatecall from a `UEAProxy` | `onlyDelegateCall` modifier | -| Direct calls to either function | Anyone | Reverts — `onlyDelegateCall` | +| Function | Caller | Guard | +| ------------------------------- | ---------------------------------- | ---------------------------- | +| `migrateUEAEVM()` | Via delegatecall from a `UEAProxy` | `onlyDelegateCall` modifier | +| `migrateUEASVM()` | Via delegatecall from a `UEAProxy` | `onlyDelegateCall` modifier | +| Direct calls to either function | Anyone | Reverts — `onlyDelegateCall` | ### Threats -| ID | STRIDE | Description | -|---|---|---| +| ID | STRIDE | Description | +| ----- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | UM-T1 | Elevation of Privilege | `onlyDelegateCall` prevents direct calls to this contract but does not prevent _any other contract_ from delegatecalling `UEAMigration` in its own storage context; any contract that knows this address can corrupt its own slot at `UEA_LOGIC_SLOT`'s storage offset | -| UM-T2 | Tampering | Constructor validates both implementations have `extcodesize > 0`; if either implementation is later `selfdestruct`-ed (on chains where this is still possible), a triggered migration writes a dangling, empty implementation pointer | -| UM-T3 | Tampering | UEA_SVM triggers migration via `abi.encodeWithSignature("migrateUEASVM()")` and UEA_EVM via `"migrateUEAEVM()"` — verify no typos in these string literals; a mismatch causes all migrations to silently revert (function selector not found) | +| UM-T2 | Tampering | Constructor validates both implementations have `extcodesize > 0`; if either implementation is later `selfdestruct`-ed (on chains where this is still possible), a triggered migration writes a dangling, empty implementation pointer | +| UM-T3 | Tampering | UEA_SVM triggers migration via `abi.encodeWithSignature("migrateUEASVM()")` and UEA_EVM via `"migrateUEAEVM()"` — verify no typos in these string literals; a mismatch causes all migrations to silently revert (function selector not found) | ### Invariants @@ -507,44 +507,44 @@ both EVM and SVM UEAs via separate `migrateUEAEVM()` and `migrateUEASVM()` funct ### Role Logic contract for external-chain execution accounts. All execution is gated to -the `VAULT` address set at initialization. `isExecuted[txId]` provides per-CEA +the `VAULT` address set at initialization. `isExecuted[subTxId]` provides per-CEA replay protection keyed on Vault-supplied transaction IDs. The self-call path `sendUniversalTxToUEA` allows a CEA to initiate outbound bridging back to Push Chain. ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `initializeCEA(...)` | Anyone (once) | `_initialized` bool flag | -| `executeUniversalTx(txId, ...)` | `VAULT` | `onlyVault`, `nonReentrant`, `payable` | -| `sendUniversalTxToUEA(token, amount, payload)` | `address(this)` only | `msg.sender == address(this)` inline check | -| `receive()` | Anyone | `payable` | -| Multicall sub-calls | Arbitrary `calls[i].to` | `to != address(0)`; self-call with `value != 0` reverts | +| Function | Caller | Guard | +| ---------------------------------------------- | ----------------------- | ------------------------------------------------------- | +| `initializeCEA(...)` | Anyone (once) | `_initialized` bool flag | +| `executeUniversalTx(subTxId, ...)` | `VAULT` | `onlyVault`, `nonReentrant`, `payable` | +| `sendUniversalTxToUEA(token, amount, payload)` | `address(this)` only | `msg.sender == address(this)` inline check | +| `receive()` | Anyone | `payable` | +| Multicall sub-calls | Arbitrary `calls[i].to` | `to != address(0)`; self-call with `value != 0` reverts | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| CEA-T1 | Tampering | **Known finding F-01**: the ERC20 path of `sendUniversalTxToUEA` (lines 145-150) calls the gateway without first calling `IERC20(token).approve(UNIVERSAL_GATEWAY, amount)`; any gateway implementation using `transferFrom` will revert, permanently locking ERC20 tokens inside the CEA | -| CEA-T2 | Tampering | `_handleMigration` fetches `factory.CEA_MIGRATION_CONTRACT()` at execution time; factory admin rotating this to a malicious contract enables full `CEAProxy` storage takeover via delegatecall (same pattern as UEA-EVM-T3) | -| CEA-T3 | Elevation of Privilege | `VAULT` is immutable per-CEA (set at `initializeCEA` time); `CEAFactory.setVault` only affects new deployments — existing CEAs cannot rotate their Vault even if it is compromised | -| CEA-T4 | Tampering | `_handleSingleCall` forwards `msg.value` to the target: `recipient.call{value: msg.value}(payload)`; if the target reverts, the EVM refunds the value to the Vault — verify Vault-side accounting correctly handles this partial-execution refund | -| CEA-T5 | Spoofing | `originCaller == pushAccount` is the sole authorization check for outbound calls; an incorrect `pushAccount` set at `initializeCEA` permanently locks or unlocks the CEA to the wrong owner with no rotation path | -| CEA-T6 | Tampering | `isExecuted[txId] = true` is set before `_handleExecution`; a revert unwinds the entire transaction including the flag — replay protection is transaction-atomic (safe), but auditors should confirm this covers all execution paths | -| CEA-T7 | Denial of Service | An entire multicall batch reverts on any single failed sub-call; a crafted batch where an early step transfers value and a late step fails would be fully rolled back — value sent with the `payable` call is refunded by EVM revert | +| ID | STRIDE | Description | +| ------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CEA-T1 | Tampering | **Known finding F-01**: the ERC20 path of `sendUniversalTxToUEA` (lines 145-150) calls the gateway without first calling `IERC20(token).approve(UNIVERSAL_GATEWAY, amount)`; any gateway implementation using `transferFrom` will revert, permanently locking ERC20 tokens inside the CEA | +| CEA-T2 | Tampering | `_handleMigration` fetches `factory.CEA_MIGRATION_CONTRACT()` at execution time; factory admin rotating this to a malicious contract enables full `CEAProxy` storage takeover via delegatecall (same pattern as UEA-EVM-T3) | +| CEA-T3 | Elevation of Privilege | `VAULT` is immutable per-CEA (set at `initializeCEA` time); `CEAFactory.setVault` only affects new deployments — existing CEAs cannot rotate their Vault even if it is compromised | +| CEA-T4 | Tampering | `_handleSingleCall` forwards `msg.value` to the target: `recipient.call{value: msg.value}(payload)`; if the target reverts, the EVM refunds the value to the Vault — verify Vault-side accounting correctly handles this partial-execution refund | +| CEA-T5 | Spoofing | `originCaller == pushAccount` is the sole authorization check for outbound calls; an incorrect `pushAccount` set at `initializeCEA` permanently locks or unlocks the CEA to the wrong owner with no rotation path | +| CEA-T6 | Tampering | `isExecuted[subTxId] = true` is set before `_handleExecution`; a revert unwinds the entire transaction including the flag — replay protection is transaction-atomic (safe), but auditors should confirm this covers all execution paths | +| CEA-T7 | Denial of Service | An entire multicall batch reverts on any single failed sub-call; a crafted batch where an early step transfers value and a late step fails would be fully rolled back — value sent with the `payable` call is refunded by EVM revert | ### External Dependencies -| Dependency | Mutability | Trust Assumption | -|---|---|---| -| `VAULT` | Immutable per-CEA (set at init) | Controls all execution; compromise equals arbitrary execution from any Vault-managed CEA | -| `UNIVERSAL_GATEWAY` | Immutable per-CEA (set at init) | Destination for outbound sends; must not require ERC20 `approve` before `transferFrom` without it being provided (see CEA-T1) | -| `factory.CEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See CEA-T2 | -| Target contracts (multicall / single-call) | Untrusted | Arbitrary external calls | +| Dependency | Mutability | Trust Assumption | +| ------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `VAULT` | Immutable per-CEA (set at init) | Controls all execution; compromise equals arbitrary execution from any Vault-managed CEA | +| `UNIVERSAL_GATEWAY` | Immutable per-CEA (set at init) | Destination for outbound sends; must not require ERC20 `approve` before `transferFrom` without it being provided (see CEA-T1) | +| `factory.CEA_MIGRATION_CONTRACT()` | Factory admin-controlled | See CEA-T2 | +| Target contracts (multicall / single-call) | Untrusted | Arbitrary external calls | ### Invariants -1. `isExecuted[txId]` transitions only `false → true`, never reset +1. `isExecuted[subTxId]` transitions only `false → true`, never reset 2. `originCaller == pushAccount` is the sole execution authorization check — no signature verification 3. `sendUniversalTxToUEA` is only reachable via `msg.sender == address(this)` (multicall self-call path inside `nonReentrant` scope) 4. `CEA_LOGIC_SLOT` written by `CEAMigration` must match `CEAProxy.CEA_LOGIC_SLOT` @@ -563,26 +563,26 @@ Maintains bidirectional `pushAccount ↔ CEA` mappings. Stores shared config ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `deployCEA(pushAccount)` | `VAULT` | `onlyVault`, `whenNotPaused` | -| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setVault` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setCEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setCEAImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUniversalGateway` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setCEAMigrationContract` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| Function | Caller | Guard | +| --------------------------- | -------------------- | ---------------------------- | +| `deployCEA(pushAccount)` | `VAULT` | `onlyVault`, `whenNotPaused` | +| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | +| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setVault` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setCEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setCEAImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setUniversalGateway` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `setCEAMigrationContract` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| CF-T1 | Tampering | `setCEAMigrationContract` has no timelock; admin instant rotation to a malicious contract enables storage takeover for all future CEA migrations triggered by any CEA (same criticality as UF-T1) | -| CF-T2 | Tampering | `setVault` changes deployment authority immediately; the old Vault loses `deployCEA` access; existing CEA Vaults are unaffected (they hold the address from init) | -| CF-T3 | Elevation of Privilege | `deployCEA` accepts any non-zero `pushAccount` from the Vault; the factory cannot verify this is a real UEA on Push Chain — the Vault is fully trusted for address correctness | -| CF-T4 | Denial of Service | If a deployed CEA's code is destroyed (e.g., via `selfdestruct` on chains that still support it), `_hasCode` returns false but `pushAccountToCEA[pushAccount]` remains non-zero; a subsequent `deployCEA` for the same `pushAccount` will attempt CREATE2 which reverts (bytecode already at that address) — permanent lock-out for that `pushAccount` | -| CF-T5 | Tampering | `setUniversalGateway` updates the factory's `UNIVERSAL_GATEWAY` for new deployments only; existing CEAs carry their original gateway address, creating state divergence where old and new CEAs use different gateways concurrently | +| ID | STRIDE | Description | +| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| CF-T1 | Tampering | `setCEAMigrationContract` has no timelock; admin instant rotation to a malicious contract enables storage takeover for all future CEA migrations triggered by any CEA (same criticality as UF-T1) | +| CF-T2 | Tampering | `setVault` changes deployment authority immediately; the old Vault loses `deployCEA` access; existing CEA Vaults are unaffected (they hold the address from init) | +| CF-T3 | Elevation of Privilege | `deployCEA` accepts any non-zero `pushAccount` from the Vault; the factory cannot verify this is a real UEA on Push Chain — the Vault is fully trusted for address correctness | +| CF-T4 | Denial of Service | If a deployed CEA's code is destroyed (e.g., via `selfdestruct` on chains that still support it), `_hasCode` returns false but `pushAccountToCEA[pushAccount]` remains non-zero; a subsequent `deployCEA` for the same `pushAccount` will attempt CREATE2 which reverts (bytecode already at that address) — permanent lock-out for that `pushAccount` | +| CF-T5 | Tampering | `setUniversalGateway` updates the factory's `UNIVERSAL_GATEWAY` for new deployments only; existing CEAs carry their original gateway address, creating state divergence where old and new CEAs use different gateways concurrently | ### Invariants @@ -604,18 +604,18 @@ All calls are delegated to the implementation. ### Access Control -| Function | Caller | Guard | -|---|---|---| +| Function | Caller | Guard | +| ---------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- | | `initializeCEAProxy(_logic)` | Anyone (intended: `CEAFactory` atomically in `deployCEA`) | OZ `initializer` + explicit zero-address check on `_logic` | -| All other calls | Anyone | Delegated to `_implementation()` | +| All other calls | Anyone | Delegated to `_implementation()` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| CP-T1 | Tampering | `CEA_LOGIC_SLOT` must match `CEAMigration.CEA_LOGIC_SLOT` exactly; a constant mismatch between the two contracts corrupts the implementation pointer on every migration | -| CP-T2 | Elevation of Privilege | Same template-hijack consideration as UP-T1: `initializeCEAProxy` is callable by anyone on the un-cloned template contract if it is not already initialized | -| CP-T3 | Tampering | No post-init upgrade path other than migration; CEAs that are never triggered for migration remain on old logic indefinitely, even after critical patches | +| ID | STRIDE | Description | +| ----- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CP-T1 | Tampering | `CEA_LOGIC_SLOT` must match `CEAMigration.CEA_LOGIC_SLOT` exactly; a constant mismatch between the two contracts corrupts the implementation pointer on every migration | +| CP-T2 | Elevation of Privilege | Same template-hijack consideration as UP-T1: `initializeCEAProxy` is callable by anyone on the un-cloned template contract if it is not already initialized | +| CP-T3 | Tampering | No post-init upgrade path other than migration; CEAs that are never triggered for migration remain on old logic indefinitely, even after critical patches | ### Invariants @@ -637,18 +637,18 @@ context. `onlyDelegateCall` is enforced via immutable ### Access Control -| Function | Caller | Guard | -|---|---|---| -| `migrateCEA()` | Via delegatecall from a `CEAProxy` | `onlyDelegateCall` modifier | -| Direct calls to `migrateCEA()` | Anyone | Reverts — `onlyDelegateCall` | +| Function | Caller | Guard | +| ------------------------------ | ---------------------------------- | ---------------------------- | +| `migrateCEA()` | Via delegatecall from a `CEAProxy` | `onlyDelegateCall` modifier | +| Direct calls to `migrateCEA()` | Anyone | Reverts — `onlyDelegateCall` | ### Threats -| ID | STRIDE | Description | -|---|---|---| -| CM-T1 | Elevation of Privilege | Same as UM-T1: any contract knowing this address can delegatecall `migrateCEA()` to corrupt its own storage at `CEA_LOGIC_SLOT`'s offset | -| CM-T2 | Tampering | Constructor validates `_ceaImplementation` has code at deploy time; if the implementation is later destroyed, a triggered migration writes a dangling empty implementation pointer | -| CM-T3 | Tampering | CEA's `_handleMigration` encodes `abi.encodeWithSignature("migrateCEA()")`; a typo in this string literal causes all CEA migrations to silently revert (function selector not found) | +| ID | STRIDE | Description | +| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| CM-T1 | Elevation of Privilege | Same as UM-T1: any contract knowing this address can delegatecall `migrateCEA()` to corrupt its own storage at `CEA_LOGIC_SLOT`'s offset | +| CM-T2 | Tampering | Constructor validates `_ceaImplementation` has code at deploy time; if the implementation is later destroyed, a triggered migration writes a dangling empty implementation pointer | +| CM-T3 | Tampering | CEA's `_handleMigration` encodes `abi.encodeWithSignature("migrateCEA()")`; a typo in this string literal causes all CEA migrations to silently revert (function selector not found) | ### Invariants @@ -664,10 +664,10 @@ context. `onlyDelegateCall` is enforced via immutable ### Summary -| ID | Confidence | Location | Title | Status | -|---|---|---|---|---| -| F-01 | 80 | `src/cea/CEA.sol` L145-150 | Missing ERC20 approval before gateway call | Open | -| F-02 | 75 | `src/uea/UEA_EVM.sol` L80-93, `src/uea/UEA_SVM.sol` | Domain separator omits Push Chain `chainId` | Open | +| ID | Confidence | Location | Title | Status | +| ---- | ---------- | --------------------------------------------------- | ------------------------------------------- | ------ | +| F-01 | 80 | `src/cea/CEA.sol` L145-150 | Missing ERC20 approval before gateway call | Open | +| F-02 | 75 | `src/uea/UEA_EVM.sol` L80-93, `src/uea/UEA_SVM.sol` | Domain separator omits Push Chain `chainId` | Open | --- diff --git a/src/Interfaces/ICEA.sol b/src/Interfaces/ICEA.sol index cf955a8..e194734 100644 --- a/src/Interfaces/ICEA.sol +++ b/src/Interfaces/ICEA.sol @@ -13,13 +13,13 @@ interface ICEA { // ========================= /// @notice Emitted for each execution step (multicall or single call). - /// @param txId Unique transaction identifier + /// @param subTxId Unique transaction identifier /// @param universalTxId Universal transaction identifier on Universal Gateway /// @param originCaller Original caller on source chain (Push Chain) /// @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 subTxId, bytes32 indexed universalTxId, address indexed originCaller, address target, bytes data ); /// @notice Emitted when funds are sent from CEA to its UEA on Push Chain. @@ -45,10 +45,10 @@ interface ICEA { /// @return Initialization status function isInitialized() external view returns (bool); - /// @notice Returns whether a given txId has been executed. - /// @param txId Transaction identifier to check + /// @notice Returns whether a given subTxId has been executed. + /// @param subTxId Transaction identifier to check /// @return True if already executed - function isExecuted(bytes32 txId) external view returns (bool); + function isExecuted(bytes32 subTxId) external view returns (bool); // ========================= // CEA_2: VAULT OPERATIONS @@ -57,13 +57,13 @@ interface ICEA { /// @notice Executes a universal transaction. /// @dev Payload can be MULTICALL, MIGRATION, or SINGLE CALL format. /// Only callable by Vault. SDK crafts correct payload format. - /// @param txId Unique transaction identifier (must not be executed before) + /// @param subTxId Unique transaction identifier (must not be executed before) /// @param universalTxId Universal transaction identifier for cross-chain tracking /// @param originCaller Origin caller address (must match pushAccount) /// @param recipient Target contract for single-call. Ignored for multicall/migration. /// @param payload Multicall, migration, or single call payload function executeUniversalTx( - bytes32 txId, + bytes32 subTxId, bytes32 universalTxId, address originCaller, address recipient, diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index b53469b..14d1c5e 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -86,18 +86,18 @@ contract CEA is ICEA, ReentrancyGuard { /// @inheritdoc ICEA function executeUniversalTx( - bytes32 txId, + bytes32 subTxId, bytes32 universalTxId, address originCaller, address recipient, bytes calldata payload ) external payable onlyVault nonReentrant { - if (isExecuted[txId]) revert CEAErrors.PayloadExecuted(); + if (isExecuted[subTxId]) revert CEAErrors.PayloadExecuted(); if (originCaller != pushAccount) revert CEAErrors.InvalidUEA(); - isExecuted[txId] = true; + isExecuted[subTxId] = true; - _handleExecution(txId, universalTxId, originCaller, recipient, payload); + _handleExecution(subTxId, universalTxId, originCaller, recipient, payload); } // ========================= @@ -150,13 +150,13 @@ contract CEA is ICEA, ReentrancyGuard { /// @dev Routes execution based on payload type. /// Three-way branch: MULTICALL, MIGRATION, or SINGLE CALL. - /// @param txId Transaction identifier for event emission + /// @param subTxId Transaction identifier for event emission /// @param universalTxId Universal tx identifier for event emission /// @param originCaller Origin caller for event emission /// @param recipient Target for single-call path (ignored otherwise) /// @param payload Raw payload bytes function _handleExecution( - bytes32 txId, + bytes32 subTxId, bytes32 universalTxId, address originCaller, address recipient, @@ -164,22 +164,22 @@ contract CEA is ICEA, ReentrancyGuard { ) internal { if (_isMulticall(payload)) { Multicall[] memory calls = _decodeCalls(payload); - _handleMulticall(txId, universalTxId, originCaller, calls); + _handleMulticall(subTxId, universalTxId, originCaller, calls); } else if (_isMigration(payload)) { _handleMigration(recipient); - emit UniversalTxExecuted(txId, universalTxId, originCaller, address(this), payload); + emit UniversalTxExecuted(subTxId, universalTxId, originCaller, address(this), payload); } else { - _handleSingleCall(txId, universalTxId, originCaller, recipient, payload); + _handleSingleCall(subTxId, universalTxId, originCaller, recipient, payload); } } /// @dev Executes each multicall step sequentially. /// Self-calls must have value == 0. - /// @param txId Transaction identifier for event emission + /// @param subTxId Transaction identifier for event emission /// @param universalTxId Universal tx identifier for event emission /// @param originCaller Origin caller for event emission /// @param calls Decoded Multicall[] array - function _handleMulticall(bytes32 txId, bytes32 universalTxId, address originCaller, Multicall[] memory calls) + function _handleMulticall(bytes32 subTxId, bytes32 universalTxId, address originCaller, Multicall[] memory calls) internal { for (uint256 i = 0; i < calls.length; i++) { @@ -203,7 +203,7 @@ contract CEA is ICEA, ReentrancyGuard { } } - emit UniversalTxExecuted(txId, universalTxId, originCaller, calls[i].to, calls[i].data); + emit UniversalTxExecuted(subTxId, universalTxId, originCaller, calls[i].to, calls[i].data); } } @@ -211,20 +211,20 @@ contract CEA is ICEA, ReentrancyGuard { /// Note: Funds-parking mode is explicitly signalled by BOTH an empty payload AND a /// zero `recipient`. /// - /// @param txId Transaction identifier for event emission + /// @param subTxId Transaction identifier for event emission /// @param universalTxId Universal tx identifier for event emission /// @param originCaller Origin caller for event emission /// @param recipient Target contract for execution (zero + empty payload = park funds) /// @param payload Raw calldata to forward (empty + zero recipient = park funds) function _handleSingleCall( - bytes32 txId, + bytes32 subTxId, bytes32 universalTxId, address originCaller, address recipient, bytes calldata payload ) internal { if (payload.length == 0 && recipient == address(0)) { - emit UniversalTxExecuted(txId, universalTxId, originCaller, address(this), payload); + emit UniversalTxExecuted(subTxId, universalTxId, originCaller, address(this), payload); return; } @@ -246,7 +246,7 @@ contract CEA is ICEA, ReentrancyGuard { } } - emit UniversalTxExecuted(txId, universalTxId, originCaller, recipient, payload); + emit UniversalTxExecuted(subTxId, universalTxId, originCaller, recipient, payload); } /// @dev Fetches migration contract from factory and delegates. diff --git a/test/fuzz/CEAMigration_Fuzz.t.sol b/test/fuzz/CEAMigration_Fuzz.t.sol index 7885b81..5eebe93 100644 --- a/test/fuzz/CEAMigration_Fuzz.t.sol +++ b/test/fuzz/CEAMigration_Fuzz.t.sol @@ -83,10 +83,10 @@ contract CEAMigration_FuzzTest is Test { // Trigger migration via executeUniversalTx with MIGRATION_SELECTOR payload bytes memory payload = abi.encodePacked(bytes4(keccak256("UEA_MIGRATION"))); - bytes32 txId = keccak256("migration_slot_test"); + bytes32 subTxId = keccak256("migration_slot_test"); vm.prank(vault); - ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, ceaAddr, payload); + ICEA(ceaAddr).executeUniversalTx(subTxId, bytes32(0), ueaOnPush, ceaAddr, payload); // Verify slot was updated to ceaV2 bytes32 slotAfter = vm.load(ceaAddr, CEA_LOGIC_SLOT); @@ -101,13 +101,13 @@ contract CEAMigration_FuzzTest is Test { factory.setCEAMigrationContract(address(migration)); bytes memory payload = abi.encodePacked(bytes4(keccak256("UEA_MIGRATION"))); - bytes32 txId = keccak256("migration_event_test"); + bytes32 subTxId = keccak256("migration_event_test"); vm.expectEmit(true, false, false, false); emit CEAMigration.ImplementationUpdated(address(ceaV2)); vm.prank(vault); - ICEA(ceaAddr).executeUniversalTx(txId, bytes32(0), ueaOnPush, ceaAddr, payload); + ICEA(ceaAddr).executeUniversalTx(subTxId, bytes32(0), ueaOnPush, ceaAddr, payload); } // ========================================================================= diff --git a/test/fuzz/CEA_Fuzz.t.sol b/test/fuzz/CEA_Fuzz.t.sol index 2ea5279..018f8d2 100644 --- a/test/fuzz/CEA_Fuzz.t.sol +++ b/test/fuzz/CEA_Fuzz.t.sol @@ -80,26 +80,26 @@ contract CEA_FuzzTest is Test { // 8.1 Replay Protection Properties // ========================================================================= - /// @dev After successful execution, isExecuted[txId] is true. - function testFuzz_executeUniversalTx_uniqueTxId(bytes32 txId, bytes32 universalTxId) public { + /// @dev After successful execution, isExecuted[subTxId] is true. + function testFuzz_executeUniversalTx_uniqueTxId(bytes32 subTxId, bytes32 universalTxId) public { bytes memory payload = emptyMulticallPayload(); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, universalTxId, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxId, ueaOnPush, address(0), payload); - assertTrue(ceaInstance.isExecuted(txId)); + assertTrue(ceaInstance.isExecuted(subTxId)); } - /// @dev Second call with same txId always reverts with PayloadExecuted. - function testFuzz_executeUniversalTx_replayReverts(bytes32 txId, bytes32 universalTxId) public { + /// @dev Second call with same subTxId always reverts with PayloadExecuted. + function testFuzz_executeUniversalTx_replayReverts(bytes32 subTxId, bytes32 universalTxId) public { bytes memory payload = emptyMulticallPayload(); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, universalTxId, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxId, ueaOnPush, address(0), payload); vm.expectRevert(CEAErrors.PayloadExecuted.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, universalTxId, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxId, ueaOnPush, address(0), payload); } /// @dev Different txIds execute independently without replay issues. @@ -123,24 +123,24 @@ contract CEA_FuzzTest is Test { // ========================================================================= /// @dev When originCaller != pushAccount, reverts with InvalidUEA. - function testFuzz_executeUniversalTx_wrongOriginCaller_reverts(address wrongCaller, bytes32 txId) public { + function testFuzz_executeUniversalTx_wrongOriginCaller_reverts(address wrongCaller, bytes32 subTxId) public { vm.assume(wrongCaller != ueaOnPush); bytes memory payload = emptyMulticallPayload(); vm.expectRevert(CEAErrors.InvalidUEA.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), wrongCaller, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), wrongCaller, address(0), payload); } /// @dev When originCaller == pushAccount, origin check passes. - function testFuzz_executeUniversalTx_correctOriginCaller_passes(bytes32 txId) public { + function testFuzz_executeUniversalTx_correctOriginCaller_passes(bytes32 subTxId) public { bytes memory payload = emptyMulticallPayload(); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); - assertTrue(ceaInstance.isExecuted(txId)); + assertTrue(ceaInstance.isExecuted(subTxId)); } // ========================================================================= @@ -158,22 +158,22 @@ contract CEA_FuzzTest is Test { // Valid multicall selector — would attempt multicall decode // Use a properly encoded empty multicall to verify it passes bytes memory validPayload = emptyMulticallPayload(); - bytes32 txId = keccak256(abi.encode("multicall_test", selector)); + bytes32 subTxId = keccak256(abi.encode("multicall_test", selector)); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), validPayload); - assertTrue(ceaInstance.isExecuted(txId)); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), validPayload); + assertTrue(ceaInstance.isExecuted(subTxId)); } else if (selector == MIGRATION_SELECTOR) { // Migration selector — different path vm.assume(selector != MULTICALL_SELECTOR && selector != MIGRATION_SELECTOR); } else { // Non-multicall, non-migration — single call path with the payload // An empty non-special payload parks funds successfully - bytes32 txId = keccak256(abi.encode("non_multicall", selector)); + bytes32 subTxId = keccak256(abi.encode("non_multicall", selector)); vm.prank(vault); // Single-call path with non-zero selector and no recipient won't revert if payload is short // Use empty payload to park funds safely - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), ""); - assertTrue(ceaInstance.isExecuted(txId)); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), ""); + assertTrue(ceaInstance.isExecuted(subTxId)); } } @@ -182,10 +182,10 @@ contract CEA_FuzzTest is Test { vm.assume(selector != MIGRATION_SELECTOR && selector != MULTICALL_SELECTOR); // Build a payload with a non-migration, non-multicall selector // Should go to single-call path — use empty payload which parks funds - bytes32 txId = keccak256(abi.encode("migration_selector_test", selector, remaining)); + bytes32 subTxId = keccak256(abi.encode("migration_selector_test", selector, remaining)); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), ""); - assertTrue(ceaInstance.isExecuted(txId)); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), ""); + assertTrue(ceaInstance.isExecuted(subTxId)); } /// @dev Payloads shorter than 4 bytes never trigger multicall or migration. @@ -198,30 +198,30 @@ contract CEA_FuzzTest is Test { payload[i] = 0xff; } - bytes32 txId = keccak256(abi.encode("short_payload", length)); + bytes32 subTxId = keccak256(abi.encode("short_payload", length)); vm.prank(vault); // Short payload goes to single-call path; empty payload parks funds // Non-empty short payloads without a valid recipient will revert with InvalidRecipient if (length == 0) { - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); - assertTrue(ceaInstance.isExecuted(txId)); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); + assertTrue(ceaInstance.isExecuted(subTxId)); } else { // Non-empty short payload -> single call path -> needs recipient // With address(0) recipient it reverts InvalidRecipient vm.expectRevert(CEAErrors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } } /// @dev When payload is empty, funds are parked in CEA without external call. - function testFuzz_singleCall_emptyPayload_parksFunds(bytes32 txId, uint256 value) public { + function testFuzz_singleCall_emptyPayload_parksFunds(bytes32 subTxId, uint256 value) public { value = bound(value, 0, 100 ether); vm.deal(vault, value); vm.prank(vault); - ceaInstance.executeUniversalTx{value: value}(txId, bytes32(0), ueaOnPush, address(0), ""); + ceaInstance.executeUniversalTx{value: value}(subTxId, bytes32(0), ueaOnPush, address(0), ""); - assertTrue(ceaInstance.isExecuted(txId)); + assertTrue(ceaInstance.isExecuted(subTxId)); // Funds are parked in CEA assertEq(address(ceaInstance).balance, value); } @@ -247,11 +247,11 @@ contract CEA_FuzzTest is Test { } bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("zero_target", numCalls, zeroIndex)); + bytes32 subTxId = keccak256(abi.encode("zero_target", numCalls, zeroIndex)); vm.expectRevert(CEAErrors.InvalidTarget.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } /// @dev Self-call with value > 0 reverts with InvalidInput. @@ -263,11 +263,11 @@ contract CEA_FuzzTest is Test { calls[0] = makeCall(address(ceaInstance), value, ""); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("self_call_value", value)); + bytes32 subTxId = keccak256(abi.encode("self_call_value", value)); vm.expectRevert(CEAErrors.InvalidInput.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } /// @dev Self-call with value == 0 is allowed in multicall. @@ -284,12 +284,12 @@ contract CEA_FuzzTest is Test { calls[0] = makeCall(address(ceaInstance), 0, safeData); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("self_call_zero_value", safeData)); + bytes32 subTxId = keccak256(abi.encode("self_call_zero_value", safeData)); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); - assertTrue(ceaInstance.isExecuted(txId)); + assertTrue(ceaInstance.isExecuted(subTxId)); } /// @dev In CEA._handleMulticall there is NO migration selector check inside array. @@ -318,13 +318,13 @@ contract CEA_FuzzTest is Test { } bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("migration_in_array", numCalls, migIdx)); + bytes32 subTxId = keccak256(abi.encode("migration_in_array", numCalls, migIdx)); // The call with MIGRATION_SELECTOR data will fail at the target level, // causing ExecutionFailed — but crucially NOT InvalidCall. vm.expectRevert(CEAErrors.ExecutionFailed.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } // ========================================================================= @@ -332,7 +332,7 @@ contract CEA_FuzzTest is Test { // ========================================================================= /// @dev Migration with msg.value > 0 reverts with InvalidInput. - function testFuzz_migration_withValue_reverts(uint256 value, bytes32 txId) public { + function testFuzz_migration_withValue_reverts(uint256 value, bytes32 subTxId) public { value = bound(value, 1, 100 ether); vm.deal(vault, value); @@ -340,17 +340,17 @@ contract CEA_FuzzTest is Test { vm.expectRevert(CEAErrors.InvalidInput.selector); vm.prank(vault); - ceaInstance.executeUniversalTx{value: value}(txId, bytes32(0), ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx{value: value}(subTxId, bytes32(0), ueaOnPush, address(ceaInstance), payload); } /// @dev When factory has no migration contract set, migration reverts with InvalidCall. - function testFuzz_migration_noMigrationContract_reverts(bytes32 txId) public { + function testFuzz_migration_noMigrationContract_reverts(bytes32 subTxId) public { // Factory has no migration contract (CEA_MIGRATION_CONTRACT == address(0) by default) bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); vm.expectRevert(CEAErrors.InvalidCall.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -358,7 +358,7 @@ contract CEA_FuzzTest is Test { // ========================================================================= /// @dev When caller != VAULT, executeUniversalTx always reverts with NotVault. - function testFuzz_executeUniversalTx_nonVault_reverts(address caller, bytes32 txId) public { + function testFuzz_executeUniversalTx_nonVault_reverts(address caller, bytes32 subTxId) public { vm.assume(caller != vault); vm.assume(caller != address(0)); @@ -366,7 +366,7 @@ contract CEA_FuzzTest is Test { vm.expectRevert(CEAErrors.NotVault.selector); vm.prank(caller); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } /// @dev When caller != address(this), sendUniversalTxToUEA always reverts with Unauthorized. @@ -396,10 +396,10 @@ contract CEA_FuzzTest is Test { ); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("zero_amount_native")); + bytes32 subTxId = keccak256(abi.encode("zero_amount_native")); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for native"); } @@ -417,10 +417,10 @@ contract CEA_FuzzTest is Test { ); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("zero_amount_erc20")); + bytes32 subTxId = keccak256(abi.encode("zero_amount_erc20")); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for ERC20"); } @@ -442,11 +442,11 @@ contract CEA_FuzzTest is Test { ); bytes memory payload = encodeCalls(calls); - bytes32 txId = keccak256(abi.encode("insufficient_balance", amount)); + bytes32 subTxId = keccak256(abi.encode("insufficient_balance", amount)); // The inner call reverts with InsufficientBalance, now propagated vm.expectRevert(CEAErrors.InsufficientBalance.selector); vm.prank(vault); - ceaInstance.executeUniversalTx(txId, bytes32(0), ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, bytes32(0), ueaOnPush, address(0), payload); } } diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 613d543..b6a2538 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -87,9 +87,9 @@ contract CEATest is Test { // Helper Functions - Canonical Multicall Builders // ========================================================================= - /// @notice Generate a unique txID for testing + /// @notice Generate a unique subTxId for testing function generateTxID(uint256 nonce) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("txID", nonce)); + return keccak256(abi.encodePacked("subTxId", nonce)); } /// @notice Generate a unique universalTxID for testing @@ -366,14 +366,14 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = 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); vm.prank(nonVault); vm.expectRevert(Errors.NotVault.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function testExecuteUniversalTx_SuccessWhenCalledByVault() public deployCEA { @@ -381,15 +381,15 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = 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); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(spender.totalReceived(address(token)), 100 ether, "Target should receive tokens"); } @@ -402,18 +402,18 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = 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); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - // Try to execute same txID again + // Try to execute same subTxId again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } // ------------------------------------------------------------------------- @@ -424,7 +424,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -432,14 +432,14 @@ 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"), address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload); } function testExecuteUniversalTx_RevertWhenTargetIsZero() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -447,7 +447,7 @@ 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testExecuteUniversalTx_SuccessWithSufficientTokenBalance() public deployCEA { @@ -455,14 +455,14 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 100 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Exact balance should work"); } @@ -482,14 +482,14 @@ contract CEATest is Test { token.approve(address(spender), 500 ether); assertEq(token.allowance(address(ceaInstance), address(spender)), 500 ether, "Initial approval should exist"); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Approval should be reset to 0 after execution assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Approval should be reset"); @@ -500,7 +500,7 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); @@ -508,7 +508,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Correct amount should be approved and spent"); } @@ -518,14 +518,14 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Approval should be reset to 0 after execution assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Approval should be reset after execution"); @@ -540,7 +540,7 @@ contract CEATest is Test { token.approve(address(target), 500 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("spendTokens(address,uint256)", address(token), 100 ether); @@ -548,7 +548,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq( spender.totalReceived(address(token)), 100 ether, "Execution should succeed despite zero approval revert" @@ -563,14 +563,14 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); } @@ -580,7 +580,7 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenReceiverTarget receiver = new TokenReceiverTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("receiveTokens(address,uint256)", address(token), 100 ether); @@ -588,7 +588,7 @@ contract CEATest is Test { bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(receiver), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), 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"); @@ -599,7 +599,7 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("revertWithReason()"); @@ -609,11 +609,12 @@ contract CEATest is Test { // Underlying revert reason is now propagated vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - // txID should NOT be marked as executed when execution fails + // subTxId 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" + CEA(payable(address(ceaInstance))).isExecuted(subTxId), + "subTxId should not be marked as executed on failure" ); } @@ -622,7 +623,7 @@ contract CEATest is Test { fundCEAWithTokens(address(token), 1000 ether); TokenSpenderTarget spender = new TokenSpenderTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = ""; // Empty payload @@ -634,7 +635,7 @@ contract CEATest is Test { bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(spender), 100 ether, spendPayload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(spender.totalReceived(address(token)), 100 ether, "Empty payload should work"); } @@ -643,7 +644,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 magicValue = 999; bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", magicValue); @@ -651,7 +652,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildERC20MulticallPayload(address(token), address(target), 100 ether, payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(target.getMagicNumber(), magicValue, "Payload should execute with correct parameters"); } @@ -663,7 +664,7 @@ contract CEATest is Test { function testExecuteUniversalTx_RevertWhenCalledByNonVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -672,13 +673,15 @@ 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0.1 ether}( + subTxId, universalTxID, ueaOnPush, address(0), multicallPayload + ); } function testExecuteUniversalTx_RevertWhenInvalidUEA_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -688,14 +691,14 @@ contract CEATest is Test { bytes memory multicallPayload = buildNativeMulticallPayload(address(target), 0.1 ether, payload); ceaInstance.executeUniversalTx{value: 0.1 ether}( - txID, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload + subTxId, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload ); } function testExecuteUniversalTx_MsgValueExceedsCallValue_Native_Succeeds() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -704,7 +707,9 @@ contract CEATest is Test { 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0.2 ether}( + subTxId, universalTxID, ueaOnPush, address(0), multicallPayload + ); assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); } @@ -712,7 +717,7 @@ contract CEATest is Test { function testExecuteUniversalTx_SuccessWhenMsgValueEqualsAmount_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); uint256 amount = 0.1 ether; @@ -721,7 +726,7 @@ contract CEATest is Test { vm.deal(vault, amount); bytes memory multicallPayload = buildNativeMulticallPayload(address(target), amount, payload); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(address(target).balance, amount, "Target should receive correct amount"); } @@ -734,7 +739,7 @@ contract CEATest is Test { function testExecuteUniversalTx_SuccessfulCallToTarget_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); @@ -742,7 +747,9 @@ 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0.1 ether}( + subTxId, universalTxID, ueaOnPush, address(0), multicallPayload + ); assertEq(target.getMagicNumber(), 42, "Target should execute correctly"); assertEq(address(target).balance, 0.1 ether, "Target should receive native tokens"); @@ -752,7 +759,7 @@ contract CEATest is Test { fundCEAWithNative(1000 ether); TokenReceiverTarget receiver = new TokenReceiverTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("receiveNative()"); uint256 amount = 0.5 ether; @@ -761,7 +768,7 @@ contract CEATest is Test { vm.deal(vault, amount); bytes memory multicallPayload = buildNativeMulticallPayload(address(receiver), amount, payload); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(receiver.nativeBalance(), amount, "Target should receive correct native amount"); } @@ -770,7 +777,7 @@ contract CEATest is Test { fundCEAWithNative(1000 ether); RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("revertWithReason()"); @@ -779,7 +786,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, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0.1 ether}( + subTxId, universalTxID, ueaOnPush, address(0), multicallPayload + ); } // ========================================================================= @@ -790,7 +799,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); @@ -799,15 +808,15 @@ contract CEATest is Test { // Note: Event is emitted per multicall step (3 events: reset approval, approve, execute) vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(target), payload); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(target), payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testExecuteUniversalTx_EmitsUniversalTxExecutedEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumberWithFee(uint256)", 42); uint256 amount = 0.1 ether; @@ -817,9 +826,9 @@ contract CEATest is Test { bytes memory multicallPayload = buildNativeMulticallPayload(address(target), amount, payload); vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(target), payload); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(target), payload); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } // ------------------------------------------------------------------------- // 1. ACCESS CONTROL & AUTHORIZATION TESTS @@ -829,7 +838,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); @@ -837,23 +846,23 @@ contract CEATest is Test { vm.expectRevert(Errors.NotVault.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_SuccessWhenCalledByVault() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } @@ -865,26 +874,26 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - // Try to execute same txID again + // Try to execute same subTxId again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenInvalidUEA() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); @@ -892,14 +901,14 @@ contract CEATest is Test { vm.expectRevert(Errors.InvalidUEA.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenPayloadTooShort() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Create multicall with malformed self-call data (too short) @@ -910,14 +919,14 @@ 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenInvalidSelector() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Create multicall with wrong selector (try to call initializeCEA) @@ -934,7 +943,7 @@ contract CEATest is Test { vm.prank(vault); // Calls initializeCEA via .call() which reverts with AlreadyInitialized vm.expectRevert(Errors.AlreadyInitialized.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } // ------------------------------------------------------------------------- @@ -945,7 +954,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 100 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); @@ -953,23 +962,23 @@ contract CEATest is Test { vm.expectRevert(Errors.InsufficientBalance.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_SuccessWithExactERC20Balance() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 500 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } @@ -977,16 +986,16 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); } // ------------------------------------------------------------------------- @@ -997,7 +1006,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), amount, ueaOnPush); @@ -1005,7 +1014,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(token), "Token should match"); @@ -1020,7 +1029,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); @@ -1029,7 +1038,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.callCount(), callCountBefore + 1, "Gateway should be called exactly once"); } @@ -1051,14 +1060,14 @@ contract CEATest is Test { "Initial approval should exist" ); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Approval persists after gateway call (gateway consumes via transferFrom in production) assertEq( @@ -1072,7 +1081,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), amount, ueaOnPush); @@ -1080,7 +1089,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Approval persists after gateway call (mock gateway doesn't consume) assertEq( @@ -1096,18 +1105,18 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(token), 500 ether, ueaOnPush); - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be executed before"); + assertFalse(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be executed before"); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 500 ether, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed after"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed after"); } function testSendUniversalTxToUEA_ERC20BalanceDecreases() public deployCEA { @@ -1115,7 +1124,7 @@ contract CEATest is Test { uint256 initialBalance = 1000 ether; fundCEAWithTokens(address(token), initialBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), sendAmount, ueaOnPush); @@ -1125,16 +1134,14 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), sendAmount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Mock gateway doesn't transfer tokens, so balance unchanged uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), - sendAmount, - "Approval should persist" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, "Approval should persist" ); } @@ -1146,7 +1153,7 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), amount, ueaOnPush); @@ -1157,14 +1164,14 @@ contract CEATest is Test { bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_EmitsUniversalTxExecutedEvent_ERC20() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), amount, ueaOnPush); @@ -1173,9 +1180,9 @@ contract CEATest is Test { bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } // ------------------------------------------------------------------------- @@ -1186,13 +1193,13 @@ contract CEATest is Test { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), 0, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for ERC20"); } @@ -1204,16 +1211,16 @@ contract CEATest is Test { uint256 amount = 500 ether; for (uint256 i = 1; i <= 3; i++) { - bytes32 txID = generateTxID(i); + bytes32 subTxId = generateTxID(i); bytes32 universalTxID = generateUniversalTxID(i); bytes memory payload = buildSendToUEAPayload(address(token), amount, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), amount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); } assertEq(mockUniversalGateway.callCount(), 3, "Gateway should be called 3 times"); @@ -1228,7 +1235,7 @@ contract CEATest is Test { uint256 initialBalance = 1000 ether; fundCEAWithTokens(address(token), initialBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(token), sendAmount, ueaOnPush); @@ -1239,10 +1246,10 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), sendAmount, true); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Verify all state changes - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), gatewayCallCountBefore + 1, "Gateway should be called once"); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); @@ -1254,9 +1261,7 @@ contract CEATest is Test { assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), - sendAmount, - "Approval should persist" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, "Approval should persist" ); } @@ -1267,7 +1272,7 @@ contract CEATest is Test { function testSendUniversalTxToUEA_RevertWhenCalledByNonVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); @@ -1276,47 +1281,47 @@ contract CEATest is Test { vm.expectRevert(Errors.NotVault.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_SuccessWhenCalledByVault_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } function testSendUniversalTxToUEA_RevertWhenTxIDAlreadyExecuted_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - // Try to execute same txID again + // Try to execute same subTxId again vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenInvalidUEA_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); @@ -1325,14 +1330,14 @@ contract CEATest is Test { bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); ceaInstance.executeUniversalTx{value: 0}( - txID, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload + subTxId, universalTxID, makeAddr("wrongUEA"), address(0), multicallPayload ); } function testSendUniversalTxToUEA_RevertWhenPayloadTooShort_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Create multicall with malformed self-call data (too short) @@ -1343,13 +1348,13 @@ 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, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenInvalidSelector_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Create multicall with wrong selector (try to call initializeCEA) @@ -1366,12 +1371,12 @@ contract CEATest is Test { vm.prank(vault); // Calls initializeCEA via .call() which reverts with AlreadyInitialized vm.expectRevert(Errors.AlreadyInitialized.selector); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_RevertWhenInsufficientNativeBalance() public deployCEA { // Don't fund CEA - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); @@ -1379,45 +1384,45 @@ contract CEATest is Test { vm.expectRevert(Errors.InsufficientBalance.selector); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_SuccessWithExactNativeBalance() public deployCEA { uint256 balance = 500 ether; fundCEAWithNative(balance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), balance, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), balance, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } function testSendUniversalTxToUEA_SuccessWithMoreThanRequiredBalance_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); } function testSendUniversalTxToUEA_CallsGatewayWithCorrectParams_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(0), amount, ueaOnPush); @@ -1425,7 +1430,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); assertEq(mockUniversalGateway.lastToken(), address(0), "Token should be address(0) for native"); @@ -1438,7 +1443,7 @@ contract CEATest is Test { function testSendUniversalTxToUEA_CallsGatewayExactlyOnce_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); @@ -1447,7 +1452,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.callCount(), callCountBefore + 1, "Gateway should be called exactly once"); } @@ -1455,25 +1460,25 @@ contract CEATest is Test { function testSendUniversalTxToUEA_MarksTxIDAsExecuted_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildSendToUEAPayload(address(0), 500 ether, ueaOnPush); - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be executed before"); + assertFalse(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be executed before"); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 500 ether, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed after"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed after"); } function testSendUniversalTxToUEA_NativeBalanceDecreases() public deployCEA { uint256 initialBalance = 1000 ether; fundCEAWithNative(initialBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(0), sendAmount, ueaOnPush); @@ -1483,7 +1488,7 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), sendAmount, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); uint256 balanceAfter = address(ceaInstance).balance; assertEq(balanceAfter, balanceBefore - sendAmount, "Balance should decrease by exact amount"); @@ -1493,7 +1498,7 @@ contract CEATest is Test { function testSendUniversalTxToUEA_EmitsUniversalTxToUEAEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(0), amount, ueaOnPush); @@ -1504,13 +1509,13 @@ contract CEATest is Test { bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_EmitsUniversalTxExecutedEvent_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(0), amount, ueaOnPush); @@ -1519,21 +1524,21 @@ contract CEATest is Test { bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); } function testSendUniversalTxToUEA_HandlesZeroAmount_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), 0, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); assertEq(mockUniversalGateway.lastAmount(), 0, "Should allow zero amount for native"); } @@ -1544,16 +1549,16 @@ contract CEATest is Test { uint256 amount = 500 ether; for (uint256 i = 1; i <= 3; i++) { - bytes32 txID = generateTxID(i); + bytes32 subTxId = generateTxID(i); bytes32 universalTxID = generateUniversalTxID(i); bytes memory payload = buildSendToUEAPayload(address(0), amount, ueaOnPush); vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), amount, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); } assertEq(mockUniversalGateway.callCount(), 3, "Gateway should be called 3 times"); @@ -1563,7 +1568,7 @@ contract CEATest is Test { uint256 initialBalance = 1000 ether; fundCEAWithNative(initialBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; bytes memory payload = buildSendToUEAPayload(address(0), sendAmount, ueaOnPush); @@ -1574,10 +1579,10 @@ contract CEATest is Test { vm.prank(vault); bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(0), sendAmount, false); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), multicallPayload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); // Verify all state changes - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked as executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked as executed"); assertEq(mockUniversalGateway.callCount(), gatewayCallCountBefore + 1, "Gateway should be called once"); assertEq(mockUniversalGateway.lastRecipient(), ueaOnPush, "Recipient should be UEA"); @@ -1662,7 +1667,7 @@ contract CEATest is Test { function testExecuteUniversalTx_Native_IsExecutedOnlyOnSuccess() public deployCEA { RevertingTarget reverter = new RevertingTarget(); uint256 amount = 0.1 ether; - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); vm.deal(vault, amount); vm.prank(vault); @@ -1670,11 +1675,11 @@ contract CEATest is Test { bytes memory multicallPayload = buildNativeMulticallPayload(address(reverter), amount, bytes("")); ceaInstance.executeUniversalTx{value: amount}( - txID, generateUniversalTxID(1), ueaOnPush, address(0), multicallPayload + subTxId, generateUniversalTxID(1), ueaOnPush, address(0), multicallPayload ); assertFalse( - CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked executed on failure" + CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be marked executed on failure" ); } diff --git a/test/tests_cea/CEA_multicalls.t.sol b/test/tests_cea/CEA_multicalls.t.sol index 031d86d..4aee6b4 100644 --- a/test/tests_cea/CEA_multicalls.t.sol +++ b/test/tests_cea/CEA_multicalls.t.sol @@ -15,7 +15,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_RevertWhen_NonEmptyPayload_ZeroRecipient() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Non-empty payload without MULTICALL_SELECTOR hits single-call path @@ -23,11 +23,11 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert(Errors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), invalidPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), invalidPayload); } function test_RevertWhen_PayloadIsEmptyCallsArray() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Empty calls array @@ -36,9 +36,9 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); // Should succeed (empty multicall is valid, just does nothing) - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } // ========================================================================= @@ -46,21 +46,21 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_SingleExternalCall_NoValue_Success() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory targetCalldata = abi.encodeWithSignature("setMagicNumber(uint256)", 42); bytes memory payload = buildExternalSingleCall(address(target), 0, targetCalldata); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(target.magicNumber(), 42, "Target should have magic number set"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be executed"); } function test_SingleExternalCall_WithValue_Success() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 value = 0.1 ether; // Target requires exactly 0.1 ETH fee @@ -70,14 +70,14 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = buildExternalSingleCall(address(target), value, targetCalldata); vm.prank(vault); - ceaInstance.executeUniversalTx{value: value}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: value}(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(target.magicNumber(), 42, "Target should have magic number set"); assertEq(address(target).balance, value, "Target should have received ETH"); } function test_MultiStepBatch_AllSucceed_InOrder() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](3); @@ -88,7 +88,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = buildExternalBatch(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Final value should be 30 (last call) assertEq(target.magicNumber(), 30, "Target should have final magic number"); @@ -101,7 +101,7 @@ contract CEA_NewMulticallTests is CEATest { // Fund CEA with tokens fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -116,7 +116,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = buildExternalBatch(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(spender.totalReceived(address(token)), 100 ether, "Spender should have received tokens"); } @@ -128,7 +128,7 @@ contract CEA_NewMulticallTests is CEATest { function test_RevertWhen_AnySubcallReverts_BubblesReason() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = @@ -136,13 +136,13 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_RevertWhen_LaterSubcallReverts_RollsBackEarlierEffects() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -155,7 +155,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // First call's effect should be rolled back assertEq(target.magicNumber(), magicBefore, "Target magic number should be unchanged after revert"); @@ -164,7 +164,7 @@ contract CEA_NewMulticallTests is CEATest { function test_TxIDNotMarked_WhenExecutionReverts() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = @@ -172,16 +172,16 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - // txID should NOT be marked as executed since the tx reverted - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked executed"); + // subTxId should NOT be marked as executed since the tx reverted + assertFalse(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be marked executed"); } function test_NoEventsEmitted_WhenExecutionReverts() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = @@ -191,7 +191,7 @@ contract CEA_NewMulticallTests is CEATest { vm.recordLogs(); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); Vm.Log[] memory logs = vm.getRecordedLogs(); assertEq(logs.length, 0, "No events should be emitted on revert"); @@ -202,7 +202,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_RevertWhen_SelfCallDataLengthLessThan4() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); @@ -213,11 +213,11 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); // Now that _handleSelfCall is removed, malformed calls execute via .call() and fail vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_RevertWhen_SelfCallSelectorNotSendToUEA() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Try to call initializeCEA (wrong selector) @@ -233,14 +233,14 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); // Mismatched selector (3 params vs 4) — no function match, empty return data vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_SelfCallSendToUEA_ERC20_Success() public deployCEA { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; @@ -259,7 +259,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } @@ -268,7 +268,7 @@ contract CEA_NewMulticallTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 100 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; // More than balance @@ -279,13 +279,13 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert(Errors.InsufficientBalance.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_SelfCallSendToUEA_Native_Success() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; @@ -295,7 +295,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); } @@ -303,7 +303,7 @@ contract CEA_NewMulticallTests is CEATest { function test_RevertWhen_SelfCallSendToUEA_InsufficientNativeBalance() public deployCEA { fundCEAWithNative(100 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 500 ether; // More than balance @@ -314,7 +314,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert(Errors.InsufficientBalance.selector); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), payload); } // ========================================================================= @@ -325,7 +325,7 @@ contract CEA_NewMulticallTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](4); @@ -343,7 +343,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(target.magicNumber(), 99, "Final magic number should be 99"); assertEq(mockUniversalGateway.callCount(), 1, "Gateway should be called once"); @@ -354,7 +354,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_SameTxID_CannotExecuteTwice_EvenWithDifferentPayload() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload1 = @@ -365,12 +365,12 @@ contract CEA_NewMulticallTests is CEATest { // First execution succeeds vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload1); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload1); - // Second execution with same txID but different payload should fail + // Second execution with same subTxId but different payload should fail vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload2); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload2); } function test_DifferentTxID_CanExecuteSamePayload() public deployCEA { @@ -385,7 +385,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); ceaInstance.executeUniversalTx(txID1, universalTxID, ueaOnPush, address(0), payload); - // Second execution with different txID but same payload should succeed + // Second execution with different subTxId but same payload should succeed vm.prank(vault); ceaInstance.executeUniversalTx(txID2, universalTxID, ueaOnPush, address(0), payload); @@ -398,7 +398,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_Events_OnePerMulticallStep() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](3); @@ -410,7 +410,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.recordLogs(); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -432,7 +432,7 @@ contract CEA_NewMulticallTests is CEATest { function test_RevertWhen_ReentrantCall() public deployCEA { MaliciousTarget malicious = new MaliciousTarget(address(ceaInstance)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = @@ -442,7 +442,7 @@ contract CEA_NewMulticallTests is CEATest { // The malicious contract will try to reenter but should be blocked // Note: Since the malicious contract doesn't actually attempt reentry in execute(), // this test just verifies the call succeeds and reentrancy guard is in place - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); assertTrue(malicious.attackAttempted(), "Attack should have been attempted"); } @@ -452,7 +452,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_MsgValueLessThanSum_CEAHasPreExistingBalance_Succeeds() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 feePerCall = 0.1 ether; @@ -470,13 +470,13 @@ contract CEA_NewMulticallTests is CEATest { // msg.value (0.1) < sum of call values (0.2), but CEA has pre-existing 0.1 balance vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(target.magicNumber(), 20, "Final magic number should be 20"); } function test_MsgValueExceedsSum_ExcessStaysInCEA() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 feePerCall = 0.1 ether; @@ -493,14 +493,16 @@ contract CEA_NewMulticallTests is CEATest { // Excess ETH stays in CEA (belongs to user's UEA) vm.prank(vault); - ceaInstance.executeUniversalTx{value: totalValue + excess}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: totalValue + excess}( + subTxId, universalTxID, ueaOnPush, address(0), 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 { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Both calls require exactly 0.1 ETH each @@ -517,7 +519,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx{value: totalValue}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: totalValue}(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(target.magicNumber(), 20, "Final magic number should be 20"); assertEq(address(target).balance, totalValue, "Target should have received all ETH"); @@ -528,7 +530,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_RevertWhen_SelfCall_WithNonZeroValue() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Try to send value to self-call (not allowed) @@ -546,7 +548,7 @@ contract CEA_NewMulticallTests is CEATest { vm.deal(vault, 0.1 ether); vm.prank(vault); vm.expectRevert(Errors.InvalidInput.selector); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_RevertWhen_DirectCallToSendUniversalTxToUEA() public deployCEA { @@ -566,7 +568,7 @@ contract CEA_NewMulticallTests is CEATest { // Fund CEA with 0.5 ETH initially fundCEAWithNative(0.5 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Batch: external call first (uses 0.1 ETH), then send more than remaining balance @@ -583,7 +585,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert(Errors.InsufficientBalance.selector); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify rollback - target should not have received ETH assertEq(address(target).balance, 0, "Target balance should be 0 due to rollback"); @@ -596,7 +598,7 @@ contract CEA_NewMulticallTests is CEATest { // Configure gateway to revert mockUniversalGateway.setWillRevert(true, "Gateway intentionally reverted"); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -611,10 +613,10 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("Gateway intentionally reverted"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - // Verify txID not marked executed - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked"); + // Verify subTxId not marked executed + assertFalse(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be marked"); } // ========================================================================= @@ -628,7 +630,7 @@ contract CEA_NewMulticallTests is CEATest { fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -642,7 +644,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify allowance rolled back to 0 assertEq(token.allowance(address(ceaInstance), address(spender)), 0, "Allowance should be 0"); @@ -654,7 +656,7 @@ contract CEA_NewMulticallTests is CEATest { fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](3); @@ -671,7 +673,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify gateway was NOT called (entire tx reverted before gateway interaction persisted) assertEq(mockUniversalGateway.callCount(), 0, "Gateway should not be called due to rollback"); @@ -684,7 +686,7 @@ contract CEA_NewMulticallTests is CEATest { function test_SuccessWhen_RetrySameTxID_AfterFirstRevert() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory failingPayload = @@ -693,25 +695,25 @@ contract CEA_NewMulticallTests is CEATest { // First attempt - should revert vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), failingPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), failingPayload); // Verify not marked executed - assertFalse(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked"); + assertFalse(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be marked"); - // Retry with same txID but different (succeeding) payload + // Retry with same subTxId but different (succeeding) payload bytes memory succeedingPayload = buildExternalSingleCall(address(target), 0, abi.encodeWithSignature("setMagicNumber(uint256)", 42)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), succeedingPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), succeedingPayload); // Verify now marked executed - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); assertEq(target.magicNumber(), 42, "Target should have been updated"); } function test_RevertWhen_ReplaySameTxID_WithDifferentMsgValue() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 value1 = 0.1 ether; @@ -725,12 +727,12 @@ contract CEA_NewMulticallTests is CEATest { // First execution with value1 vm.prank(vault); - ceaInstance.executeUniversalTx{value: value1}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: value1}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Try to replay with different msg.value - should still be blocked vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx{value: value2}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: value2}(subTxId, universalTxID, ueaOnPush, address(0), payload); } // ========================================================================= @@ -738,7 +740,7 @@ contract CEA_NewMulticallTests is CEATest { // ========================================================================= function test_Events_DataMatchesEachCall() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory data1 = abi.encodeWithSignature("setMagicNumber(uint256)", 10); @@ -752,12 +754,12 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.recordLogs(); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); Vm.Log[] memory logs = vm.getRecordedLogs(); // Find UniversalTxExecuted events and validate data - // Event signature: UniversalTxExecuted(bytes32 indexed txID, bytes32 indexed universalTxID, address indexed originCaller, address target, bytes data) + // Event signature: UniversalTxExecuted(bytes32 indexed subTxId, bytes32 indexed universalTxID, address indexed originCaller, address target, bytes data) uint256 eventIndex = 0; for (uint256 i = 0; i < logs.length; i++) { if (logs[i].topics[0] == keccak256("UniversalTxExecuted(bytes32,bytes32,address,address,bytes)")) { @@ -770,7 +772,7 @@ contract CEA_NewMulticallTests is CEATest { (address emittedTo, bytes memory emittedData) = abi.decode(logs[i].data, (address, bytes)); // Validate against expected call - assertEq(emittedTxID, txID, "Event txID should match"); + assertEq(emittedTxID, subTxId, "Event subTxId should match"); assertEq(emittedUniversalTxID, universalTxID, "Event universalTxID should match"); assertEq(emittedOrigin, ueaOnPush, "Event origin should match"); assertEq(emittedTo, calls[eventIndex].to, "Event target should match call"); @@ -787,7 +789,7 @@ contract CEA_NewMulticallTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 sendAmount = 100 ether; @@ -803,12 +805,12 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.recordLogs(); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); Vm.Log[] memory logs = vm.getRecordedLogs(); // Find the self-call event - // Event signature: UniversalTxExecuted(bytes32 indexed txID, bytes32 indexed universalTxID, address indexed originCaller, address target, bytes data) + // Event signature: UniversalTxExecuted(bytes32 indexed subTxId, bytes32 indexed universalTxID, address indexed originCaller, address target, bytes data) bool foundSelfCallEvent = false; for (uint256 i = 0; i < logs.length; i++) { if (logs[i].topics[0] == keccak256("UniversalTxExecuted(bytes32,bytes32,address,address,bytes)")) { @@ -837,7 +839,7 @@ contract CEA_NewMulticallTests is CEATest { function test_RevertWhen_NativeTransfer_LaterRevert_RollbackReceiverBalance() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 transferAmount = 0.1 ether; // Exact fee required by target @@ -856,7 +858,7 @@ contract CEA_NewMulticallTests is CEATest { vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx{value: transferAmount}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: transferAmount}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify target balance unchanged (rollback) assertEq(address(target).balance, targetBalanceBefore, "Target balance should not change due to rollback"); @@ -880,15 +882,15 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); // This now includes MULTICALL_SELECTOR - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify execution succeeded assertEq(testTarget.magicNumber(), 42); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId)); } function test_MulticallSelector_MsgValueExceeds_NoRevert() public deployCEA { @@ -900,12 +902,12 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.deal(vault, 2 ether); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 2 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 2 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); assertEq(testTarget.magicNumber(), 42, "Target should have magic number set"); assertEq(address(ceaInstance).balance, 1.9 ether, "Excess should stay in CEA"); @@ -920,14 +922,14 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); // Includes MULTICALL_SELECTOR - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.deal(vault, 0.1 ether); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId)); assertEq(testTarget.magicNumber(), 999); } @@ -940,37 +942,37 @@ contract CEA_NewMulticallTests is CEATest { // Payload with MULTICALL_SELECTOR but malformed data after it bytes memory invalidPayload = abi.encodePacked(MULTICALL_SELECTOR, bytes("malformed data")); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); vm.expectRevert(); // Should revert during abi.decode - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), invalidPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), invalidPayload); } function test_PayloadLength_LessThan4Bytes_RevertsInvalidRecipient() public deployCEA { // Short non-empty payload hits single-call path; recipient=address(0) reverts bytes memory shortPayload = bytes("abc"); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); vm.expectRevert(Errors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), shortPayload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), shortPayload); } function test_MulticallSelector_EmptyCallsArray_Succeeds() public deployCEA { Multicall[] memory calls = new Multicall[](0); bytes memory payload = encodeCalls(calls); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId)); } function test_MulticallSelector_MixedValueCalls() public deployCEA { @@ -984,14 +986,14 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.deal(vault, 0.2 ether); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0.2 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.2 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId)); assertEq(target1.magicNumber(), 300); // Last call to target1 assertEq(target2.magicNumber(), 200); } @@ -1015,13 +1017,13 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.deal(vault, 0.1 ether); vm.prank(vault); vm.expectRevert(Errors.InvalidInput.selector); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_SelfCall_WithMulticallSelector_ZeroValue_Succeeds() public deployCEA { @@ -1042,13 +1044,13 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID)); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId)); } // ========================================================================= diff --git a/test/tests_cea/CEA_selfCalls.t.sol b/test/tests_cea/CEA_selfCalls.t.sol index 90c594b..b44dd44 100644 --- a/test/tests_cea/CEA_selfCalls.t.sol +++ b/test/tests_cea/CEA_selfCalls.t.sol @@ -21,7 +21,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; @@ -33,7 +33,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify UniversalTxRequest.recipient == UEA UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); @@ -43,7 +43,7 @@ contract CEA_ComprehensiveTests is CEATest { function test_UniversalTxRequest_RecipientIsUEA_Native() public deployCEA { fundCEAWithNative(1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; @@ -53,7 +53,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify UniversalTxRequest.recipient == UEA UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); @@ -64,7 +64,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; @@ -76,7 +76,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify revertRecipient == UEA UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); @@ -87,7 +87,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 500 ether; @@ -99,7 +99,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify revertRecipient == UEA (no revertMsg field anymore) UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); @@ -116,7 +116,7 @@ contract CEA_ComprehensiveTests is CEATest { uint256 totalBalance = 1000 ether; fundCEAWithTokens(address(token), totalBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -131,7 +131,7 @@ contract CEA_ComprehensiveTests is CEATest { assertEq(balanceBefore, totalBalance, "CEA should have full balance before"); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify CEA balance is exactly 0 after sending 100% uint256 balanceAfter = token.balanceOf(address(ceaInstance)); @@ -146,7 +146,7 @@ contract CEA_ComprehensiveTests is CEATest { uint256 totalBalance = 10 ether; fundCEAWithNative(totalBalance); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); @@ -158,7 +158,7 @@ contract CEA_ComprehensiveTests is CEATest { assertEq(balanceBefore, totalBalance, "CEA should have full balance before"); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify CEA balance is exactly 0 after sending 100% uint256 balanceAfter = address(ceaInstance).balance; @@ -177,7 +177,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 minAmount = 1 wei; @@ -190,7 +190,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); assertEq(lastReq.amount, minAmount, "Should handle 1 wei minimum amount"); @@ -199,7 +199,7 @@ contract CEA_ComprehensiveTests is CEATest { function test_SendUniversalTxToUEA_MinimumAmount_1Wei_Native() public deployCEA { fundCEAWithNative(10 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 minAmount = 1 wei; @@ -209,7 +209,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), payload); UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); assertEq(lastReq.amount, minAmount, "Should handle 1 wei minimum amount"); @@ -223,7 +223,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 2000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](4); @@ -242,7 +242,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify both calls went through assertEq(mockUniversalGateway.callCount(), 2, "Gateway should be called twice"); @@ -258,7 +258,7 @@ contract CEA_ComprehensiveTests is CEATest { Target payableTarget = new Target(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](2); @@ -276,7 +276,7 @@ contract CEA_ComprehensiveTests is CEATest { // After second call (send 0.4 to UEA) → CEA has 0 ETH vm.deal(vault, 0.1 ether); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0.1 ether}(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx{value: 0.1 ether}(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify gateway was called with 0.4 ETH UniversalTxRequest memory lastReq = mockUniversalGateway.getLastRequest(); @@ -291,7 +291,7 @@ contract CEA_ComprehensiveTests is CEATest { MockGasToken token = new MockGasToken(); fundCEAWithTokens(address(token), 1000 ether); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); @@ -301,7 +301,7 @@ contract CEA_ComprehensiveTests is CEATest { vm.prank(vault); vm.expectRevert(Errors.InsufficientBalance.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } // ========================================================================= @@ -392,8 +392,7 @@ contract CEA_ComprehensiveTests is CEATest { bytes memory ueaPayload = abi.encodeWithSignature("someFunction()"); Multicall[] memory calls = new Multicall[](1); - calls[0] = - makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(fakeToken, 0, ueaPayload, ueaOnPush)); + calls[0] = makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(fakeToken, 0, ueaPayload, ueaOnPush)); vm.prank(vault); ceaInstance.executeUniversalTx( @@ -788,16 +787,17 @@ contract CEA_ComprehensiveTests is CEATest { address(ceaInstance), 0, buildSendToUEAPayloadWithData(address(0), 5 ether, ueaPayload, ueaOnPush) ); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); vm.prank(vault); vm.expectRevert("GatewayError"); ceaInstance.executeUniversalTx{value: 0}( - txID, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) + subTxId, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); assertFalse( - CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should NOT be marked executed on gateway revert" + CEA(payable(address(ceaInstance))).isExecuted(subTxId), + "subTxId should NOT be marked executed on gateway revert" ); } @@ -841,11 +841,11 @@ contract CEA_ComprehensiveTests is CEATest { Multicall[] memory calls = new Multicall[](1); calls[0] = buildSelfSendToUEACall(address(0), amount); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx{value: 0}(txID, universalTxID, ueaOnPush, address(0), encodeCalls(calls)); + ceaInstance.executeUniversalTx{value: 0}(subTxId, universalTxID, ueaOnPush, address(0), encodeCalls(calls)); // Assertions assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS-only should use sendUniversalTxFromCEA"); @@ -857,7 +857,7 @@ contract CEA_ComprehensiveTests is CEATest { assertEq(req.amount, amount, "amount correct"); assertEq(req.payload.length, 0, "payload empty"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId marked executed"); assertEq(address(ceaInstance).balance, 5 ether, "CEA balance decreased"); } @@ -874,10 +874,10 @@ contract CEA_ComprehensiveTests is CEATest { ); calls[1] = buildSelfSendToUEACall(address(token), amount); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls)); + ceaInstance.executeUniversalTx(subTxId, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls)); assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS-only should use sendUniversalTxFromCEA"); assertEq(mockUniversalGateway.lastValue(), 0, "msg.value should be 0 for ERC20"); @@ -897,11 +897,11 @@ contract CEA_ComprehensiveTests is CEATest { calls[0] = makeCall(address(ceaInstance), 0, buildSendToUEAPayloadWithData(address(0), amount, ueaPayload, ueaOnPush)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); vm.prank(vault); ceaInstance.executeUniversalTx{value: 0}( - txID, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) + subTxId, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls) ); assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS_AND_PAYLOAD should use sendUniversalTxFromCEA"); @@ -912,7 +912,7 @@ contract CEA_ComprehensiveTests is CEATest { assertEq(req.amount, amount, "amount correct"); assertEq(req.payload, ueaPayload, "payload matches"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId marked executed"); assertEq(address(ceaInstance).balance, 5 ether, "CEA balance decreased"); } @@ -932,10 +932,10 @@ contract CEA_ComprehensiveTests is CEATest { address(ceaInstance), 0, buildSendToUEAPayloadWithData(address(token), amount, ueaPayload, ueaOnPush) ); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls)); + ceaInstance.executeUniversalTx(subTxId, generateUniversalTxID(1), ueaOnPush, address(0), encodeCalls(calls)); assertTrue(mockUniversalGateway.lastCallWasViaCEA(), "FUNDS_AND_PAYLOAD should use sendUniversalTxFromCEA"); assertEq(mockUniversalGateway.lastValue(), 0, "msg.value should be 0 for ERC20"); diff --git a/test/tests_cea/CEA_singleCall.t.sol b/test/tests_cea/CEA_singleCall.t.sol index cd60053..922c178 100644 --- a/test/tests_cea/CEA_singleCall.t.sol +++ b/test/tests_cea/CEA_singleCall.t.sol @@ -14,17 +14,17 @@ contract CEA_SingleCallTests is CEATest { // ========================================================================= function test_ParkFunds_EmptyPayload_NativeViaValue() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 1 ether; vm.deal(vault, amount); vm.prank(vault); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, address(0), ""); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, address(0), ""); assertEq(address(ceaInstance).balance, amount, "CEA should hold parked native funds"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_ParkFunds_EmptyPayload_ERC20PreFunded() public deployCEA { @@ -32,28 +32,28 @@ contract CEA_SingleCallTests is CEATest { uint256 amount = 500 ether; fundCEAWithTokens(address(token), amount); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), ""); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), ""); assertEq(token.balanceOf(address(ceaInstance)), amount, "CEA should hold ERC20 tokens"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_ParkFunds_EmptyPayload_ZeroValue() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), ""); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), ""); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_EmptyPayload_NonZeroRecipient_ForwardsNative() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 1 ether; @@ -62,25 +62,25 @@ contract CEA_SingleCallTests is CEATest { address someRecipient = makeAddr("someRecipient"); vm.prank(vault); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, someRecipient, ""); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, someRecipient, ""); // Empty payload + non-zero recipient is a plain native send to recipient, not fund-parking. // Fund-parking requires BOTH empty payload AND address(0) recipient. assertEq(address(someRecipient).balance, amount, "Recipient should receive the native funds"); assertEq(address(ceaInstance).balance, 0, "CEA should not hold funds when recipient is non-zero"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_ParkFunds_EmitsEvent_TargetIsSelf() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); vm.prank(vault); vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(ceaInstance), ""); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(ceaInstance), ""); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), ""); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), ""); } // ========================================================================= @@ -88,20 +88,20 @@ contract CEA_SingleCallTests is CEATest { // ========================================================================= function test_SingleCall_ExecuteTargetFunction() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); assertEq(target.getMagicNumber(), 42, "Target should have magic number set"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_SingleCall_ForwardsMsgValueToRecipient() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); uint256 amount = 0.1 ether; @@ -109,26 +109,26 @@ contract CEA_SingleCallTests is CEATest { vm.deal(vault, amount); vm.prank(vault); - ceaInstance.executeUniversalTx{value: amount}(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx{value: amount}(subTxId, universalTxID, ueaOnPush, address(target), payload); assertEq(target.getMagicNumber(), 42, "Target should have magic number set"); assertEq(address(target).balance, amount, "Target should receive native value"); } function test_SingleCall_ZeroValue_ValidRecipient() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 99); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); assertEq(target.getMagicNumber(), 99, "Target should have magic number set"); } function test_SingleCall_EmitsEvent_CorrectTargetAndPayload() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 77); @@ -136,9 +136,9 @@ contract CEA_SingleCallTests is CEATest { vm.prank(vault); vm.expectEmit(true, true, true, true); - emit ICEA.UniversalTxExecuted(txID, universalTxID, ueaOnPush, address(target), payload); + emit ICEA.UniversalTxExecuted(subTxId, universalTxID, ueaOnPush, address(target), payload); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); } // ========================================================================= @@ -146,78 +146,78 @@ contract CEA_SingleCallTests is CEATest { // ========================================================================= function test_SingleCall_RevertWhen_RecipientIsZero() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); vm.expectRevert(Errors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_SingleCall_RevertWhen_RecipientIsSelf() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); vm.expectRevert(Errors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } function test_SingleCall_RevertWhen_TargetReverts() public deployCEA { RevertingTarget reverter = new RevertingTarget(); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("revertWithReason()"); vm.prank(vault); vm.expectRevert("This function always reverts with reason"); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(reverter), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(reverter), payload); assertFalse( - CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should not be marked executed on failure" + CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should not be marked executed on failure" ); } function test_SingleCall_RevertWhen_NotVault() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(nonVault); vm.expectRevert(Errors.NotVault.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); } function test_SingleCall_RevertWhen_WrongOriginCaller() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); vm.expectRevert(Errors.InvalidUEA.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, makeAddr("wrongUEA"), address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, makeAddr("wrongUEA"), address(target), payload); } function test_SingleCall_RevertWhen_DuplicateTxId() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodeWithSignature("setMagicNumber(uint256)", 42); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(target), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(target), payload); } // ========================================================================= @@ -225,7 +225,7 @@ contract CEA_SingleCallTests is CEATest { // ========================================================================= function test_MulticallPayload_IgnoresRecipient() public deployCEA { - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); Multicall[] memory calls = new Multicall[](1); @@ -235,10 +235,10 @@ contract CEA_SingleCallTests is CEATest { address randomRecipient = makeAddr("randomRecipient"); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, randomRecipient, payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, randomRecipient, payload); assertEq(target.getMagicNumber(), 55, "Multicall should execute normally regardless of recipient"); - assertTrue(CEA(payable(address(ceaInstance))).isExecuted(txID), "txID should be marked executed"); + assertTrue(CEA(payable(address(ceaInstance))).isExecuted(subTxId), "subTxId should be marked executed"); } function test_MigrationPayload_RevertsWhenRecipientNotSelf() public deployCEA { @@ -247,7 +247,7 @@ contract CEA_SingleCallTests is CEATest { CEAMigration migration = new CEAMigration(address(ceaV2)); factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); @@ -256,7 +256,7 @@ contract CEA_SingleCallTests is CEATest { // Migration with non-self recipient should revert vm.prank(vault); vm.expectRevert(Errors.InvalidRecipient.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, randomRecipient, payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, randomRecipient, payload); } function test_MigrationPayload_SucceedsWhenRecipientIsSelf() public deployCEA { @@ -265,14 +265,14 @@ contract CEA_SingleCallTests is CEATest { CEAMigration migration = new CEAMigration(address(ceaV2)); factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); // Migration with self recipient should succeed vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); assertEq( CEAProxy(payable(address(ceaInstance))).getImplementation(), diff --git a/test/tests_ceaMigration/CEAMigration_Integration.t.sol b/test/tests_ceaMigration/CEAMigration_Integration.t.sol index 8cd0811..c20cdf2 100644 --- a/test/tests_ceaMigration/CEAMigration_Integration.t.sol +++ b/test/tests_ceaMigration/CEAMigration_Integration.t.sol @@ -80,7 +80,7 @@ contract CEAMigration_IntegrationTest is Test { // ========================================================================= function generateTxID(uint256 nonce) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("txID", nonce)); + return keccak256(abi.encodePacked("subTxId", nonce)); } function generateUniversalTxID(uint256 nonce) internal pure returns (bytes32) { @@ -92,12 +92,12 @@ contract CEAMigration_IntegrationTest is Test { } function executeMigration() internal { - bytes32 txID = generateTxID(999); + bytes32 subTxId = generateTxID(999); bytes32 universalTxID = generateUniversalTxID(999); bytes memory payload = buildMigrationPayload(address(ceaInstance)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -249,7 +249,7 @@ contract CEAMigration_IntegrationTest is Test { executeMigration(); // Execute a new transaction after migration - bytes32 txID = generateTxID(100); + bytes32 subTxId = generateTxID(100); bytes32 universalTxID = generateUniversalTxID(100); Multicall[] memory calls = new Multicall[](1); @@ -257,10 +257,10 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify executed successfully - assertTrue(ceaInstance.isExecuted(txID), "Post-migration execution should work"); + assertTrue(ceaInstance.isExecuted(subTxId), "Post-migration execution should work"); } function test_PostMigration_Multicall() public { @@ -268,7 +268,7 @@ contract CEAMigration_IntegrationTest is Test { executeMigration(); // Execute a multicall after migration - bytes32 txID = generateTxID(101); + bytes32 subTxId = generateTxID(101); bytes32 universalTxID = generateUniversalTxID(101); Multicall[] memory calls = new Multicall[](3); @@ -278,10 +278,10 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); // Verify executed successfully - assertTrue(ceaInstance.isExecuted(txID), "Post-migration multicall should work"); + assertTrue(ceaInstance.isExecuted(subTxId), "Post-migration multicall should work"); } // ========================================================================= @@ -289,18 +289,18 @@ contract CEAMigration_IntegrationTest is Test { // ========================================================================= function test_Migration_ReplayProtection() public { - bytes32 txID = generateTxID(999); + bytes32 subTxId = generateTxID(999); bytes32 universalTxID = generateUniversalTxID(999); bytes memory payload = buildMigrationPayload(address(ceaInstance)); // Execute migration vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); // Attempt to replay same migration vm.prank(vault); vm.expectRevert(Errors.PayloadExecuted.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -308,7 +308,7 @@ contract CEAMigration_IntegrationTest is Test { // ========================================================================= function test_Migration_NotVault() public { - bytes32 txID = generateTxID(999); + bytes32 subTxId = generateTxID(999); bytes32 universalTxID = generateUniversalTxID(999); bytes memory payload = buildMigrationPayload(address(ceaInstance)); @@ -316,11 +316,11 @@ contract CEAMigration_IntegrationTest is Test { vm.prank(nonVault); vm.expectRevert(Errors.NotVault.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_Migration_WrongOriginCaller() public { - bytes32 txID = generateTxID(999); + bytes32 subTxId = generateTxID(999); bytes32 universalTxID = generateUniversalTxID(999); bytes memory payload = buildMigrationPayload(address(ceaInstance)); @@ -328,7 +328,7 @@ contract CEAMigration_IntegrationTest is Test { vm.prank(vault); vm.expectRevert(Errors.InvalidUEA.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, wrongUEA, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, wrongUEA, address(0), payload); } // ========================================================================= @@ -350,12 +350,12 @@ contract CEAMigration_IntegrationTest is Test { factory.setCEAMigrationContract(address(migration2)); // Migration 2: v2 → v3 - bytes32 txID = generateTxID(1000); + bytes32 subTxId = generateTxID(1000); bytes32 universalTxID = generateUniversalTxID(1000); bytes memory payload = buildMigrationPayload(address(ceaInstance)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); assertEq( CEAProxy(payable(address(ceaInstance))).getImplementation(), @@ -375,7 +375,7 @@ contract CEAMigration_IntegrationTest is Test { function test_MigrationAfterManyExecutions() public { // Execute many transactions before migration for (uint256 i = 1; i <= 100; i++) { - bytes32 txID = generateTxID(i); + bytes32 subTxId = generateTxID(i); bytes32 universalTxID = generateUniversalTxID(i); Multicall[] memory calls = new Multicall[](1); @@ -383,7 +383,7 @@ contract CEAMigration_IntegrationTest is Test { bytes memory payload = abi.encodePacked(MULTICALL_SELECTOR, abi.encode(calls)); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } // Verify all executed @@ -409,12 +409,12 @@ contract CEAMigration_IntegrationTest is Test { CEA freshCEAInstance = CEA(payable(freshCEA)); // Execute migration immediately - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildMigrationPayload(freshCEA); vm.prank(vault); - freshCEAInstance.executeUniversalTx(txID, universalTxID, freshUEA, address(freshCEAInstance), payload); + freshCEAInstance.executeUniversalTx(subTxId, universalTxID, freshUEA, address(freshCEAInstance), payload); // Verify migration successful assertEq( diff --git a/test/tests_ceaMigration/CEA_Migration.t.sol b/test/tests_ceaMigration/CEA_Migration.t.sol index 4b11568..b89c950 100644 --- a/test/tests_ceaMigration/CEA_Migration.t.sol +++ b/test/tests_ceaMigration/CEA_Migration.t.sol @@ -69,7 +69,7 @@ contract CEA_MigrationTest is Test { // ========================================================================= function generateTxID(uint256 nonce) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("txID", nonce)); + return keccak256(abi.encodePacked("subTxId", nonce)); } function generateUniversalTxID(uint256 nonce) internal pure returns (bytes32) { @@ -108,7 +108,7 @@ contract CEA_MigrationTest is Test { // Set migration contract in factory factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Build migration payload @@ -116,7 +116,7 @@ contract CEA_MigrationTest is Test { // Execute migration (will test isMigration detection internally) vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); // If execution reaches here without reverting, isMigration worked assertTrue(true, "Migration selector detected successfully"); @@ -130,14 +130,14 @@ contract CEA_MigrationTest is Test { // Set migration contract factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Top-level MIGRATION_SELECTOR (no Multicall wrapper) bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); // Verify implementation changed address implAfter = CEAProxy(payable(address(ceaInstance))).getImplementation(); @@ -147,7 +147,7 @@ contract CEA_MigrationTest is Test { function test_handleMigration_NonZeroMsgValue_Reverts() public { factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = abi.encodePacked(MIGRATION_SELECTOR); @@ -155,14 +155,14 @@ contract CEA_MigrationTest is Test { vm.prank(vault); vm.expectRevert(Errors.InvalidInput.selector); - ceaInstance.executeUniversalTx{value: 1 ether}(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx{value: 1 ether}(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } function test_handleMigration_MigrationInsideMulticall_Reverts() public { // Set migration contract factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // MIGRATION_SELECTOR wrapped in multicall fails as generic execution failure @@ -173,13 +173,13 @@ contract CEA_MigrationTest is Test { vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_handleMigration_NoMigrationContract() public { // Do NOT set migration contract (remains address(0)) - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Build migration payload @@ -188,7 +188,7 @@ contract CEA_MigrationTest is Test { // Expect InvalidCall revert vm.prank(vault); vm.expectRevert(Errors.InvalidCall.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } // ========================================================================= @@ -199,7 +199,7 @@ contract CEA_MigrationTest is Test { // Set migration contract factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Build batched payload with migration @@ -215,14 +215,14 @@ contract CEA_MigrationTest is Test { // Migration selector in multicall fails as generic execution failure vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } function test_handleMulticall_MigrationInBatch_FirstPosition() public { // Set migration contract factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Build batched payload with migration in first position @@ -238,7 +238,7 @@ contract CEA_MigrationTest is Test { // Migration selector in multicall fails as generic execution failure vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(0), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } // ========================================================================= @@ -249,7 +249,7 @@ contract CEA_MigrationTest is Test { // Set migration contract factory.setCEAMigrationContract(address(migration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); // Build standalone migration payload @@ -260,7 +260,7 @@ contract CEA_MigrationTest is Test { // Execute migration vm.prank(vault); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); // Get updated implementation address implAfter = CEAProxy(payable(address(ceaInstance))).getImplementation(); @@ -277,13 +277,13 @@ contract CEA_MigrationTest is Test { FailingMigration failMigration = new FailingMigration(); factory.setCEAMigrationContract(address(failMigration)); - bytes32 txID = generateTxID(1); + bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); bytes memory payload = buildMigrationPayload(address(ceaInstance)); vm.prank(vault); vm.expectRevert(Errors.ExecutionFailed.selector); - ceaInstance.executeUniversalTx(txID, universalTxID, ueaOnPush, address(ceaInstance), payload); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(ceaInstance), payload); } } From 0f8e49da83ccecc0fcc36cbdc388f5379cc7c2f2 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 18:46:13 +0530 Subject: [PATCH 40/56] F-2026-15549: sync UniversalCore.md with current implementation Align documentation with deployed contract signatures, events, and role ownership. Docs-only change; no contract behaviour modified. - setChainMeta: 3-arg (observedAt derived from block.timestamp) - swapAndBurnGas: 5-arg signature and event; clarify protocol fee is routed by UniversalGatewayPC to VaultPC, not through swapAndBurnGas - pause/unpause: PAUSER_ROLE only, not admin - setWPCContractAddress -> setWPC - refundUnusedGas: full 6-arg signature - setUniswapV3Addresses: 2-arg (quoter removed) - Remove setSupportedToken and setSlippageTolerance (removed from impl) - Add setMaxStalenessByChain, setUniversalGatewayPC, setPauserRole - Fix getRescueFundsGasLimit: does not return protocolFee - Correct src/Interfaces/ path casing --- docs/UniversalCore.md | 49 +++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/UniversalCore.md b/docs/UniversalCore.md index d291181..5d4268d 100644 --- a/docs/UniversalCore.md +++ b/docs/UniversalCore.md @@ -3,7 +3,7 @@ ## Contract Locations - **UniversalCore**: [`src/UniversalCore.sol`](../src/UniversalCore.sol) -- **IUniversalCore Interface**: [`src/interfaces/IUniversalCore.sol`](../src/interfaces/IUniversalCore.sol) +- **IUniversalCore Interface**: [`src/Interfaces/IUniversalCore.sol`](../src/Interfaces/IUniversalCore.sol) - **PRC20**: [`src/PRC20.sol`](../src/PRC20.sol) - **WPC (Wrapped PC)**: [`src/WPC.sol`](../src/WPC.sol) @@ -28,7 +28,7 @@ UniversalCore maintains an on-chain oracle of external chain state. For each sup | `timestampObservedAtByChainNamespace` | Timestamp when the observation was recorded | | `gasTokenPRC20ByChainNamespace` | PRC-20 address of the chain's native gas token (e.g. pETH for Ethereum) | -The Universal Executor Module periodically calls `setChainMeta(chainNamespace, price, chainHeight, observedAt)` to push fresh external chain data on-chain, updating both `gasPriceByChainNamespace` and `chainHeightByChainNamespace` in a single call. This makes UniversalCore the single source of truth for external chain gas pricing and block height within Push Chain's contract layer. +The Universal Executor Module periodically calls `setChainMeta(chainNamespace, price, chainHeight)` to push fresh external chain data on-chain, updating `gasPriceByChainNamespace` and `chainHeightByChainNamespace` in a single call. The observation timestamp is derived internally from `block.timestamp` and stored in `timestampObservedAtByChainNamespace`. This makes UniversalCore the single source of truth for external chain gas pricing and block height within Push Chain's contract layer. This oracle data drives fee computation: when a user initiates an outbound transaction, the gateway queries `getOutboundTxGasAndFees(prc20, gasLimit)` which reads the stored gas price and multiplies it by the gas limit to produce the fee denominated in the destination chain's gas token. @@ -60,8 +60,8 @@ The UE Module is a protocol-level system address that executes on behalf of the |---|---| | `depositPRC20Token(prc20, amount, recipient)` | Mint PRC-20 tokens to a recipient address on inbound | | `depositPRC20WithAutoSwap(prc20, amount, recipient, fee, minPCOut, deadline)` | Mint PRC-20 and swap to native PC in one step | -| `setChainMeta(chainNamespace, price, chainHeight, observedAt)` | Update gas price and block height oracle data for an external chain | -| `refundUnusedGas(recipient, amount)` | Refund unused gas to a recipient after execution | +| `setChainMeta(chainNamespace, price, chainHeight)` | Update gas price and block height oracle data for an external chain (observation timestamp is set to `block.timestamp` internally) | +| `refundUnusedGas(gasToken, amount, recipient, withSwap, fee, minPCOut)` | Refund unused gas: either mint `gasToken` PRC-20 directly to `recipient`, or swap back to native PC via Uniswap V3 when `withSwap = true` | ### Manager Role (`MANAGER_ROLE`) @@ -71,26 +71,26 @@ Managers handle operational configuration that changes with external chain condi | Function | Purpose | |---|---| -| `setGasTokenPRC20(chainNamespace, prc20)` | Map a chain namespace to its gas token PRC-20 | +| `setGasTokenPRC20(chainNamespace, prc20)` | Map a chain namespace to its gas token PRC-20 (resets `gasPriceByChainNamespace` to `0` to force explicit reconfiguration) | | `setGasPCPool(chainNamespace, gasToken, fee)` | Register a Uniswap V3 pool for PC/gas-token swaps | -| `setSupportedToken(prc20, supported)` | Mark a PRC-20 token as officially supported | | `setBaseGasLimitByChain(chainNamespace, gasLimit)` | Set the minimum base gas limit for TSS execution on a chain | | `setRescueFundsGasLimitByChain(chainNamespace, gasLimit)` | Set the fixed gas limit for rescue operations on a chain | +| `setMaxStalenessByChain(chainNamespace, maxStaleness)` | Set the maximum acceptable age (seconds) of stored gas data before fee quotes revert as stale (`0` disables the check, opt-in) | | `setProtocolFeeByToken(prc20, fee)` | Set the protocol fee (in native PC) for a PRC-20 token | ### Admin Role (`DEFAULT_ADMIN_ROLE`) -Granted to the deployer at initialization. Controls contract-level configuration and emergency operations. +Granted to the deployer at initialization. Controls contract-level configuration (Uniswap addresses, fee tier, WPC, gateway address) and role administration. Note: pause/unpause authority is **not** held by the admin — it is restricted to `PAUSER_ROLE`. | Function | Purpose | |---|---| | `setAutoSwapSupported(token, supported)` | Enable/disable auto-swap for a PRC-20 token | -| `setWPCContractAddress(addr)` | Update the Wrapped PC token address | -| `setUniswapV3Addresses(factory, swapRouter, quoter)` | Update Uniswap V3 infrastructure addresses | -| `setDefaultFeeTier(token, feeTier)` | Set default Uniswap V3 fee tier for a token | -| `setSlippageTolerance(token, tolerance)` | Set slippage tolerance in basis points | +| `setWPC(addr)` | Update the Wrapped PC token address | +| `setUniswapV3Addresses(factory, swapRouter)` | Update Uniswap V3 infrastructure addresses | +| `setDefaultFeeTier(token, feeTier)` | Set default Uniswap V3 fee tier for a token (allowed tiers: 100, 500, 3000, 10000) | | `setDefaultDeadlineMins(minutesValue)` | Set default swap deadline | -| `pause()` / `unpause()` | Emergency pause/unpause all deposit operations | +| `setUniversalGatewayPC(addr)` | Update the address authorized to call `swapAndBurnGas` | +| `setPauserRole(addr)` | Grant `PAUSER_ROLE` to an address (guardian) | ### Gateway (`universalGatewayPC`) @@ -98,7 +98,7 @@ Not an OZ `AccessControl` role. `universalGatewayPC` is a mutable address stored | Function | Purpose | |---|---| -| `swapAndBurnGas(gasToken, vault, fee, gasFee, protocolFee, deadline, caller)` | Swap PC for gas token, burn gas fee, send protocol fee to vault | +| `swapAndBurnGas(gasToken, fee, gasFee, deadline, caller)` | Swap PC for `gasToken`, burn the `gasFee` amount, refund unused PC to `caller` | ### Pauser Role (`PAUSER_ROLE`) @@ -130,6 +130,8 @@ getOutboundTxGasAndFees(prc20, gasLimitWithBaseLimit) |--> look up chainNamespace from prc20.SOURCE_CHAIN_NAMESPACE() |--> look up gasToken from gasTokenPRC20ByChainNamespace[chainNamespace] |--> look up gasPrice from gasPriceByChainNamespace[chainNamespace] + |--> if maxStalenessByChainNamespace[chainNamespace] > 0, enforce freshness of gas data + | (reverts with StaleGasData if block.timestamp > observedAt + maxStaleness) | |--> gasFee = gasPrice * gasLimitWithBaseLimit (denominated in gas token units) |--> protocolFee = protocolFeeByToken[prc20] (flat fee in native PC) @@ -148,7 +150,10 @@ getRescueFundsGasLimit(prc20) | |--> look up chainNamespace from prc20.SOURCE_CHAIN_NAMESPACE() |--> look up rescueGasLimit from rescueFundsGasLimitByChainNamespace[chainNamespace] - |--> look up gasToken, gasPrice, protocolFee (same as outbound) + |--> look up gasToken and gasPrice (protocol fee is NOT applied on the rescue path) + |--> if maxStalenessByChainNamespace[chainNamespace] > 0, enforce freshness of gas data + | + |--> gasFee = gasPrice * rescueGasLimit | '--> returns (gasToken, gasFee, rescueGasLimit, gasPrice, chainNamespace) ``` @@ -157,7 +162,7 @@ getRescueFundsGasLimit(prc20) ## Swap-and-Burn: Gas Fee vs Protocol Fee -Outbound transactions require fee settlement. The user pays in native PC, which gets swapped to the destination chain's gas token PRC-20 via Uniswap V3. The resulting gas tokens are then split into two portions with different destinations: +Outbound transactions require fee settlement. The user pays in native PC. `UniversalGatewayPC` sends the `protocolFee` portion directly to `VaultPC` in native PC, and forwards the remaining PC (intended to cover the gas fee) to `UniversalCore.swapAndBurnGas`, which swaps it into the destination chain's gas token PRC-20 via Uniswap V3 and burns it. The two fee components therefore settle through different paths: ### Gas Fee (burned) @@ -178,8 +183,8 @@ User pays native PC | v UniversalGatewayPC - | - | calls swapAndBurnGas{value: pcAmount}(gasToken, vault, fee, gasFee, protocolFee, deadline, caller) + | (pays protocolFee to VaultPC directly in native PC, then:) + | calls swapAndBurnGas{value: pcAmount}(gasToken, fee, gasFee, deadline, caller) v UniversalCore | @@ -187,16 +192,20 @@ UniversalCore |--> 2. Approve Uniswap V3 router to spend WPC |--> 3. Swap WPC -> gasToken via exactOutputSingle | (swap exactly gasFee worth of gas token) + |--> 4. Clear router allowance (forceApprove 0) | - |--> 4. BURN gasFee portion: IPRC20(gasToken).burn(gasFee) + |--> 5. BURN gasFee portion: IPRC20(gasToken).burn(gasFee) | - |--> 5. REFUND unused PC: unwrap leftover WPC, send native PC back to caller + |--> 6. REFUND unused PC: unwrap leftover WPC, send native PC back to caller | - '--> emit SwapAndBurnGas(gasToken, vault, pcUsed, gasFee, protocolFee, fee, caller) + '--> emit SwapAndBurnGas(gasToken, pcIn, gasFee, fee, caller) + returns (gasTokenOut, refund) ``` The swap uses `exactOutputSingle` — the caller specifies exactly how much gas token output is needed (`gasFee`), and any unused PC input is refunded directly to the caller address. This ensures users never overpay. +Note: `swapAndBurnGas` does not receive or route the protocol fee. `UniversalGatewayPC` pays the `protocolFee` (in native PC) directly to `VaultPC` before invoking `swapAndBurnGas`; only the `gasFee` burn and PC refund happen inside this function. The event therefore emits `(gasToken, pcIn, gasFee, fee, caller)` and does not include vault or protocol-fee fields. + ### Why burn vs transfer? - **Burn (gas fee)**: The gas fee represents real execution cost on the destination chain. Burning the equivalent PRC-20 on Push Chain keeps the wrapped token supply in sync with actual external-chain liabilities. The protocol (via validators/TSS) covers the real gas on the destination side. From 5ff49eda9d1294651ac3e185d0aa4d93a8269131 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 19:02:48 +0530 Subject: [PATCH 41/56] F-2026-15556: check IPRC20 deposit and burn return values Add explicit return-value checks on all IPRC20.deposit() and IPRC20.burn() call sites in UniversalCore. Reverts with PRC20OperationFailed if the token returns false instead of reverting. --- src/UniversalCore.sol | 8 ++--- src/libraries/Errors.sol | 1 + test/mocks/FalseReturningPRC20.sol | 25 ++++++++++++++++ test/tests_token_and_core/UniversalCore.t.sol | 21 +++++++++++++ .../UniversalCoreSwapFee.t.sol | 30 +++++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 test/mocks/FalseReturningPRC20.sol diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index dd88900..812ce8a 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -150,7 +150,7 @@ contract UniversalCore is /// @inheritdoc IUniversalCore function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule whenNotPaused nonReentrant { _validateParams(prc20, amount, recipient); - IPRC20(prc20).deposit(recipient, amount); + if (!IPRC20(prc20).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } /// @inheritdoc IUniversalCore @@ -183,7 +183,7 @@ contract UniversalCore is uint256 pcOut; if (!withSwap) { - IPRC20(gasToken).deposit(recipient, amount); + if (!IPRC20(gasToken).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } else { if (minPCOut == 0) { revert UniversalCoreErrors.MinPCOutRequired(); @@ -244,7 +244,7 @@ contract UniversalCore is uint256 amountInUsed = ISwapRouter(uniswapV3SwapRouter).exactOutputSingle(params); IERC20(WPC).forceApprove(uniswapV3SwapRouter, 0); - IPRC20(gasToken).burn(gasFee); + if (!IPRC20(gasToken).burn(gasFee)) revert UniversalCoreErrors.PRC20OperationFailed(); gasTokenOut = gasFee; refund = msg.value - amountInUsed; @@ -555,7 +555,7 @@ contract UniversalCore is if (minPCOut == 0) revert CommonErrors.ZeroAmount(); - IPRC20(prc20).deposit(address(this), amount); + if (!IPRC20(prc20).deposit(address(this), amount)) revert UniversalCoreErrors.PRC20OperationFailed(); IERC20(prc20).forceApprove(uniswapV3SwapRouter, amount); ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index ce06e6c..a397c04 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -50,6 +50,7 @@ library UniversalCoreErrors { error ZeroBaseGasLimit(); error ZeroRescueGasLimit(); error StaleGasData(uint256 observedAt, uint256 nowTimestamp, uint256 maxAge); + error PRC20OperationFailed(); } // ========================= diff --git a/test/mocks/FalseReturningPRC20.sol b/test/mocks/FalseReturningPRC20.sol new file mode 100644 index 0000000..cc7df3e --- /dev/null +++ b/test/mocks/FalseReturningPRC20.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @dev PRC20 mock that returns false instead of reverting on deposit/burn. +contract FalseReturningPRC20 { + string public SOURCE_CHAIN_NAMESPACE; + string public SOURCE_TOKEN_ADDRESS; + + constructor(string memory ns, string memory tokenAddr) { + SOURCE_CHAIN_NAMESPACE = ns; + SOURCE_TOKEN_ADDRESS = tokenAddr; + } + + function deposit(address, uint256) external pure returns (bool) { + return false; + } + + function burn(uint256) external pure returns (bool) { + return false; + } + + function approve(address, uint256) external pure returns (bool) { + return false; + } +} diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index e93b8d9..21f88c3 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -14,6 +14,7 @@ import "../../test/mocks/MockWPC.sol"; import "../../test/mocks/MockPRC20.sol"; import "../../test/mocks/MaliciousPRC20.sol"; import "../../test/mocks/RevertingPRC20.sol"; +import "../../test/mocks/FalseReturningPRC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -1453,4 +1454,24 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); universalCore.getOutboundTxGasAndFees(address(freshPRC20), 0); } + + // ========================= + // PRC20 Return Value Check Tests + // ========================= + + function test_DepositPRC20Token_FalseReturn_Reverts() public { + FalseReturningPRC20 falseToken = new FalseReturningPRC20(CHAIN_NAMESPACE, SOURCE_TOKEN_ADDRESS); + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + vm.expectRevert(UniversalCoreErrors.PRC20OperationFailed.selector); + universalCore.depositPRC20Token(address(falseToken), 1000, makeAddr("target")); + } + + function test_RefundUnusedGas_FalseDeposit_Reverts() public { + FalseReturningPRC20 falseToken = new FalseReturningPRC20(CHAIN_NAMESPACE, SOURCE_TOKEN_ADDRESS); + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + vm.expectRevert(UniversalCoreErrors.PRC20OperationFailed.selector); + universalCore.refundUnusedGas(address(falseToken), 1000, makeAddr("target"), false, 0, 0); + } } diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index 9b9a193..10fa830 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -12,6 +12,7 @@ import "../../test/mocks/MockUniswapV3Factory.sol"; import "../../test/mocks/MockUniswapV3Router.sol"; import "../../test/mocks/MockWPC.sol"; import "../../test/mocks/MockPRC20.sol"; +import "../../test/mocks/FalseReturningPRC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; @@ -415,4 +416,33 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { vm.expectRevert(CommonErrors.ZeroAddress.selector); universalCore.setProtocolFeeByToken(address(0), 1000); } + + // ======================================== + // PRC20 Return Value Check Tests + // ======================================== + + function test_SwapAndBurnGas_FalseBurnReturn_Reverts() public { + FalseReturningPRC20 falseGasToken = + new FalseReturningPRC20(CHAIN_NAMESPACE, SOURCE_TOKEN_ADDRESS); + + vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(falseGasToken)); + universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); + vm.stopPrank(); + + universalCore.setDefaultFeeTier(address(falseGasToken), FEE_TIER); + + address pool = makeAddr("falsePool"); + if (address(mockWPC) < address(falseGasToken)) { + mockFactory.setPool(address(mockWPC), address(falseGasToken), FEE_TIER, pool); + } else { + mockFactory.setPool(address(falseGasToken), address(mockWPC), FEE_TIER, pool); + } + + vm.prank(gateway); + vm.expectRevert(UniversalCoreErrors.PRC20OperationFailed.selector); + universalCore.swapAndBurnGas{value: 1 ether}( + address(falseGasToken), FEE_TIER, GAS_FEE, 0, user + ); + } } From b41f174b3270ea0d34608ce71ea71d167dae196f Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 21 Apr 2026 19:17:02 +0530 Subject: [PATCH 42/56] F-2026-15528 - added rescueNativePC --- src/Interfaces/IUniversalCore.sol | 5 ++ src/UniversalCore.sol | 12 +++++ test/tests_token_and_core/UniversalCore.t.sol | 54 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 84275a8..fb4580d 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -35,6 +35,11 @@ interface IUniversalCore { /// @param pauser Address that was granted the pauser role event PauserRoleGranted(address indexed pauser); + /// @notice Emitted when stuck native PC is rescued by admin. + /// @param to Recipient of the rescued PC + /// @param amount Amount of native PC rescued + event RescueNativePC(address indexed to, uint256 amount); + // ========================= // UC_1: UE MODULE FUNCTIONS // ========================= diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 812ce8a..9a59af4 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -489,6 +489,18 @@ contract UniversalCore is emit PauserRoleGranted(newPauser); } + /// @notice Rescue native PC stuck in the contract. Only callable by admin. + /// @param to Recipient address for the rescued PC + /// @param amount Amount of native PC to rescue + function rescueNativePC(address payable to, uint256 amount) external onlyAdmin { + if (to == address(0)) revert CommonErrors.ZeroAddress(); + if (amount == 0) revert CommonErrors.ZeroAmount(); + if (amount > address(this).balance) revert CommonErrors.InsufficientBalance(); + (bool ok,) = to.call{value: amount}(""); + if (!ok) revert CommonErrors.TransferFailed(); + emit RescueNativePC(to, amount); + } + // ========================= // UC_6: PRIVATE HELPERS // ========================= diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 21f88c3..70b9aba 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -15,6 +15,7 @@ import "../../test/mocks/MockPRC20.sol"; import "../../test/mocks/MaliciousPRC20.sol"; import "../../test/mocks/RevertingPRC20.sol"; import "../../test/mocks/FalseReturningPRC20.sol"; +import "../../test/mocks/RevertingTarget.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -65,6 +66,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); + event RescueNativePC(address indexed to, uint256 amount); function setUp() public { // Setup accounts @@ -1474,4 +1476,56 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.expectRevert(UniversalCoreErrors.PRC20OperationFailed.selector); universalCore.refundUnusedGas(address(falseToken), 1000, makeAddr("target"), false, 0, 0); } + + // ========================= + // Rescue Native PC Tests + // ========================= + + function test_RescueNativePC_HappyPath() public { + address payable recipient = payable(makeAddr("rescueRecipient")); + uint256 stuckAmount = 1 ether; + vm.deal(address(universalCore), stuckAmount); + + vm.expectEmit(true, false, false, true); + emit RescueNativePC(recipient, stuckAmount); + + universalCore.rescueNativePC(recipient, stuckAmount); + + assertEq(address(universalCore).balance, 0); + assertEq(recipient.balance, stuckAmount); + } + + function test_RescueNativePC_OnlyAdmin() public { + vm.deal(address(universalCore), 1 ether); + address nonAdmin = makeAddr("nonAdmin"); + + vm.expectRevert(CommonErrors.InvalidOwner.selector); + vm.prank(nonAdmin); + universalCore.rescueNativePC(payable(nonAdmin), 1 ether); + } + + function test_RescueNativePC_ZeroAddressReverts() public { + vm.deal(address(universalCore), 1 ether); + vm.expectRevert(CommonErrors.ZeroAddress.selector); + universalCore.rescueNativePC(payable(address(0)), 1 ether); + } + + function test_RescueNativePC_ZeroAmountReverts() public { + vm.deal(address(universalCore), 1 ether); + vm.expectRevert(CommonErrors.ZeroAmount.selector); + universalCore.rescueNativePC(payable(makeAddr("r")), 0); + } + + function test_RescueNativePC_InsufficientBalanceReverts() public { + vm.deal(address(universalCore), 0.5 ether); + vm.expectRevert(CommonErrors.InsufficientBalance.selector); + universalCore.rescueNativePC(payable(makeAddr("r")), 1 ether); + } + + function test_RescueNativePC_TransferToNonPayableReverts() public { + vm.deal(address(universalCore), 1 ether); + RevertingTarget nonPayable = new RevertingTarget(); + vm.expectRevert(CommonErrors.TransferFailed.selector); + universalCore.rescueNativePC(payable(address(nonPayable)), 1 ether); + } } From 41ebc6a5f3f3d81a5a0fb50fa65f4aa1691146d8 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 22 Apr 2026 07:13:12 +0530 Subject: [PATCH 43/56] F-2026-15543: remove setPauserRole, use OZ grantRole/revokeRole directly --- docs/THREAT_MODELLING_DOC.md | 10 ++++---- docs/UniversalCore.md | 3 ++- src/Interfaces/ICEAFactory.sol | 4 --- src/Interfaces/IUEAFactory.sol | 4 --- src/Interfaces/IUniversalCore.sol | 4 --- src/UniversalCore.sol | 10 -------- src/cea/CEAFactory.sol | 9 ------- src/testnetV0/UEAFactoryV0.sol | 6 +++++ src/uea/UEAFactory.sol | 9 ------- test/tests_cea/CEAFactory.t.sol | 22 +++++++--------- test/tests_token_and_core/UniversalCore.t.sol | 25 ++++++++----------- test/tests_uea_and_factory/UEAFactory.t.sol | 21 ++++++---------- 12 files changed, 40 insertions(+), 87 deletions(-) diff --git a/docs/THREAT_MODELLING_DOC.md b/docs/THREAT_MODELLING_DOC.md index b836ffb..fc59c1a 100644 --- a/docs/THREAT_MODELLING_DOC.md +++ b/docs/THREAT_MODELLING_DOC.md @@ -160,7 +160,7 @@ Uniswap V3 pool infrastructure. | `setDefaultFeeTier` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `setSlippageTolerance` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `setDefaultDeadlineMins` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `grantRole(PAUSER_ROLE, addr)` | `DEFAULT_ADMIN_ROLE` | OZ `AccessControl` | | `pause` | `PAUSER_ROLE` | OZ `Pausable` | | `unpause` | `PAUSER_ROLE` | OZ `Pausable` | | `receive()` | Anyone | `payable` | @@ -400,9 +400,9 @@ mappings. Maintains bidirectional `UOA ↔ UEA` address index. | Function | Caller | Guard | | --------------------------- | -------------------- | --------------- | | `deployUEA(id)` | Anyone | `whenNotPaused` | -| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | -| `setUEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | +| `grantRole(PAUSER_ROLE, a)` | `DEFAULT_ADMIN_ROLE` | OZ `AccessControl` | +| `setUEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `setUEAMigrationContract` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `registerNewChain` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `registerUEA` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | @@ -567,7 +567,7 @@ Maintains bidirectional `pushAccount ↔ CEA` mappings. Stores shared config | --------------------------- | -------------------- | ---------------------------- | | `deployCEA(pushAccount)` | `VAULT` | `onlyVault`, `whenNotPaused` | | `pause` / `unpause` | `PAUSER_ROLE` | OZ `Pausable` | -| `setPauserRole` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | +| `grantRole(PAUSER_ROLE, a)` | `DEFAULT_ADMIN_ROLE` | OZ `AccessControl` | | `setVault` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `setCEAProxyImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | | `setCEAImplementation` | `DEFAULT_ADMIN_ROLE` | `onlyAdmin` | diff --git a/docs/UniversalCore.md b/docs/UniversalCore.md index 5d4268d..5d7b1b2 100644 --- a/docs/UniversalCore.md +++ b/docs/UniversalCore.md @@ -90,7 +90,8 @@ Granted to the deployer at initialization. Controls contract-level configuration | `setDefaultFeeTier(token, feeTier)` | Set default Uniswap V3 fee tier for a token (allowed tiers: 100, 500, 3000, 10000) | | `setDefaultDeadlineMins(minutesValue)` | Set default swap deadline | | `setUniversalGatewayPC(addr)` | Update the address authorized to call `swapAndBurnGas` | -| `setPauserRole(addr)` | Grant `PAUSER_ROLE` to an address (guardian) | +| `grantRole(PAUSER_ROLE, addr)` | Grant `PAUSER_ROLE` to an address (guardian) — inherited from OZ AccessControl | +| `revokeRole(PAUSER_ROLE, addr)` | Revoke `PAUSER_ROLE` from an address — inherited from OZ AccessControl | ### Gateway (`universalGatewayPC`) diff --git a/src/Interfaces/ICEAFactory.sol b/src/Interfaces/ICEAFactory.sol index 6093a40..58d76d3 100644 --- a/src/Interfaces/ICEAFactory.sol +++ b/src/Interfaces/ICEAFactory.sol @@ -41,10 +41,6 @@ interface ICEAFactory { /// @param newContract New migration contract address event CEAMigrationContractUpdated(address indexed oldContract, address indexed newContract); - /// @notice Emitted when the PAUSER_ROLE is granted to a new address. - /// @param pauser Address that was granted the pauser role - event PauserRoleGranted(address indexed pauser); - // ========================= // CF_1: VIEW FUNCTIONS // ========================= diff --git a/src/Interfaces/IUEAFactory.sol b/src/Interfaces/IUEAFactory.sol index 41679ec..e04c383 100644 --- a/src/Interfaces/IUEAFactory.sol +++ b/src/Interfaces/IUEAFactory.sol @@ -36,10 +36,6 @@ interface IUEAFactory { /// @param newUEA New UEA implementation address event UEAImplementationUpdated(bytes32 indexed vmHash, address previousUEA, address newUEA); - /// @notice Emitted when the PAUSER_ROLE is granted to a new address. - /// @param pauser Address that was granted the pauser role - event PauserRoleGranted(address indexed pauser); - // ========================= // UF_1: VIEW FUNCTIONS // ========================= diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index fb4580d..1cb2bb6 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -31,10 +31,6 @@ interface IUniversalCore { event SetUniswapV3Addresses(address factory, address swapRouter); event SetDefaultFeeTier(address indexed token, uint24 feeTier); - /// @notice Emitted when the PAUSER_ROLE is granted to a new address. - /// @param pauser Address that was granted the pauser role - event PauserRoleGranted(address indexed pauser); - /// @notice Emitted when stuck native PC is rescued by admin. /// @param to Recipient of the rescued PC /// @param amount Amount of native PC rescued diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 9a59af4..f647084 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -135,8 +135,6 @@ contract UniversalCore is _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(PAUSER_ROLE, initialPauser_); - emit PauserRoleGranted(initialPauser_); - WPC = wpc_; uniswapV3Factory = uniswapV3Factory_; uniswapV3SwapRouter = uniswapV3SwapRouter_; @@ -481,14 +479,6 @@ contract UniversalCore is _unpause(); } - /// @notice Grant PAUSER_ROLE to a new address. Only callable by admin. - /// @param newPauser Address to grant pauser role to - function setPauserRole(address newPauser) external onlyAdmin { - if (newPauser == address(0)) revert CommonErrors.ZeroAddress(); - _grantRole(PAUSER_ROLE, newPauser); - emit PauserRoleGranted(newPauser); - } - /// @notice Rescue native PC stuck in the contract. Only callable by admin. /// @param to Recipient address for the rescued PC /// @param amount Amount of native PC to rescue diff --git a/src/cea/CEAFactory.sol b/src/cea/CEAFactory.sol index cb36277..d01d436 100644 --- a/src/cea/CEAFactory.sol +++ b/src/cea/CEAFactory.sol @@ -104,7 +104,6 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); _grantRole(PAUSER_ROLE, initialPauser); - emit PauserRoleGranted(initialPauser); VAULT = initialVault; CEA_PROXY_IMPLEMENTATION = ceaProxyImplementation; @@ -191,14 +190,6 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _unpause(); } - /// @notice Grant PAUSER_ROLE to a new address. Only callable by DEFAULT_ADMIN_ROLE. - /// @param newPauser Address to grant pauser role to - function setPauserRole(address newPauser) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (newPauser == address(0)) revert CEAErrors.ZeroAddress(); - _grantRole(PAUSER_ROLE, newPauser); - emit PauserRoleGranted(newPauser); - } - /// @notice Sets the Vault address. Only callable by DEFAULT_ADMIN_ROLE. /// @param newVault New Vault address function setVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/src/testnetV0/UEAFactoryV0.sol b/src/testnetV0/UEAFactoryV0.sol index d24d9a5..56a467b 100644 --- a/src/testnetV0/UEAFactoryV0.sol +++ b/src/testnetV0/UEAFactoryV0.sol @@ -21,6 +21,12 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, IUEAFactory { using Clones for address; + // ========================= + // UF: EVENTS (V0-only, removed from IUEAFactory in V1) + // ========================= + + event PauserRoleGranted(address indexed pauser); + // ========================= // UF: STATE VARIABLES // ========================= diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index adeaf25..2ae9627 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -82,7 +82,6 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); _grantRole(PAUSER_ROLE, initialPauser); pushChainId = _pushChainId; - emit PauserRoleGranted(initialPauser); } // ========================= @@ -213,14 +212,6 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _unpause(); } - /// @notice Grant PAUSER_ROLE to a new address. Only callable by DEFAULT_ADMIN_ROLE. - /// @param newPauser Address to grant pauser role to - function setPauserRole(address newPauser) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (newPauser == address(0)) revert UEAErrors.InvalidInputArgs(); - _grantRole(PAUSER_ROLE, newPauser); - emit PauserRoleGranted(newPauser); - } - /// @notice Sets the UEAProxy implementation address. /// @param ueaProxyImplementation New UEAProxy implementation address function setUEAProxyImplementation(address ueaProxyImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/test/tests_cea/CEAFactory.t.sol b/test/tests_cea/CEAFactory.t.sol index deb009f..3b1d6f8 100644 --- a/test/tests_cea/CEAFactory.t.sol +++ b/test/tests_cea/CEAFactory.t.sol @@ -1446,31 +1446,27 @@ contract CEAFactoryTest is Test { assertTrue(factory.isCEA(cea)); } - function testSetPauserRole_OnlyOwner() public { + function testGrantPauserRole_OnlyAdmin() public { address newPauser = makeAddr("newPauser"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 pauserRole = factory.PAUSER_ROLE(); + vm.expectRevert( abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) ); vm.prank(nonOwner); - factory.setPauserRole(newPauser); - - vm.prank(owner); - factory.setPauserRole(newPauser); - assertTrue(factory.hasRole(factory.PAUSER_ROLE(), newPauser)); - } + factory.grantRole(pauserRole, newPauser); - function testSetPauserRole_ZeroAddressReverts() public { vm.prank(owner); - vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setPauserRole(address(0)); + factory.grantRole(pauserRole, newPauser); + assertTrue(factory.hasRole(pauserRole, newPauser)); } - function testSetPauserRole_NewPauserCanPause() public { + function testGrantPauserRole_NewPauserCanPause() public { address newPauser = makeAddr("newPauser2"); + bytes32 pauserRole = factory.PAUSER_ROLE(); vm.prank(owner); - factory.setPauserRole(newPauser); + factory.grantRole(pauserRole, newPauser); vm.prank(newPauser); factory.pause(); diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 70b9aba..8794f78 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -564,31 +564,26 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertFalse(universalCore.hasRole(universalCore.PAUSER_ROLE(), deployer)); } - function test_SetPauserRole_OnlyAdmin() public { + function test_GrantPauserRole_OnlyAdmin() public { address newPauser = makeAddr("newPauser"); + bytes32 role = universalCore.PAUSER_ROLE(); - // Non-admin cannot set pauser role vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setPauserRole(newPauser); - - // Admin can set pauser role - vm.prank(deployer); - universalCore.setPauserRole(newPauser); - assertTrue(universalCore.hasRole(universalCore.PAUSER_ROLE(), newPauser)); - } + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, bytes32(0)) + ); + universalCore.grantRole(role, newPauser); - function test_SetPauserRole_ZeroAddressReverts() public { vm.prank(deployer); - vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setPauserRole(address(0)); + universalCore.grantRole(role, newPauser); + assertTrue(universalCore.hasRole(role, newPauser)); } - function test_SetPauserRole_NewPauserCanPause() public { + function test_GrantPauserRole_NewPauserCanPause() public { address newPauser = makeAddr("newPauser"); vm.prank(deployer); - universalCore.setPauserRole(newPauser); + universalCore.grantRole(universalCore.PAUSER_ROLE(), newPauser); vm.prank(newPauser); universalCore.pause(); diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index 86e17a0..4974c3a 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -1053,29 +1053,24 @@ contract UEAFactoryTest is Test { assertTrue(factory.hasCode(ueaAddress)); } - function testSetPauserRole_OnlyOwner() public { + function testGrantPauserRole_OnlyAdmin() public { address newPauser = makeAddr("newPauser"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 pauserRole = factory.PAUSER_ROLE(); + vm.expectRevert( abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) ); vm.prank(nonOwner); - factory.setPauserRole(newPauser); + factory.grantRole(pauserRole, newPauser); - // Admin can grant pauser role - factory.setPauserRole(newPauser); - assertTrue(factory.hasRole(factory.PAUSER_ROLE(), newPauser)); - } - - function testSetPauserRole_ZeroAddressReverts() public { - vm.expectRevert(Errors.InvalidInputArgs.selector); - factory.setPauserRole(address(0)); + factory.grantRole(pauserRole, newPauser); + assertTrue(factory.hasRole(pauserRole, newPauser)); } - function testSetPauserRole_NewPauserCanPause() public { + function testGrantPauserRole_NewPauserCanPause() public { address newPauser = makeAddr("newPauser2"); - factory.setPauserRole(newPauser); + factory.grantRole(factory.PAUSER_ROLE(), newPauser); vm.prank(newPauser); factory.pause(); From 26745b08f5e0b98799d82e00631b9beaa9cbed44 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Wed, 22 Apr 2026 10:50:27 +0530 Subject: [PATCH 44/56] w.r.t F-2026-15660: removed VAULT+UG direct address in CEA.sol --- src/Interfaces/ICEA.sol | 8 ++-- src/Interfaces/ICEAFactory.sol | 4 ++ src/cea/CEA.sol | 45 ++++++++++-------- src/cea/CEAFactory.sol | 10 ++-- test/fuzz/CEAFactory_Fuzz.t.sol | 6 +-- test/tests_cea/CEA.t.sol | 28 +++-------- test/tests_cea/CEAFactory.t.sol | 52 ++++++++++----------- test/tests_cea/CEA_multicalls.t.sol | 2 +- test/tests_ceaMigration/CEA_Migration.t.sol | 4 +- 9 files changed, 76 insertions(+), 83 deletions(-) diff --git a/src/Interfaces/ICEA.sol b/src/Interfaces/ICEA.sol index e194734..b2cdda5 100644 --- a/src/Interfaces/ICEA.sol +++ b/src/Interfaces/ICEA.sol @@ -87,10 +87,8 @@ interface ICEA { // CEA_4: INITIALIZER // ========================= - /// @notice Initializes this CEA with its identity and references. + /// @notice Initializes this CEA with its identity and factory reference. /// @param _pushAccount Address of the UEA on Push Chain - /// @param _vault Address of the Vault on this chain - /// @param _universalGateway Address of the Universal Gateway - /// @param _factory Address of the CEA factory - function initializeCEA(address _pushAccount, address _vault, address _universalGateway, address _factory) external; + /// @param _factory Address of the CEA factory (source of truth for VAULT and gateway) + function initializeCEA(address _pushAccount, address _factory) external; } diff --git a/src/Interfaces/ICEAFactory.sol b/src/Interfaces/ICEAFactory.sol index 58d76d3..cff3387 100644 --- a/src/Interfaces/ICEAFactory.sol +++ b/src/Interfaces/ICEAFactory.sol @@ -49,6 +49,10 @@ interface ICEAFactory { /// @return Vault address function VAULT() external view returns (address); + /// @notice Returns the current Universal Gateway address. + /// @return Universal Gateway address + function UNIVERSAL_GATEWAY() external view returns (address); + /// @notice Returns the CEA proxy implementation used for clones. /// @return CEA proxy implementation address function CEA_PROXY_IMPLEMENTATION() external view returns (address); diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 14d1c5e..3944771 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -25,13 +25,7 @@ contract CEA is ICEA, ReentrancyGuard { /// @inheritdoc ICEA address public pushAccount; - /// @inheritdoc ICEA - address public VAULT; - - /// @notice Address of the Universal Gateway on this external chain. - address public UNIVERSAL_GATEWAY; - - /// @notice Reference to the CEA factory for fetching migration contract. + /// @notice Reference to the CEA factory — single source of truth for VAULT and UNIVERSAL_GATEWAY. ICEAFactory public factory; bool private _initialized; @@ -43,9 +37,9 @@ contract CEA is ICEA, ReentrancyGuard { // CEA: MODIFIERS // ========================= - /// @notice Restricts to the Vault contract. + /// @notice Restricts to the Vault contract (read live from factory). modifier onlyVault() { - if (msg.sender != VAULT) revert CEAErrors.NotVault(); + if (msg.sender != factory.VAULT()) revert CEAErrors.NotVault(); _; } @@ -54,23 +48,32 @@ contract CEA is ICEA, ReentrancyGuard { // ========================= /// @inheritdoc ICEA - function initializeCEA(address _pushAccount, address _vault, address _universalGateway, address _factory) external { + function initializeCEA(address _pushAccount, address _factory) external { if (_initialized) revert CEAErrors.AlreadyInitialized(); - if ( - _pushAccount == address(0) || _vault == address(0) || _universalGateway == address(0) - || _factory == address(0) - ) { + if (_pushAccount == address(0) || _factory == address(0)) { revert CEAErrors.ZeroAddress(); } pushAccount = _pushAccount; - VAULT = _vault; - UNIVERSAL_GATEWAY = _universalGateway; factory = ICEAFactory(_factory); _initialized = true; } + // ========================= + // CEA: FACTORY-BACKED GETTERS + // ========================= + + /// @inheritdoc ICEA + function VAULT() external view returns (address) { + return factory.VAULT(); + } + + /// @notice Returns the Universal Gateway address (live from factory). + function UNIVERSAL_GATEWAY() external view returns (address) { + return factory.UNIVERSAL_GATEWAY(); + } + // ========================= // CEA_1: VIEW FUNCTIONS // ========================= @@ -124,21 +127,23 @@ contract CEA is ICEA, ReentrancyGuard { signatureData: "" }); + address gateway = factory.UNIVERSAL_GATEWAY(); + if (amount > 0) { if (token == address(0)) { if (address(this).balance < amount) { revert CEAErrors.InsufficientBalance(); } - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA{value: amount}(req); + IUniversalGateway(gateway).sendUniversalTxFromCEA{value: amount}(req); } else { if (IERC20(token).balanceOf(address(this)) < amount) { revert CEAErrors.InsufficientBalance(); } - IERC20(token).approve(UNIVERSAL_GATEWAY, amount); - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA(req); + IERC20(token).approve(gateway, amount); + IUniversalGateway(gateway).sendUniversalTxFromCEA(req); } } else { - IUniversalGateway(UNIVERSAL_GATEWAY).sendUniversalTxFromCEA(req); + IUniversalGateway(gateway).sendUniversalTxFromCEA(req); } emit UniversalTxToUEA(address(this), pushAccount, token, amount); diff --git a/src/cea/CEAFactory.sol b/src/cea/CEAFactory.sol index d01d436..cfb399b 100644 --- a/src/cea/CEAFactory.sol +++ b/src/cea/CEAFactory.sol @@ -168,7 +168,7 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea ICEAProxy(cea).initializeCEAProxy(CEA_IMPLEMENTATION); - ICEA(cea).initializeCEA(pushAccount, VAULT, UNIVERSAL_GATEWAY, address(this)); + ICEA(cea).initializeCEA(pushAccount, address(this)); pushAccountToCEA[pushAccount] = cea; ceaToPushAccount[cea] = pushAccount; @@ -190,9 +190,9 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _unpause(); } - /// @notice Sets the Vault address. Only callable by DEFAULT_ADMIN_ROLE. + /// @notice Updates the Vault address. Only callable by DEFAULT_ADMIN_ROLE. /// @param newVault New Vault address - function setVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newVault == address(0)) revert CEAErrors.ZeroAddress(); address old = VAULT; VAULT = newVault; @@ -217,9 +217,9 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea emit CEAImplementationUpdated(old, newImplementation); } - /// @notice Sets the Universal Gateway address. Only callable by DEFAULT_ADMIN_ROLE. + /// @notice Updates the Universal Gateway address. Only callable by DEFAULT_ADMIN_ROLE. /// @param newUG New Universal Gateway address - function setUniversalGateway(address newUG) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateUniversalGateway(address newUG) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newUG == address(0)) revert CEAErrors.ZeroAddress(); address old = UNIVERSAL_GATEWAY; UNIVERSAL_GATEWAY = newUG; diff --git a/test/fuzz/CEAFactory_Fuzz.t.sol b/test/fuzz/CEAFactory_Fuzz.t.sol index d98c9a5..a876301 100644 --- a/test/fuzz/CEAFactory_Fuzz.t.sol +++ b/test/fuzz/CEAFactory_Fuzz.t.sol @@ -205,7 +205,7 @@ contract CEAFactory_FuzzTest is Test { abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, adminRole) ); vm.prank(caller); - factory.setVault(newVault); + factory.updateVault(newVault); } /// @dev Non-owner callers cannot call setCEAImplementation. @@ -243,7 +243,7 @@ contract CEAFactory_FuzzTest is Test { /// @dev setVault(address(0)) reverts with ZeroAddress. function testFuzz_setVault_zeroAddress_reverts() public { vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setVault(address(0)); + factory.updateVault(address(0)); } /// @dev setCEAProxyImplementation(address(0)) reverts with ZeroAddress. @@ -261,6 +261,6 @@ contract CEAFactory_FuzzTest is Test { /// @dev setUniversalGateway(address(0)) reverts with ZeroAddress. function testFuzz_setUniversalGateway_zeroAddress_reverts() public { vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setUniversalGateway(address(0)); + factory.updateUniversalGateway(address(0)); } } diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index b6a2538..a979fe6 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -286,38 +286,24 @@ contract CEATest is Test { function testRevertWhenInitializingTwice() public { CEA newCEA = new CEA(); - newCEA.initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); + newCEA.initializeCEA(ueaOnPush, address(factory)); vm.expectRevert(Errors.AlreadyInitialized.selector); - newCEA.initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); + newCEA.initializeCEA(ueaOnPush, 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(); - - vm.expectRevert(Errors.ZeroAddress.selector); - newCEA.initializeCEA(ueaOnPush, vault, address(0), address(factory)); + newCEA.initializeCEA(address(0), address(factory)); } function testRevertWhenInitializingWithZeroFactory() public { CEA newCEA = new CEA(); vm.expectRevert(Errors.ZeroAddress.selector); - newCEA.initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(0)); + newCEA.initializeCEA(ueaOnPush, address(0)); } function testIsInitializedBeforeInitialization() public { @@ -935,7 +921,7 @@ contract CEATest is Test { address(ceaInstance), 0, abi.encodeWithSignature( - "initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0) + "initializeCEA(address,address)", address(0), address(0) ) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1363,7 +1349,7 @@ contract CEATest is Test { address(ceaInstance), 0, abi.encodeWithSignature( - "initializeCEA(address,address,address,address)", address(0), address(0), address(0), address(0) + "initializeCEA(address,address)", address(0), address(0) ) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1745,7 +1731,7 @@ contract CEATest is Test { function testInitializeCEA_CannotBeCalledAgainAfterProxyDeployment() public deployCEA { vm.expectRevert(Errors.AlreadyInitialized.selector); CEA(payable(address(ceaInstance))) - .initializeCEA(ueaOnPush, vault, address(mockUniversalGateway), address(factory)); + .initializeCEA(ueaOnPush, address(factory)); } function testReceive_DirectETHTransferSucceeds() public deployCEA { diff --git a/test/tests_cea/CEAFactory.t.sol b/test/tests_cea/CEAFactory.t.sol index 3b1d6f8..7c8e477 100644 --- a/test/tests_cea/CEAFactory.t.sol +++ b/test/tests_cea/CEAFactory.t.sol @@ -197,17 +197,17 @@ contract CEAFactoryTest is Test { abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) ); vm.prank(nonOwner); - factory.setVault(newVault); + factory.updateVault(newVault); vm.prank(owner); - factory.setVault(newVault); + factory.updateVault(newVault); assertEq(factory.VAULT(), newVault, "Vault should be updated"); } function testSetVaultZeroAddressReverts() public { vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setVault(address(0)); + factory.updateVault(address(0)); } function testSetVaultUpdatesState() public { @@ -215,7 +215,7 @@ contract CEAFactoryTest is Test { address oldVault = factory.VAULT(); vm.prank(owner); - factory.setVault(newVault); + factory.updateVault(newVault); assertEq(factory.VAULT(), newVault, "Vault should be updated"); assertNotEq(factory.VAULT(), oldVault, "Vault should be different from old"); @@ -228,7 +228,7 @@ contract CEAFactoryTest is Test { vm.prank(owner); vm.expectEmit(true, true, false, false); emit ICEAFactory.VaultUpdated(oldVault, newVault); - factory.setVault(newVault); + factory.updateVault(newVault); } function testSetVaultMultipleTimes() public { @@ -237,13 +237,13 @@ contract CEAFactoryTest is Test { address vault3 = makeAddr("vault3"); vm.startPrank(owner); - factory.setVault(vault1); + factory.updateVault(vault1); assertEq(factory.VAULT(), vault1); - factory.setVault(vault2); + factory.updateVault(vault2); assertEq(factory.VAULT(), vault2); - factory.setVault(vault3); + factory.updateVault(vault3); assertEq(factory.VAULT(), vault3); vm.stopPrank(); } @@ -252,7 +252,7 @@ contract CEAFactoryTest is Test { address currentVault = factory.VAULT(); vm.prank(owner); - factory.setVault(currentVault); + factory.updateVault(currentVault); assertEq(factory.VAULT(), currentVault, "Vault should remain the same"); } @@ -263,7 +263,7 @@ contract CEAFactoryTest is Test { address newVault = makeAddr("newVault"); vm.prank(owner); - factory.setVault(newVault); + factory.updateVault(newVault); assertTrue(hasCode(cea), "CEA should still have code"); assertEq(factory.getPushAccountForCEA(cea), ueaOnPush, "Mapping should persist"); @@ -274,7 +274,7 @@ contract CEAFactoryTest is Test { address contractAddress = address(contractVault); vm.prank(owner); - factory.setVault(contractAddress); + factory.updateVault(contractAddress); assertEq(factory.VAULT(), contractAddress, "Vault can be a contract"); } @@ -463,17 +463,17 @@ contract CEAFactoryTest is Test { abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) ); vm.prank(nonOwner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); vm.prank(owner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); assertEq(factory.UNIVERSAL_GATEWAY(), address(newGateway)); } function testSetUniversalGatewayZeroAddressReverts() public { vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setUniversalGateway(address(0)); + factory.updateUniversalGateway(address(0)); } function testSetUniversalGatewayUpdatesState() public { @@ -481,7 +481,7 @@ contract CEAFactoryTest is Test { address oldGateway = factory.UNIVERSAL_GATEWAY(); vm.prank(owner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); assertEq(factory.UNIVERSAL_GATEWAY(), address(newGateway)); assertNotEq(factory.UNIVERSAL_GATEWAY(), oldGateway); @@ -819,7 +819,7 @@ contract CEAFactoryTest is Test { // Similar to above - setter prevents zero address vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); - factory.setUniversalGateway(address(0)); + factory.updateUniversalGateway(address(0)); } // ========================================================================= @@ -1021,7 +1021,7 @@ contract CEAFactoryTest is Test { function testDeployCEAWithUpdatedVault() public { address newVault = makeAddr("newVault"); vm.prank(owner); - factory.setVault(newVault); + factory.updateVault(newVault); // Deploy with new vault vm.prank(newVault); @@ -1034,7 +1034,7 @@ contract CEAFactoryTest is Test { function testDeployCEAWithUpdatedGateway() public { MockUniversalGateway newGateway = new MockUniversalGateway(); vm.prank(owner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); address cea = deployCEAHelper(ueaOnPush); CEA ceaInstance = CEA(payable(cea)); @@ -1230,7 +1230,7 @@ contract CEAFactoryTest is Test { function testUpdateGatewayBeforeDeployment() public { MockUniversalGateway newGateway = new MockUniversalGateway(); vm.prank(owner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); address cea = deployCEAHelper(ueaOnPush); CEA ceaInstance = CEA(payable(cea)); @@ -1240,7 +1240,7 @@ contract CEAFactoryTest is Test { function testUpdateVaultBeforeDeployment() public { address newVault = makeAddr("newVault"); vm.prank(owner); - factory.setVault(newVault); + factory.updateVault(newVault); vm.prank(newVault); address cea = factory.deployCEA(ueaOnPush); @@ -1252,15 +1252,15 @@ contract CEAFactoryTest is Test { // Deploy first address cea = deployCEAHelper(ueaOnPush); CEA ceaInstance = CEA(payable(cea)); - address originalGateway = ceaInstance.UNIVERSAL_GATEWAY(); // Update gateway MockUniversalGateway newGateway = new MockUniversalGateway(); vm.prank(owner); - factory.setUniversalGateway(address(newGateway)); + factory.updateUniversalGateway(address(newGateway)); - // Existing CEA should still have old gateway - assertEq(ceaInstance.UNIVERSAL_GATEWAY(), originalGateway, "Existing CEA should keep old gateway"); + // UNIVERSAL_GATEWAY() delegates to the factory, so all existing CEAs immediately + // reflect the factory's current value — there is no per-CEA stored copy. + assertEq(ceaInstance.UNIVERSAL_GATEWAY(), address(newGateway), "Existing CEA should see new gateway via factory"); } // ========================================================================= @@ -1276,14 +1276,14 @@ contract CEAFactoryTest is Test { abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, adminRole) ); vm.prank(vault); - factory.setVault(newVault); + factory.updateVault(newVault); // Non-owner cannot change vm.expectRevert( abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) ); vm.prank(nonOwner); - factory.setVault(newVault); + factory.updateVault(newVault); } function testPreventUnauthorizedImplementationChange() public { diff --git a/test/tests_cea/CEA_multicalls.t.sol b/test/tests_cea/CEA_multicalls.t.sol index 4aee6b4..9d3ac1d 100644 --- a/test/tests_cea/CEA_multicalls.t.sol +++ b/test/tests_cea/CEA_multicalls.t.sol @@ -231,7 +231,7 @@ contract CEA_NewMulticallTests is CEATest { bytes memory payload = encodeCalls(calls); vm.prank(vault); - // Mismatched selector (3 params vs 4) — no function match, empty return data + // Mismatched selector (3 params vs 2) — no function match, empty return data vm.expectRevert(Errors.ExecutionFailed.selector); ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), payload); } diff --git a/test/tests_ceaMigration/CEA_Migration.t.sol b/test/tests_ceaMigration/CEA_Migration.t.sol index b89c950..5c002d0 100644 --- a/test/tests_ceaMigration/CEA_Migration.t.sol +++ b/test/tests_ceaMigration/CEA_Migration.t.sol @@ -87,7 +87,7 @@ contract CEA_MigrationTest is Test { function test_initializeCEA_WithFactory() public { CEA newCEA = new CEA(); - newCEA.initializeCEA(ueaOnPush, vault, universalGateway, address(factory)); + newCEA.initializeCEA(ueaOnPush, address(factory)); assertTrue(newCEA.isInitialized(), "CEA should be initialized"); assertEq(address(newCEA.factory()), address(factory), "Factory should be set"); @@ -97,7 +97,7 @@ contract CEA_MigrationTest is Test { CEA newCEA = new CEA(); vm.expectRevert(Errors.ZeroAddress.selector); - newCEA.initializeCEA(ueaOnPush, vault, universalGateway, address(0)); + newCEA.initializeCEA(ueaOnPush, address(0)); } // ========================================================================= From 6215a516b21ec1cd7bf9c94806572c1a866b27ec Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 27 Apr 2026 15:46:01 +0530 Subject: [PATCH 45/56] F-2026-15615 | Inclusion of Granular Role Separation with AccessControlDefaultAdminRulesUpgradeable --- scripts/cea/deployCEA.s.sol | 14 +- scripts/cea/deployCEAMigration.s.sol | 6 +- scripts/uea/deployFactory.s.sol | 4 +- src/Interfaces/IUniversalCore.sol | 4 +- src/UniversalCore.sol | 125 +++--- src/cea/CEAFactory.sol | 91 ++-- src/testnetV0/IUniversalCoreV0.sol | 4 +- src/testnetV0/UEAFactoryV0.sol | 6 +- src/testnetV0/UniversalCoreV0.sol | 22 +- src/uea/UEAFactory.sol | 63 +-- test/fork/ForkUniversalCore.t.sol | 23 +- test/fuzz/CEAFactory_Fuzz.t.sol | 36 +- test/fuzz/CEAMigration_Fuzz.t.sol | 4 +- test/fuzz/PRC20_Fuzz.t.sol | 2 +- test/fuzz/UEAFactory_Fuzz.t.sol | 19 +- test/fuzz/UEA_EVM_Fuzz.t.sol | 4 +- test/fuzz/UEA_SVM_Fuzz.t.sol | 2 +- test/fuzz/UniversalCore_Fuzz.t.sol | 92 ++-- test/mocks/MaliciousPRC20.sol | 2 +- test/tests_cea/CEAFactory.t.sol | 170 +++++-- test/tests_cea/CEA_singleCall.t.sol | 4 +- .../CEAFactory_Migration.t.sol | 26 +- .../CEAMigration_Integration.t.sol | 4 +- test/tests_ceaMigration/CEA_Migration.t.sol | 16 +- .../ForkUniversalCoreAMM.t.sol | 82 ++-- test/tests_token_and_core/PRC20.t.sol | 30 +- test/tests_token_and_core/UniversalCore.t.sol | 418 +++++++++++------- .../UniversalCoreRefund.t.sol | 13 +- .../UniversalCoreSwapFee.t.sol | 46 +- test/tests_ueaMigration/BaseTest.t.sol | 4 +- test/tests_uea_and_factory/UEAFactory.t.sol | 217 ++++++--- .../tests_uea_and_factory/UEAProxyCalls.t.sol | 2 +- test/tests_uea_and_factory/UEA_EVM.t.sol | 8 +- test/tests_uea_and_factory/UEA_SVM.t.sol | 8 +- 34 files changed, 952 insertions(+), 619 deletions(-) diff --git a/scripts/cea/deployCEA.s.sol b/scripts/cea/deployCEA.s.sol index 3c55d6b..475b2d9 100644 --- a/scripts/cea/deployCEA.s.sol +++ b/scripts/cea/deployCEA.s.sol @@ -11,7 +11,7 @@ import {CEA} from "../../src/cea/CEA.sol"; * @dev Deploys only the CEA logic contract (no factory, proxy, or admin). * Useful when upgrading the CEA implementation on an existing CEAFactory. * - * After deployment, call `CEAFactory.updateCEAImplementation(newCEAImpl)` + * After deployment, call `CEAFactory.setCEAImplementation(newCEAImpl)` * on the factory to point clones at the new implementation. * * CONFIGURATION: @@ -62,19 +62,13 @@ contract DeployCEAScript is Script { console.log(json); // Write to file - string memory filename = string( - abi.encodePacked( - "deployments/cea-impl-", - vm.toString(chainId), - ".json" - ) - ); + string memory filename = string(abi.encodePacked("deployments/cea-impl-", vm.toString(chainId), ".json")); vm.writeFile(filename, json); console.log("\nDeployment saved to:", filename); console.log("\n=== Deployment Complete ==="); console.log( - "NEXT STEP: Call CEAFactory.updateCEAImplementation(", + "NEXT STEP: Call CEAFactory.setCEAImplementation(", address(ceaImplementation), ") on the factory proxy to activate this implementation." ); @@ -111,7 +105,7 @@ contract DeployCEAScript is Script { * Update the factory to use the new implementation: * * cast send \ - * "updateCEAImplementation(address)" \ + * "setCEAImplementation(address)" \ * --rpc-url $RPC_URL \ * --private-key $KEY */ diff --git a/scripts/cea/deployCEAMigration.s.sol b/scripts/cea/deployCEAMigration.s.sol index 931c7af..1364c81 100644 --- a/scripts/cea/deployCEAMigration.s.sol +++ b/scripts/cea/deployCEAMigration.s.sol @@ -13,7 +13,7 @@ import {CEAFactory} from "../../src/cea/CEAFactory.sol"; * @dev Steps: * 1. Deploy CEA_V2 (new implementation) * 2. Deploy CEAMigration(ceaV2Address) - * 3. Call CEAFactory.setCEAMigrationContract(migrationAddress) + * 3. Call CEAFactory.updateCEAMigrationContract(migrationAddress) * * CONFIGURATION: * Environment variables needed: KEY, RPC_URL @@ -48,8 +48,8 @@ contract DeployCEAMigrationScript is Script { // 3. Set migration contract in factory CEAFactory factory = CEAFactory(CEA_FACTORY_PROXY); - factory.setCEAMigrationContract(address(migration)); - console.log("[3/3] setCEAMigrationContract called"); + factory.updateCEAMigrationContract(address(migration)); + console.log("[3/3] updateCEAMigrationContract called"); vm.stopBroadcast(); diff --git a/scripts/uea/deployFactory.s.sol b/scripts/uea/deployFactory.s.sol index 5eb9a0a..ca7e04d 100644 --- a/scripts/uea/deployFactory.s.sol +++ b/scripts/uea/deployFactory.s.sol @@ -68,7 +68,7 @@ contract DeployUEAFactoryScript is Script { UEAProxy proxyImpl = new UEAProxy(); console.log("UEAProxy Implementation deployed at:", address(proxyImpl)); - factory.setUEAProxyImplementation(address(proxyImpl)); + factory.updateUEAProxyImplementation(address(proxyImpl)); console.log("UEAProxy impl set in the factory"); // 1. Deploy UEA_EVM implementation @@ -87,7 +87,7 @@ contract DeployUEAFactoryScript is Script { UEAProxy ueaProxy = new UEAProxy(); console.log("UEAProxy deployed at:", address(ueaProxy)); - factory.setUEAProxyImplementation(address(ueaProxy)); + factory.updateUEAProxyImplementation(address(ueaProxy)); console.log("UEAProxy set in the factory"); vm.stopBroadcast(); diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 1cb2bb6..17b7a6a 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -177,12 +177,12 @@ interface IUniversalCore { /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function setProtocolFeeByToken(address token, uint256 fee) external; + function updateProtocolFeeByToken(address token, uint256 fee) external; /// @notice Set rescue funds gas limit for a specific chain. /// @param chainNamespace Chain Namespace /// @param gasLimit Rescue funds gas limit for the chain - function setRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; + function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; /// @notice Get the UniversalGatewayPC address. function universalGatewayPC() external view returns (address); diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index f647084..8926edf 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + AccessControlDefaultAdminRulesUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; @@ -24,12 +26,17 @@ import {UniversalCoreErrors, CommonErrors} from "./libraries/Errors.sol"; * - Setting up the gas price for each chain. * - Maintaining a registry of Uniswap V3 pools for each token pair. * @dev All imperative functionalities are handled by the Universal Executor Module. + * + * Access control: AccessControlDefaultAdminRulesUpgradeable (2-day delay). + * Roles: DEFAULT_ADMIN_ROLE (root), ROLE_MANAGER_ROLE (grants operational roles), + * UVCORE_ADMIN_ROLE (protocol config), OPERATOR_ROLE (address setters + unpause), + * PAUSER_ROLE (pause only). */ contract UniversalCore is IUniversalCore, Initializable, ReentrancyGuardUpgradeable, - AccessControlUpgradeable, + AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable { using SafeERC20 for IERC20; @@ -41,7 +48,9 @@ contract UniversalCore is // -- Protocol constants & roles -- address public immutable UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; - bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant ROLE_MANAGER_ROLE = keccak256("ROLE_MANAGER_ROLE"); + bytes32 public constant UVCORE_ADMIN_ROLE = keccak256("UVCORE_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // -- Uniswap V3 fee tiers -- @@ -66,7 +75,7 @@ contract UniversalCore is /// @notice Maximum acceptable age (seconds) of `timestampObservedAtByChainNamespace` /// before gas fee quotes for that chain are rejected as stale. /// @dev `0` disables the check for that chain (opt-in). Set per chain by - /// MANAGER_ROLE via `setMaxStalenessByChain`. + /// UVCORE_ADMIN_ROLE via `updateMaxStalenessByChain`. mapping(string => uint256) public maxStalenessByChainNamespace; // -- Token configuration -- @@ -102,13 +111,6 @@ contract UniversalCore is _; } - modifier onlyAdmin() { - if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { - revert CommonErrors.InvalidOwner(); - } - _; - } - // ========================= // UC: CONSTRUCTOR // ========================= @@ -118,26 +120,36 @@ contract UniversalCore is } /// @dev Initializer function for the upgradeable contract. - /// @param wpc_ Address of the wrapped PC token - /// @param uniswapV3Factory_ Address of the Uniswap V3 factory - /// @param uniswapV3SwapRouter_ Address of the Uniswap V3 swap router + /// @param _admin Admin address — granted DEFAULT_ADMIN_ROLE + all operational roles + /// @param _pauser Address granted the PAUSER_ROLE + /// @param _wpc Address of the wrapped PC token + /// @param _uniswapV3Factory Address of the Uniswap V3 factory + /// @param _uniswapV3SwapRouter Address of the Uniswap V3 swap router function initialize( - address wpc_, - address uniswapV3Factory_, - address uniswapV3SwapRouter_, - address initialPauser_ + address _admin, + address _pauser, + address _wpc, + address _uniswapV3Factory, + address _uniswapV3SwapRouter ) public virtual initializer { - if (initialPauser_ == address(0)) revert CommonErrors.ZeroAddress(); + if (_admin == address(0) || _pauser == address(0)) revert CommonErrors.ZeroAddress(); + __ReentrancyGuard_init(); - __AccessControl_init(); + __AccessControlDefaultAdminRules_init(1 days, _admin); __Pausable_init(); - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - _grantRole(PAUSER_ROLE, initialPauser_); + _setRoleAdmin(UVCORE_ADMIN_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(OPERATOR_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(PAUSER_ROLE, ROLE_MANAGER_ROLE); - WPC = wpc_; - uniswapV3Factory = uniswapV3Factory_; - uniswapV3SwapRouter = uniswapV3SwapRouter_; + _grantRole(ROLE_MANAGER_ROLE, _admin); + _grantRole(UVCORE_ADMIN_ROLE, _admin); + _grantRole(OPERATOR_ROLE, _admin); + _grantRole(PAUSER_ROLE, _pauser); + + WPC = _wpc; + uniswapV3Factory = _uniswapV3Factory; + uniswapV3SwapRouter = _uniswapV3SwapRouter; defaultDeadlineMins = 20; } @@ -146,7 +158,12 @@ contract UniversalCore is // ========================= /// @inheritdoc IUniversalCore - function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule whenNotPaused nonReentrant { + function depositPRC20Token(address prc20, uint256 amount, address recipient) + external + onlyUEModule + whenNotPaused + nonReentrant + { _validateParams(prc20, amount, recipient); if (!IPRC20(prc20).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } @@ -318,13 +335,13 @@ contract UniversalCore is } // ========================= - // UC_4: MANAGER ACTIONS + // UC_4: ADMIN CONFIG // ========================= /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function setProtocolFeeByToken(address token, uint256 fee) external onlyRole(MANAGER_ROLE) { + function updateProtocolFeeByToken(address token, uint256 fee) external onlyRole(UVCORE_ADMIN_ROLE) { if (token == address(0)) revert CommonErrors.ZeroAddress(); protocolFeeByToken[token] = fee; emit SetProtocolFeeByToken(token, fee); @@ -336,7 +353,10 @@ contract UniversalCore is /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasToken Gas coin address /// @param fee Uniswap V3 fee tier - function setGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) external onlyRole(MANAGER_ROLE) { + function updateGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) + external + onlyRole(UVCORE_ADMIN_ROLE) + { if (gasToken == address(0)) revert CommonErrors.ZeroAddress(); address pool = IUniswapV3Factory(uniswapV3Factory) @@ -353,10 +373,7 @@ contract UniversalCore is /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param price Gas price on the external chain /// @param chainHeight Block height observed on the external chain - function setChainMeta(string memory chainNamespace, uint256 price, uint256 chainHeight) - external - onlyUEModule - { + function setChainMeta(string memory chainNamespace, uint256 price, uint256 chainHeight) external onlyUEModule { if (price == 0) revert UniversalCoreErrors.ZeroGasPrice(); gasPriceByChainNamespace[chainNamespace] = price; chainHeightByChainNamespace[chainNamespace] = chainHeight; @@ -367,7 +384,7 @@ contract UniversalCore is /// @notice Setter for gasTokenPRC20ByChainNamespace map. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param prc20 PRC20 address - function setGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(MANAGER_ROLE) { + function updateGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(UVCORE_ADMIN_ROLE) { if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); gasTokenPRC20ByChainNamespace[chainNamespace] = prc20; gasPriceByChainNamespace[chainNamespace] = 0; @@ -375,20 +392,20 @@ contract UniversalCore is } // ========================= - // UC_5: ADMIN ACTIONS + // UC_5: OPERATOR ACTIONS // ========================= /// @notice Set auto-swap support for a token. /// @param token Token address /// @param supported Whether the token supports auto-swap - function setAutoSwapSupported(address token, bool supported) external onlyAdmin { + function updateAutoSwapSupported(address token, bool supported) external onlyRole(UVCORE_ADMIN_ROLE) { isAutoSwapSupported[token] = supported; emit SetAutoSwapSupported(token, supported); } /// @notice Set the wrapped PC address. /// @param addr WPC new address - function setWPC(address addr) external onlyAdmin { + function updateWPC(address addr) external onlyRole(OPERATOR_ROLE) { if (addr == address(0)) revert CommonErrors.ZeroAddress(); address oldAddr = WPC; WPC = addr; @@ -397,7 +414,7 @@ contract UniversalCore is /// @notice Set the UniversalGatewayPC address. /// @param addr UniversalGatewayPC address - function setUniversalGatewayPC(address addr) external onlyAdmin { + function updateUniversalGatewayPC(address addr) external onlyRole(OPERATOR_ROLE) { if (addr == address(0)) revert CommonErrors.ZeroAddress(); address oldAddr = universalGatewayPC; universalGatewayPC = addr; @@ -407,7 +424,7 @@ contract UniversalCore is /// @notice Setter for Uniswap V3 addresses. /// @param factory Uniswap V3 Factory address /// @param swapRouter Uniswap V3 SwapRouter address - function setUniswapV3Addresses(address factory, address swapRouter) external onlyAdmin { + function updateUniswapV3Addresses(address factory, address swapRouter) external onlyRole(OPERATOR_ROLE) { if (factory == address(0) || swapRouter == address(0)) { revert CommonErrors.ZeroAddress(); } @@ -419,9 +436,12 @@ contract UniversalCore is /// @notice Set default fee tier for a token. /// @param token Token address /// @param feeTier Fee tier (500, 3000, 10000) - function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { + function updateDefaultFeeTier(address token, uint24 feeTier) external onlyRole(UVCORE_ADMIN_ROLE) { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { + if ( + feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM + && feeTier != FEE_TIER_HIGH + ) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; @@ -430,7 +450,7 @@ contract UniversalCore is /// @notice Set default deadline in minutes. /// @param minutesValue Default deadline in minutes - function setDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { + function updateDefaultDeadlineMins(uint256 minutesValue) external onlyRole(UVCORE_ADMIN_ROLE) { defaultDeadlineMins = minutesValue; emit SetDefaultDeadlineMins(minutesValue); } @@ -438,7 +458,10 @@ contract UniversalCore is /// @notice Set base gas limit for a specific chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasLimit Base gas limit for the chain - function setBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external onlyRole(MANAGER_ROLE) { + function updateBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + external + onlyRole(UVCORE_ADMIN_ROLE) + { baseGasLimitByChainNamespace[chainNamespace] = gasLimit; emit SetBaseGasLimitByChain(chainNamespace, gasLimit); } @@ -446,9 +469,9 @@ contract UniversalCore is /// @notice Set rescue funds gas limit for a specific chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasLimit Rescue funds gas limit for the chain - function setRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external - onlyRole(MANAGER_ROLE) + onlyRole(UVCORE_ADMIN_ROLE) { rescueFundsGasLimitByChainNamespace[chainNamespace] = gasLimit; emit SetRescueFundsGasLimitByChain(chainNamespace, gasLimit); @@ -461,9 +484,9 @@ contract UniversalCore is /// older than `block.timestamp - maxStaleness`. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param maxStaleness Maximum acceptable age of gas data in seconds (0 disables) - function setMaxStalenessByChain(string memory chainNamespace, uint256 maxStaleness) + function updateMaxStalenessByChain(string memory chainNamespace, uint256 maxStaleness) external - onlyRole(MANAGER_ROLE) + onlyRole(UVCORE_ADMIN_ROLE) { maxStalenessByChainNamespace[chainNamespace] = maxStaleness; emit SetMaxStalenessByChain(chainNamespace, maxStaleness); @@ -474,15 +497,15 @@ contract UniversalCore is _pause(); } - /// @notice Unpause the contract - resumes all deposit functions. Only callable by PAUSER_ROLE. - function unpause() external onlyRole(PAUSER_ROLE) { + /// @notice Unpause the contract - resumes all deposit functions. Only callable by OPERATOR_ROLE. + function unpause() external onlyRole(OPERATOR_ROLE) { _unpause(); } - /// @notice Rescue native PC stuck in the contract. Only callable by admin. + /// @notice Rescue native PC stuck in the contract. Only callable by UVCORE_ADMIN_ROLE. /// @param to Recipient address for the rescued PC /// @param amount Amount of native PC to rescue - function rescueNativePC(address payable to, uint256 amount) external onlyAdmin { + function rescueNativePC(address payable to, uint256 amount) external onlyRole(UVCORE_ADMIN_ROLE) { if (to == address(0)) revert CommonErrors.ZeroAddress(); if (amount == 0) revert CommonErrors.ZeroAmount(); if (amount > address(this).balance) revert CommonErrors.InsufficientBalance(); diff --git a/src/cea/CEAFactory.sol b/src/cea/CEAFactory.sol index cfb399b..94b2943 100644 --- a/src/cea/CEAFactory.sol +++ b/src/cea/CEAFactory.sol @@ -8,7 +8,9 @@ import {CEAErrors} from "../libraries/Errors.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + AccessControlDefaultAdminRulesUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; /** @@ -18,18 +20,21 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau * implementation. Maintains a 1:1 mapping between UEA (on Push) and * CEA (on this chain). * - * Access control uses OpenZeppelin AccessControl: - * - DEFAULT_ADMIN_ROLE: governance — can update all config and grant roles. - * - PAUSER_ROLE: guardian hot-wallet — can pause/unpause only. + * Access control: AccessControlDefaultAdminRulesUpgradeable (2-day delay). + * Roles: DEFAULT_ADMIN_ROLE (root), ROLE_MANAGER_ROLE (grants operational roles), + * CEA_ADMIN_ROLE (implementation config), OPERATOR_ROLE (address setters + unpause), + * PAUSER_ROLE (pause only). */ -contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradeable, ICEAFactory { +contract CEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable, ICEAFactory { using Clones for address; // ========================= // CF: ROLES // ========================= - /// @notice Role that can pause and unpause CEA deployments. + bytes32 public constant ROLE_MANAGER_ROLE = keccak256("ROLE_MANAGER_ROLE"); + bytes32 public constant CEA_ADMIN_ROLE = keccak256("CEA_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ========================= @@ -77,38 +82,44 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea } /// @dev Initializer for the upgradeable CEAFactory. - /// @param initialAdmin Owner of the factory (governance) — granted DEFAULT_ADMIN_ROLE - /// @param initialPauser Address granted the PAUSER_ROLE - /// @param initialVault Vault address on this chain - /// @param ceaProxyImplementation CEA proxy implementation to clone (CEAProxy) - /// @param ceaImplementation CEA logic implementation (CEA) - /// @param universalGateway Universal Gateway on this chain + /// @param _admin Admin address — granted DEFAULT_ADMIN_ROLE + all operational roles + /// @param _pauser Address granted the PAUSER_ROLE + /// @param _vault Vault address on this chain + /// @param _ceaProxyImplementation CEA proxy implementation to clone (CEAProxy) + /// @param _ceaImplementation CEA logic implementation (CEA) + /// @param _universalGateway Universal Gateway on this chain function initialize( - address initialAdmin, - address initialPauser, - address initialVault, - address ceaProxyImplementation, - address ceaImplementation, - address universalGateway + address _admin, + address _pauser, + address _vault, + address _ceaProxyImplementation, + address _ceaImplementation, + address _universalGateway ) external initializer { if ( - initialAdmin == address(0) || initialPauser == address(0) || initialVault == address(0) - || ceaProxyImplementation == address(0) || ceaImplementation == address(0) - || universalGateway == address(0) + _admin == address(0) || _pauser == address(0) || _vault == address(0) + || _ceaProxyImplementation == address(0) || _ceaImplementation == address(0) + || _universalGateway == address(0) ) { revert CEAErrors.ZeroAddress(); } - __AccessControl_init(); + __AccessControlDefaultAdminRules_init(1 days, _admin); __Pausable_init(); - _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); - _grantRole(PAUSER_ROLE, initialPauser); + _setRoleAdmin(CEA_ADMIN_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(OPERATOR_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(PAUSER_ROLE, ROLE_MANAGER_ROLE); - VAULT = initialVault; - CEA_PROXY_IMPLEMENTATION = ceaProxyImplementation; - CEA_IMPLEMENTATION = ceaImplementation; - UNIVERSAL_GATEWAY = universalGateway; + _grantRole(ROLE_MANAGER_ROLE, _admin); + _grantRole(CEA_ADMIN_ROLE, _admin); + _grantRole(OPERATOR_ROLE, _admin); + _grantRole(PAUSER_ROLE, _pauser); + + VAULT = _vault; + CEA_PROXY_IMPLEMENTATION = _ceaProxyImplementation; + CEA_IMPLEMENTATION = _ceaImplementation; + UNIVERSAL_GATEWAY = _universalGateway; } // ========================= @@ -185,50 +196,50 @@ contract CEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _pause(); } - /// @notice Unpause CEA deployments. Only callable by PAUSER_ROLE. - function unpause() external onlyRole(PAUSER_ROLE) { + /// @notice Unpause CEA deployments. Only callable by OPERATOR_ROLE. + function unpause() external onlyRole(OPERATOR_ROLE) { _unpause(); } - /// @notice Updates the Vault address. Only callable by DEFAULT_ADMIN_ROLE. + /// @notice Updates the Vault address. Only callable by OPERATOR_ROLE. /// @param newVault New Vault address - function updateVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateVault(address newVault) external onlyRole(OPERATOR_ROLE) { if (newVault == address(0)) revert CEAErrors.ZeroAddress(); address old = VAULT; VAULT = newVault; emit VaultUpdated(old, newVault); } - /// @notice Sets the CEA proxy implementation (CEAProxy template). + /// @notice Sets the CEA proxy implementation (CEAProxy template). Only callable by CEA_ADMIN_ROLE. /// @param newImplementation New CEA proxy implementation address - function setCEAProxyImplementation(address newImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setCEAProxyImplementation(address newImplementation) external onlyRole(CEA_ADMIN_ROLE) { if (newImplementation == address(0)) revert CEAErrors.ZeroAddress(); address old = CEA_PROXY_IMPLEMENTATION; CEA_PROXY_IMPLEMENTATION = newImplementation; emit CEAProxyImplementationUpdated(old, newImplementation); } - /// @notice Sets the CEA logic implementation. + /// @notice Sets the CEA logic implementation. Only callable by CEA_ADMIN_ROLE. /// @param newImplementation New CEA logic implementation address - function setCEAImplementation(address newImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setCEAImplementation(address newImplementation) external onlyRole(CEA_ADMIN_ROLE) { if (newImplementation == address(0)) revert CEAErrors.ZeroAddress(); address old = CEA_IMPLEMENTATION; CEA_IMPLEMENTATION = newImplementation; emit CEAImplementationUpdated(old, newImplementation); } - /// @notice Updates the Universal Gateway address. Only callable by DEFAULT_ADMIN_ROLE. + /// @notice Updates the Universal Gateway address. Only callable by OPERATOR_ROLE. /// @param newUG New Universal Gateway address - function updateUniversalGateway(address newUG) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateUniversalGateway(address newUG) external onlyRole(OPERATOR_ROLE) { if (newUG == address(0)) revert CEAErrors.ZeroAddress(); address old = UNIVERSAL_GATEWAY; UNIVERSAL_GATEWAY = newUG; emit UniversalGatewayUpdated(old, newUG); } - /// @notice Sets the CEA migration contract address. + /// @notice Sets the CEA migration contract address. Only callable by CEA_ADMIN_ROLE. /// @param newMigrationContract Address of the new migration contract - function setCEAMigrationContract(address newMigrationContract) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateCEAMigrationContract(address newMigrationContract) external onlyRole(CEA_ADMIN_ROLE) { if (newMigrationContract == address(0)) revert CEAErrors.ZeroAddress(); address old = CEA_MIGRATION_CONTRACT; CEA_MIGRATION_CONTRACT = newMigrationContract; diff --git a/src/testnetV0/IUniversalCoreV0.sol b/src/testnetV0/IUniversalCoreV0.sol index 670ca63..1168ec5 100644 --- a/src/testnetV0/IUniversalCoreV0.sol +++ b/src/testnetV0/IUniversalCoreV0.sol @@ -214,7 +214,7 @@ interface IUniversalCoreV0 { /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function setProtocolFeeByToken( + function updateProtocolFeeByToken( address token, uint256 fee ) external; @@ -222,7 +222,7 @@ interface IUniversalCoreV0 { /// @notice Set rescue funds gas limit for a specific chain. /// @param chainNamespace Chain Namespace /// @param gasLimit Rescue funds gas limit for the chain - function setRescueFundsGasLimitByChain( + function updateRescueFundsGasLimitByChain( string memory chainNamespace, uint256 gasLimit ) external; diff --git a/src/testnetV0/UEAFactoryV0.sol b/src/testnetV0/UEAFactoryV0.sol index 56a467b..d6d2701 100644 --- a/src/testnetV0/UEAFactoryV0.sol +++ b/src/testnetV0/UEAFactoryV0.sol @@ -232,7 +232,7 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, /// @notice Sets the UEAProxy implementation address. /// @param ueaProxyImplementation New UEAProxy implementation address - function setUEAProxyImplementation(address ueaProxyImplementation) external onlyOwner { + function updateUEAProxyImplementation(address ueaProxyImplementation) external onlyOwner { if (ueaProxyImplementation == address(0)) { revert UEAErrors.InvalidInputArgs(); } @@ -241,7 +241,7 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, /// @notice Sets the UEA migration contract address. /// @param ueaMigrationContract New migration contract address - function setUEAMigrationContract(address ueaMigrationContract) external onlyOwner { + function updateUEAMigrationContract(address ueaMigrationContract) external onlyOwner { if (ueaMigrationContract == address(0)) { revert UEAErrors.InvalidInputArgs(); } @@ -249,7 +249,7 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, } /// @notice Update `pushChainId`. Reverts on empty string. - function setPushChainId(string memory _pushChainId) external onlyOwner { + function updatePushChainId(string memory _pushChainId) external onlyOwner { if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); pushChainId = _pushChainId; } diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index 43ffb63..c6201cf 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -353,7 +353,7 @@ contract UniversalCoreV0 is /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function setProtocolFeeByToken(address token, uint256 fee) external onlyRole(MANAGER_ROLE) { + function updateProtocolFeeByToken(address token, uint256 fee) external onlyRole(MANAGER_ROLE) { if (token == address(0)) revert CommonErrors.ZeroAddress(); protocolFeeByToken[token] = fee; emit SetProtocolFeeByToken(token, fee); @@ -363,7 +363,7 @@ contract UniversalCoreV0 is /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasToken Gas coin address /// @param fee Uniswap V3 fee tier - function setGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) external onlyRole(MANAGER_ROLE) { + function updateGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) external onlyRole(MANAGER_ROLE) { if (gasToken == address(0)) revert CommonErrors.ZeroAddress(); address pool = IUniswapV3Factory(uniswapV3Factory) @@ -402,7 +402,7 @@ contract UniversalCoreV0 is /// @notice Setter for gasTokenPRC20ByChainNamespace map. /// @param chainNamespace Chain Namespace /// @param prc20 PRC20 address - function setGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(MANAGER_ROLE) { + function updateGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(MANAGER_ROLE) { if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); gasTokenPRC20ByChainNamespace[chainNamespace] = prc20; emit SetGasToken(chainNamespace, prc20); @@ -424,20 +424,20 @@ contract UniversalCoreV0 is /// @notice Set auto-swap support for a token. /// @param token Token address /// @param supported Whether the token supports auto-swap - function setAutoSwapSupported(address token, bool supported) external onlyAdmin { + function updateAutoSwapSupported(address token, bool supported) external onlyAdmin { isAutoSwapSupported[token] = supported; } /// @notice Set the wrapped PC address. /// @param addr WPC new address - function setWPC(address addr) external onlyAdmin { + function updateWPC(address addr) external onlyAdmin { if (addr == address(0)) revert CommonErrors.ZeroAddress(); WPC = addr; } /// @notice Set the UniversalGatewayPC address. /// @param addr UniversalGatewayPC address - function setUniversalGatewayPC(address addr) external onlyAdmin { + function updateUniversalGatewayPC(address addr) external onlyAdmin { if (addr == address(0)) revert CommonErrors.ZeroAddress(); universalGatewayPC = addr; } @@ -446,7 +446,7 @@ contract UniversalCoreV0 is /// @param factory Uniswap V3 Factory address /// @param swapRouter Uniswap V3 SwapRouter address /// @param quoter Uniswap V3 Quoter address - function setUniswapV3Addresses(address factory, address swapRouter, address quoter) external onlyAdmin { + function updateUniswapV3Addresses(address factory, address swapRouter, address quoter) external onlyAdmin { if (factory == address(0) || swapRouter == address(0) || quoter == address(0)) { revert CommonErrors.ZeroAddress(); } @@ -458,7 +458,7 @@ contract UniversalCoreV0 is /// @notice Set default fee tier for a token. /// @param token Token address /// @param feeTier Fee tier (500, 3000, 10000) - function setDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { + function updateDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { if (token == address(0)) revert CommonErrors.ZeroAddress(); if (feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { revert UniversalCoreErrors.InvalidFeeTier(); @@ -468,7 +468,7 @@ contract UniversalCoreV0 is /// @notice Set default deadline in minutes. /// @param minutesValue Default deadline in minutes - function setDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { + function updateDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { defaultDeadlineMins = minutesValue; emit SetDefaultDeadlineMins(minutesValue); } @@ -476,7 +476,7 @@ contract UniversalCoreV0 is /// @notice Set base gas limit for a specific chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasLimit Base gas limit for the chain - function setBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external onlyRole(MANAGER_ROLE) { + function updateBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external onlyRole(MANAGER_ROLE) { baseGasLimitByChainNamespace[chainNamespace] = gasLimit; emit SetBaseGasLimitByChain(chainNamespace, gasLimit); } @@ -484,7 +484,7 @@ contract UniversalCoreV0 is /// @notice Set rescue funds gas limit for a specific chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasLimit Rescue funds gas limit for the chain - function setRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external onlyRole(MANAGER_ROLE) { diff --git a/src/uea/UEAFactory.sol b/src/uea/UEAFactory.sol index 2ae9627..341fb70 100644 --- a/src/uea/UEAFactory.sol +++ b/src/uea/UEAFactory.sol @@ -9,7 +9,8 @@ import {UEAProxy} from "./UEAProxy.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; /** @@ -18,18 +19,21 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau * @dev Uses OZ Clones library for deterministic CREATE2 deployment of UEA proxies. * Maps external chain identities to UEA addresses on Push Chain. * - * Access control uses OpenZeppelin AccessControl: - * - DEFAULT_ADMIN_ROLE: governance — can update all config and grant roles. - * - PAUSER_ROLE: guardian hot-wallet — can pause/unpause only. + * Access control: AccessControlDefaultAdminRulesUpgradeable (2-day delay). + * Roles: DEFAULT_ADMIN_ROLE (root), ROLE_MANAGER_ROLE (grants operational roles), + * UEA_ADMIN_ROLE (implementation + chain config), OPERATOR_ROLE (unpause), + * PAUSER_ROLE (pause only). */ -contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradeable, IUEAFactory { +contract UEAFactory is Initializable, AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable, IUEAFactory { using Clones for address; // ========================= // UF: ROLES // ========================= - /// @notice Role that can pause and unpause UEA deployments. + bytes32 public constant ROLE_MANAGER_ROLE = keccak256("ROLE_MANAGER_ROLE"); + bytes32 public constant UEA_ADMIN_ROLE = keccak256("UEA_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ========================= @@ -71,16 +75,25 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea // ========================= /// @dev Initializer for the upgradeable UEAFactory. - /// @param initialAdmin Initial admin — granted DEFAULT_ADMIN_ROLE (governance) - /// @param initialPauser Address granted the PAUSER_ROLE + /// @param _admin Admin address — granted DEFAULT_ADMIN_ROLE + all operational roles + /// @param _pauser Address granted the PAUSER_ROLE /// @param _pushChainId Push Chain numeric identifier (e.g. "42101") - function initialize(address initialAdmin, address initialPauser, string memory _pushChainId) public initializer { - if (initialAdmin == address(0) || initialPauser == address(0)) revert UEAErrors.InvalidInputArgs(); + function initialize(address _admin, address _pauser, string memory _pushChainId) public initializer { + if (_admin == address(0) || _pauser == address(0)) revert UEAErrors.InvalidInputArgs(); if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); - __AccessControl_init(); + + __AccessControlDefaultAdminRules_init(1 days, _admin); __Pausable_init(); - _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); - _grantRole(PAUSER_ROLE, initialPauser); + + _setRoleAdmin(UEA_ADMIN_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(OPERATOR_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(PAUSER_ROLE, ROLE_MANAGER_ROLE); + + _grantRole(ROLE_MANAGER_ROLE, _admin); + _grantRole(UEA_ADMIN_ROLE, _admin); + _grantRole(OPERATOR_ROLE, _admin); + _grantRole(PAUSER_ROLE, _pauser); + pushChainId = _pushChainId; } @@ -207,37 +220,37 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea _pause(); } - /// @notice Unpause UEA deployments. Only callable by PAUSER_ROLE. - function unpause() external onlyRole(PAUSER_ROLE) { + /// @notice Unpause UEA deployments. Only callable by OPERATOR_ROLE. + function unpause() external onlyRole(OPERATOR_ROLE) { _unpause(); } - /// @notice Sets the UEAProxy implementation address. + /// @notice Sets the UEAProxy implementation address. Only callable by UEA_ADMIN_ROLE. /// @param ueaProxyImplementation New UEAProxy implementation address - function setUEAProxyImplementation(address ueaProxyImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateUEAProxyImplementation(address ueaProxyImplementation) external onlyRole(UEA_ADMIN_ROLE) { if (ueaProxyImplementation == address(0)) { revert UEAErrors.InvalidInputArgs(); } UEA_PROXY_IMPLEMENTATION = ueaProxyImplementation; } - /// @notice Sets the UEA migration contract address. + /// @notice Sets the UEA migration contract address. Only callable by UEA_ADMIN_ROLE. /// @param ueaMigrationContract New migration contract address - function setUEAMigrationContract(address ueaMigrationContract) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateUEAMigrationContract(address ueaMigrationContract) external onlyRole(UEA_ADMIN_ROLE) { if (ueaMigrationContract == address(0)) { revert UEAErrors.InvalidInputArgs(); } UEA_MIGRATION_CONTRACT = ueaMigrationContract; } - /// @notice Update `pushChainId`. Reverts on empty string. - function setPushChainId(string memory _pushChainId) external onlyRole(DEFAULT_ADMIN_ROLE) { + /// @notice Update `pushChainId`. Reverts on empty string. Only callable by UEA_ADMIN_ROLE. + function updatePushChainId(string memory _pushChainId) external onlyRole(UEA_ADMIN_ROLE) { if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); pushChainId = _pushChainId; } /// @inheritdoc IUEAFactory - function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyRole(DEFAULT_ADMIN_ROLE) { + function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyRole(UEA_ADMIN_ROLE) { (, bool isRegistered) = getVMType(_chainHash); if (isRegistered) { revert UEAErrors.InvalidInputArgs(); @@ -250,7 +263,7 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea /// @inheritdoc IUEAFactory function registerMultipleUEA(bytes32[] memory _chainHashes, bytes32[] memory _vmHashes, address[] memory _UEA) external - onlyRole(DEFAULT_ADMIN_ROLE) + onlyRole(UEA_ADMIN_ROLE) { if (_UEA.length != _vmHashes.length || _UEA.length != _chainHashes.length) { revert UEAErrors.InvalidInputArgs(); @@ -262,7 +275,7 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea } /// @inheritdoc IUEAFactory - function registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) public onlyRole(DEFAULT_ADMIN_ROLE) { + function registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) public onlyRole(UEA_ADMIN_ROLE) { _registerUEA(_chainHash, _vmHash, _UEA); } @@ -296,7 +309,7 @@ contract UEAFactory is Initializable, AccessControlUpgradeable, PausableUpgradea /// reconstruct the implementation history. /// @param _vmHash VM hash whose implementation is being updated /// @param _newUEA New UEA implementation address (must be non-zero) - function updateUEAImplementation(bytes32 _vmHash, address _newUEA) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateUEAImplementation(bytes32 _vmHash, address _newUEA) external onlyRole(UEA_ADMIN_ROLE) { if (_newUEA == address(0)) { revert UEAErrors.InvalidInputArgs(); } diff --git a/test/fork/ForkUniversalCore.t.sol b/test/fork/ForkUniversalCore.t.sol index 924e1ea..5339598 100644 --- a/test/fork/ForkUniversalCore.t.sol +++ b/test/fork/ForkUniversalCore.t.sol @@ -63,20 +63,17 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr UniversalCore implementation = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + deployer, + makeAddr("pauser"), WPC_TOKEN, UNISWAP_FACTORY, - UNISWAP_ROUTER, - UNISWAP_QUOTER, - makeAddr("pauser") + UNISWAP_ROUTER ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); universalCore = UniversalCore(payable(proxyAddress)); // Set gateway - universalCore.setUniversalGatewayPC(gateway); - - // Grant MANAGER_ROLE to deployer for config functions - universalCore.grantRole(universalCore.MANAGER_ROLE(), deployer); + universalCore.updateUniversalGatewayPC(gateway); // Configure auto-swap and fee tiers for test tokens _configureToken(PSOL_TOKEN, 500); @@ -102,8 +99,8 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr } function _configureToken(address token, uint24 fee) private { - universalCore.setAutoSwapSupported(token, true); - universalCore.setDefaultFeeTier(token, fee); + universalCore.updateAutoSwapSupported(token, true); + universalCore.updateDefaultFeeTier(token, fee); } function _updatePRC20UniversalCore(address token) private { @@ -160,15 +157,15 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr assertEq(pool, PBNB_WPC_POOL); } - function test_fork_setGasPCPool_validatesRealPool() public { + function test_fork_updateGasPCPool_validatesRealPool() public { vm.startPrank(deployer); // Valid pool succeeds - universalCore.setGasPCPool("eip155:1", PSOL_TOKEN, 500); + universalCore.updateGasPCPool("eip155:1", PSOL_TOKEN, 500); assertEq(universalCore.gasPCPoolByChainNamespace("eip155:1"), PSOL_WPC_POOL); // Nonexistent pool reverts vm.expectRevert(UniversalCoreErrors.PoolNotFound.selector); - universalCore.setGasPCPool("eip155:2", PSOL_TOKEN, 10000); + universalCore.updateGasPCPool("eip155:2", PSOL_TOKEN, 10000); vm.stopPrank(); } @@ -390,7 +387,7 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr address fakeToken = makeAddr("fakeToken"); vm.prank(deployer); - universalCore.setDefaultFeeTier(fakeToken, 500); + universalCore.updateDefaultFeeTier(fakeToken, 500); vm.prank(gateway); vm.expectRevert(UniversalCoreErrors.PoolNotFound.selector); diff --git a/test/fuzz/CEAFactory_Fuzz.t.sol b/test/fuzz/CEAFactory_Fuzz.t.sol index a876301..811e9ce 100644 --- a/test/fuzz/CEAFactory_Fuzz.t.sol +++ b/test/fuzz/CEAFactory_Fuzz.t.sol @@ -194,72 +194,76 @@ contract CEAFactory_FuzzTest is Test { factory.deployCEA(pushAccount); } - /// @dev Non-owner callers cannot call setVault. - function testFuzz_setVault_nonOwner_reverts(address caller, address newVault) public { + /// @dev Non-operator callers cannot call updateVault. + function testFuzz_updateVault_nonOperator_reverts(address caller, address newVault) public { vm.assume(caller != owner); vm.assume(caller != address(0)); vm.assume(newVault != address(0)); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 operatorRole = factory.OPERATOR_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, operatorRole) ); vm.prank(caller); factory.updateVault(newVault); } - /// @dev Non-owner callers cannot call setCEAImplementation. - function testFuzz_setCEAImplementation_nonOwner_reverts(address caller, address newImpl) public { + /// @dev Non-CEA-admin callers cannot call setCEAImplementation. + function testFuzz_setCEAImplementation_nonCEAAdmin_reverts(address caller, address newImpl) public { vm.assume(caller != owner); vm.assume(caller != address(0)); vm.assume(newImpl != address(0)); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, ceaAdminRole) ); vm.prank(caller); factory.setCEAImplementation(newImpl); } - /// @dev Non-owner callers cannot call setCEAMigrationContract. - function testFuzz_setCEAMigrationContract_nonOwner_reverts(address caller, address newMigration) public { + /// @dev Non-CEA-admin callers cannot call updateCEAMigrationContract. + function testFuzz_updateCEAMigrationContract_nonCEAAdmin_reverts(address caller, address newMigration) public { vm.assume(caller != owner); vm.assume(caller != address(0)); vm.assume(newMigration != address(0)); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, ceaAdminRole) ); vm.prank(caller); - factory.setCEAMigrationContract(newMigration); + factory.updateCEAMigrationContract(newMigration); } // ========================================================================= // 9.5 Setter Validation Properties // ========================================================================= - /// @dev setVault(address(0)) reverts with ZeroAddress. - function testFuzz_setVault_zeroAddress_reverts() public { + /// @dev updateVault(address(0)) reverts with ZeroAddress. + function testFuzz_updateVault_zeroAddress_reverts() public { + vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); factory.updateVault(address(0)); } /// @dev setCEAProxyImplementation(address(0)) reverts with ZeroAddress. function testFuzz_setCEAProxyImplementation_zeroAddress_reverts() public { + vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); factory.setCEAProxyImplementation(address(0)); } /// @dev setCEAImplementation(address(0)) reverts with ZeroAddress. function testFuzz_setCEAImplementation_zeroAddress_reverts() public { + vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); factory.setCEAImplementation(address(0)); } - /// @dev setUniversalGateway(address(0)) reverts with ZeroAddress. + /// @dev updateUniversalGateway(address(0)) reverts with ZeroAddress. function testFuzz_setUniversalGateway_zeroAddress_reverts() public { + vm.prank(owner); vm.expectRevert(CEAErrors.ZeroAddress.selector); factory.updateUniversalGateway(address(0)); } diff --git a/test/fuzz/CEAMigration_Fuzz.t.sol b/test/fuzz/CEAMigration_Fuzz.t.sol index 5eebe93..9971c35 100644 --- a/test/fuzz/CEAMigration_Fuzz.t.sol +++ b/test/fuzz/CEAMigration_Fuzz.t.sol @@ -75,7 +75,7 @@ contract CEAMigration_FuzzTest is Test { vm.prank(vault); address ceaAddr = factory.deployCEA(ueaOnPush); - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); // Verify initial slot value (should be ceaV1) bytes32 slotBefore = vm.load(ceaAddr, CEA_LOGIC_SLOT); @@ -98,7 +98,7 @@ contract CEAMigration_FuzzTest is Test { vm.prank(vault); address ceaAddr = factory.deployCEA(ueaOnPush); - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes memory payload = abi.encodePacked(bytes4(keccak256("UEA_MIGRATION"))); bytes32 subTxId = keccak256("migration_event_test"); diff --git a/test/fuzz/PRC20_Fuzz.t.sol b/test/fuzz/PRC20_Fuzz.t.sol index 3bc85f3..37dee4c 100644 --- a/test/fuzz/PRC20_Fuzz.t.sol +++ b/test/fuzz/PRC20_Fuzz.t.sol @@ -22,7 +22,7 @@ contract PRC20_Fuzz is Test, UpgradeableContractHelper { address mockRouter = makeAddr("router"); address mockPauser = makeAddr("pauser"); bytes memory ucInit = abi.encodeWithSelector( - UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, mockPauser + UniversalCore.initialize.selector, address(this), mockPauser, mockWPC, mockFactory, mockRouter ); address ucProxy = deployUpgradeableContract(address(ucImpl), ucInit); universalCore = UniversalCore(payable(ucProxy)); diff --git a/test/fuzz/UEAFactory_Fuzz.t.sol b/test/fuzz/UEAFactory_Fuzz.t.sol index d18f082..6cb67ef 100644 --- a/test/fuzz/UEAFactory_Fuzz.t.sol +++ b/test/fuzz/UEAFactory_Fuzz.t.sol @@ -27,7 +27,7 @@ contract UEAFactory_Fuzz is Test { abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); ueaEVMImpl = new UEA_EVM(); bytes32 evmChainHash = keccak256(abi.encode(CHAIN_NS, CHAIN_ID)); factory.registerNewChain(evmChainHash, EVM_HASH); @@ -179,15 +179,15 @@ contract UEAFactory_Fuzz is Test { // 5.5 Chain Registration Properties // ============================================= - function testFuzz_registerNewChain_nonOwner_reverts(address caller) public { + function testFuzz_registerNewChain_nonUEAAdmin_reverts(address caller) public { vm.assume(caller != address(this)); vm.assume(caller != address(0)); bytes32 chainHash = keccak256(abi.encode("fuzzchain", "999")); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, ueaAdminRole) ); vm.prank(caller); factory.registerNewChain(chainHash, EVM_HASH); @@ -208,17 +208,16 @@ contract UEAFactory_Fuzz is Test { // 5.3 Configurable pushChainId Properties // ============================================= - function testFuzz_setPushChainId_nonAdmin_reverts(address caller) public { + function testFuzz_updatePushChainId_nonUEAAdmin_reverts(address caller) public { vm.assume(caller != address(this)); vm.assume(caller != address(0)); + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, caller, factory.DEFAULT_ADMIN_ROLE() - ) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, ueaAdminRole) ); vm.prank(caller); - factory.setPushChainId("1"); + factory.updatePushChainId("1"); } function testFuzz_getOriginForUEA_fallbackMatchesConfiguredChainId(address addr, string memory chainId) public { @@ -226,7 +225,7 @@ contract UEAFactory_Fuzz is Test { // Ensure addr is not a UEA by using an address that cannot collide with deployed UEAs vm.assume(addr != address(0)); - factory.setPushChainId(chainId); + factory.updatePushChainId(chainId); (UniversalAccountId memory account, bool isUEA) = factory.getOriginForUEA(addr); diff --git a/test/fuzz/UEA_EVM_Fuzz.t.sol b/test/fuzz/UEA_EVM_Fuzz.t.sol index cf2423c..690a6ca 100644 --- a/test/fuzz/UEA_EVM_Fuzz.t.sol +++ b/test/fuzz/UEA_EVM_Fuzz.t.sol @@ -45,7 +45,7 @@ contract UEA_EVM_FuzzTest is Test { abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); ueaEVMImpl = new UEA_EVM(); (owner, ownerPK) = makeAddrAndKey("owner"); @@ -58,7 +58,7 @@ contract UEA_EVM_FuzzTest is Test { ueaEVMImpl2 = new UEA_EVM(); ueaSVMImpl = new UEA_SVM(); migration = new UEAMigration(address(ueaEVMImpl2), address(ueaSVMImpl)); - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); } modifier deployEvmSmartAccount() { diff --git a/test/fuzz/UEA_SVM_Fuzz.t.sol b/test/fuzz/UEA_SVM_Fuzz.t.sol index a4ef47f..e73bfda 100644 --- a/test/fuzz/UEA_SVM_Fuzz.t.sol +++ b/test/fuzz/UEA_SVM_Fuzz.t.sol @@ -42,7 +42,7 @@ contract UEA_SVM_FuzzTest is Test { abi.encodeWithSelector(UEAFactory.initialize.selector, address(this), makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); ueaEVMImpl = new UEA_EVM(); ueaSVMImpl = new UEA_SVM(); diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index ea34791..5efc86a 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -33,15 +33,15 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { UniversalCore impl = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( - UniversalCore.initialize.selector, mockWPC, mockFactory, mockRouter, pauser + UniversalCore.initialize.selector, address(this), pauser, mockWPC, mockFactory, mockRouter ); address proxyAddr = deployUpgradeableContract(address(impl), initData); universalCore = UniversalCore(payable(proxyAddr)); - universalCore.grantRole(universalCore.MANAGER_ROLE(), uExec); + universalCore.grantRole(universalCore.UVCORE_ADMIN_ROLE(), uExec); gateway = makeAddr("gateway"); - universalCore.setUniversalGatewayPC(gateway); + universalCore.updateUniversalGatewayPC(gateway); // Deploy PRC20 with SOURCE_CHAIN_NAMESPACE = "1" (matches CHAIN_NS) PRC20 prc20Impl = new PRC20(); @@ -62,9 +62,9 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { vm.prank(uExec); universalCore.setChainMeta(CHAIN_NS, 50 gwei, 0); - // setGasTokenPRC20 is onlyRole(MANAGER_ROLE), uExec has it + // updateGasTokenPRC20 is onlyRole(UVCORE_ADMIN_ROLE), uExec has it vm.prank(uExec); - universalCore.setGasTokenPRC20(CHAIN_NS, address(gasToken)); + universalCore.updateGasTokenPRC20(CHAIN_NS, address(gasToken)); } // ============================================= @@ -82,7 +82,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setChainMeta(CHAIN_NS, gasPrice, 0); vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, baseLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NS, baseLimit); (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), gasLimit); @@ -97,7 +97,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setChainMeta(CHAIN_NS, gasPrice, 0); vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, baseLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NS, baseLimit); // gasLimitWithBaseLimit == 0 → uses baseLimit (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); @@ -110,7 +110,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { vm.assume(provided > 0 && provided < baseLimit); vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, baseLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NS, baseLimit); vm.expectRevert(abi.encodeWithSelector(UniversalCoreErrors.GasLimitBelowBase.selector, provided, baseLimit)); universalCore.getOutboundTxGasAndFees(address(prc20), provided); @@ -141,8 +141,8 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { // Set gas token and base gas limit, but never call setChainMeta → price stays 0 vm.startPrank(uExec); - universalCore.setGasTokenPRC20(zeroPriceNs, address(gasToken)); - universalCore.setBaseGasLimitByChain(zeroPriceNs, baseLimit); + universalCore.updateGasTokenPRC20(zeroPriceNs, address(gasToken)); + universalCore.updateBaseGasLimitByChain(zeroPriceNs, baseLimit); vm.stopPrank(); vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); @@ -155,7 +155,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { // Set base gas limit for "nogas" chain so we pass the zero-base check vm.prank(uExec); - universalCore.setBaseGasLimitByChain("nogas", baseLimit); + universalCore.updateBaseGasLimitByChain("nogas", baseLimit); // Deploy a fresh PRC20 on chain "nogas" — no gas token configured for "nogas" PRC20 prc20Impl = new PRC20(); @@ -192,7 +192,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.setChainMeta(CHAIN_NS, gasPrice, 0); vm.prank(uExec); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NS, rescueLimit); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NS, rescueLimit); (, uint256 gasFee, uint256 returnedRescueLimit,,) = universalCore.getRescueFundsGasLimit(address(prc20)); @@ -263,17 +263,17 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { // 12.4 Fee Tier Validation Properties // ============================================= - function testFuzz_setDefaultFeeTier_validTiers(address token, uint24 feeTier) public { + function testFuzz_updateDefaultFeeTier_validTiers(address token, uint24 feeTier) public { vm.assume(token != address(0)); bool isValid = feeTier == 100 || feeTier == 500 || feeTier == 3000 || feeTier == 10000; if (isValid) { - universalCore.setDefaultFeeTier(token, feeTier); + universalCore.updateDefaultFeeTier(token, feeTier); assertEq(universalCore.defaultFeeTier(token), feeTier); } else { vm.expectRevert(UniversalCoreErrors.InvalidFeeTier.selector); - universalCore.setDefaultFeeTier(token, feeTier); + universalCore.updateDefaultFeeTier(token, feeTier); } } @@ -281,9 +281,9 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { // 12.5 Deadline Validation Properties // ============================================= - function testFuzz_setDefaultDeadlineMins_storesValue(uint256 mins) public { - // setDefaultDeadlineMins is onlyAdmin — test contract is DEFAULT_ADMIN_ROLE - universalCore.setDefaultDeadlineMins(mins); + function testFuzz_updateDefaultDeadlineMins_storesValue(uint256 mins) public { + // updateDefaultDeadlineMins is onlyAdmin — test contract is DEFAULT_ADMIN_ROLE + universalCore.updateDefaultDeadlineMins(mins); assertEq(universalCore.defaultDeadlineMins(), mins); } @@ -327,27 +327,28 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.swapAndBurnGas{value: 0}(address(gasToken), 3000, 1, 0, address(this)); } - function testFuzz_setProtocolFeeByToken_nonManager_reverts(address caller, address token, uint256 fee) public { + function testFuzz_updateProtocolFeeByToken_nonManager_reverts(address caller, address token, uint256 fee) public { vm.assume(caller != uExec); // Cache role before prank — external calls inside vm.expectRevert would consume the prank - bytes32 managerRole = universalCore.MANAGER_ROLE(); + bytes32 managerRole = universalCore.UVCORE_ADMIN_ROLE(); vm.assume(!universalCore.hasRole(managerRole, caller)); vm.prank(caller); vm.expectRevert( abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, managerRole) ); - universalCore.setProtocolFeeByToken(token, fee); + universalCore.updateProtocolFeeByToken(token, fee); } - function testFuzz_setWPC_nonAdmin_reverts(address caller, address newWPC) public { - // Cache role before prank to avoid consuming prank via external call - bytes32 adminRole = universalCore.DEFAULT_ADMIN_ROLE(); - vm.assume(!universalCore.hasRole(adminRole, caller)); + function testFuzz_updateWPC_nonOperator_reverts(address caller, address newWPC) public { + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); + vm.assume(!universalCore.hasRole(operatorRole, caller)); vm.prank(caller); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setWPC(newWPC); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, caller, operatorRole) + ); + universalCore.updateWPC(newWPC); } function testFuzz_pause_nonPauser_reverts(address caller) public { @@ -375,38 +376,38 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { // 12.7 Setter Zero-Address Validation Properties // ============================================= - function testFuzz_setWPC_zeroAddress_reverts() public { + function testFuzz_updateWPC_zeroAddress_reverts() public { vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setWPC(address(0)); + universalCore.updateWPC(address(0)); } - function testFuzz_setUniversalGatewayPC_zeroAddress_reverts() public { + function testFuzz_updateUniversalGatewayPC_zeroAddress_reverts() public { vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniversalGatewayPC(address(0)); + universalCore.updateUniversalGatewayPC(address(0)); } - function testFuzz_setUniswapV3Addresses_anyZero_reverts(address f, address r) public { + function testFuzz_updateUniswapV3Addresses_anyZero_reverts(address f, address r) public { bool anyZero = f == address(0) || r == address(0); if (anyZero) { vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(f, r); + universalCore.updateUniswapV3Addresses(f, r); } else { // No revert expected — just verify it stores values - universalCore.setUniswapV3Addresses(f, r); + universalCore.updateUniswapV3Addresses(f, r); } } - function testFuzz_setGasTokenPRC20_zeroAddress_reverts(string memory chainNamespace) public { + function testFuzz_updateGasTokenPRC20_zeroAddress_reverts(string memory chainNamespace) public { vm.prank(uExec); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setGasTokenPRC20(chainNamespace, address(0)); + universalCore.updateGasTokenPRC20(chainNamespace, address(0)); } - function testFuzz_setProtocolFeeByToken_zeroToken_reverts(uint256 fee) public { + function testFuzz_updateProtocolFeeByToken_zeroToken_reverts(uint256 fee) public { vm.prank(uExec); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setProtocolFeeByToken(address(0), fee); + universalCore.updateProtocolFeeByToken(address(0), fee); } // ============================================= @@ -421,10 +422,10 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { vm.prank(uExec); universalCore.setChainMeta(CHAIN_NS, 50 gwei, 0); vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, 100_000); + universalCore.updateBaseGasLimitByChain(CHAIN_NS, 100_000); vm.prank(uExec); - universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + universalCore.updateMaxStalenessByChain(CHAIN_NS, maxAge); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NS); vm.warp(uint256(observedAt) + uint256(timePast)); @@ -444,10 +445,10 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { vm.prank(uExec); universalCore.setChainMeta(CHAIN_NS, 50 gwei, 0); vm.prank(uExec); - universalCore.setBaseGasLimitByChain(CHAIN_NS, 100_000); + universalCore.updateBaseGasLimitByChain(CHAIN_NS, 100_000); vm.prank(uExec); - universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + universalCore.updateMaxStalenessByChain(CHAIN_NS, maxAge); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NS); vm.warp(uint256(observedAt) + uint256(timePast)); @@ -456,16 +457,15 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { assertGt(gasFee, 0, "should succeed within the staleness window"); } - function testFuzz_setMaxStalenessByChain_nonManager_reverts(address caller, uint256 maxAge) public { - // Only uExec was granted MANAGER_ROLE in setUp; any other address must revert. + function testFuzz_updateMaxStalenessByChain_nonUCoreAdmin_reverts(address caller, uint256 maxAge) public { vm.assume(caller != uExec); vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, caller, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, caller, universalCore.UVCORE_ADMIN_ROLE() ) ); vm.prank(caller); - universalCore.setMaxStalenessByChain(CHAIN_NS, maxAge); + universalCore.updateMaxStalenessByChain(CHAIN_NS, maxAge); } } diff --git a/test/mocks/MaliciousPRC20.sol b/test/mocks/MaliciousPRC20.sol index d84b107..e34ee09 100644 --- a/test/mocks/MaliciousPRC20.sol +++ b/test/mocks/MaliciousPRC20.sol @@ -10,7 +10,7 @@ contract MaliciousPRC20 { function deposit(address to, uint256 amount) external returns (bool) { // Try to reenter handler with a function that requires admin role - (bool success,) = handler.call(abi.encodeWithSignature("setWPC(address)", address(0x123))); + (bool success,) = handler.call(abi.encodeWithSignature("updateWPC(address)", address(0x123))); if (!success) { revert("Reentry failed"); } diff --git a/test/tests_cea/CEAFactory.t.sol b/test/tests_cea/CEAFactory.t.sol index 7c8e477..09df5d1 100644 --- a/test/tests_cea/CEAFactory.t.sol +++ b/test/tests_cea/CEAFactory.t.sol @@ -13,6 +13,9 @@ import {CEAErrors} from "../../src/libraries/Errors.sol"; import {MockUniversalGateway} from "../mocks/MockUniversalGateway.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ICEAFactory} from "../../src/interfaces/ICEAFactory.sol"; @@ -189,12 +192,12 @@ contract CEAFactoryTest is Test { // Admin Functions - setVault // ========================================================================= - function testSetVaultOnlyOwner() public { + function testUpdateVault_OnlyOperator() public { address newVault = makeAddr("newVault"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 operatorRole = factory.OPERATOR_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); factory.updateVault(newVault); @@ -279,12 +282,12 @@ contract CEAFactoryTest is Test { assertEq(factory.VAULT(), contractAddress, "Vault can be a contract"); } - function testSetCEAProxyImplementationOnlyOwner() public { + function testSetCEAProxyImplementation_OnlyCEAAdmin() public { CEAProxy newProxyImpl = new CEAProxy(); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ceaAdminRole) ); vm.prank(nonOwner); factory.setCEAProxyImplementation(address(newProxyImpl)); @@ -366,12 +369,12 @@ contract CEAFactoryTest is Test { factory.deployCEA(newUEA); } - function testSetCEAImplementationOnlyOwner() public { + function testSetCEAImplementation_OnlyCEAAdmin() public { CEA newCEAImpl = new CEA(); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ceaAdminRole) ); vm.prank(nonOwner); factory.setCEAImplementation(address(newCEAImpl)); @@ -455,12 +458,12 @@ contract CEAFactoryTest is Test { // Admin Functions - setUniversalGateway // ========================================================================= - function testSetUniversalGatewayOnlyOwner() public { + function testUpdateUniversalGateway_OnlyOperator() public { MockUniversalGateway newGateway = new MockUniversalGateway(); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 operatorRole = factory.OPERATOR_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); factory.updateUniversalGateway(address(newGateway)); @@ -1218,7 +1221,7 @@ contract CEAFactoryTest is Test { assertTrue(hasCode(cea), "Should deploy with new proxy implementation"); } - function testUpdateCEAImplementationBeforeDeployment() public { + function testsetCEAImplementationBeforeDeployment() public { CEA newCEAImpl = new CEA(); vm.prank(owner); factory.setCEAImplementation(address(newCEAImpl)); @@ -1260,7 +1263,9 @@ contract CEAFactoryTest is Test { // UNIVERSAL_GATEWAY() delegates to the factory, so all existing CEAs immediately // reflect the factory's current value — there is no per-CEA stored copy. - assertEq(ceaInstance.UNIVERSAL_GATEWAY(), address(newGateway), "Existing CEA should see new gateway via factory"); + assertEq( + ceaInstance.UNIVERSAL_GATEWAY(), address(newGateway), "Existing CEA should see new gateway via factory" + ); } // ========================================================================= @@ -1269,18 +1274,18 @@ contract CEAFactoryTest is Test { function testPreventUnauthorizedVaultChange() public { address newVault = makeAddr("newVault"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 operatorRole = factory.OPERATOR_ROLE(); // Vault cannot change itself vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, operatorRole) ); vm.prank(vault); factory.updateVault(newVault); // Non-owner cannot change vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); factory.updateVault(newVault); @@ -1288,31 +1293,29 @@ contract CEAFactoryTest is Test { function testPreventUnauthorizedImplementationChange() public { CEA newImpl = new CEA(); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); // Vault cannot change vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, ceaAdminRole) ); vm.prank(vault); factory.setCEAImplementation(address(newImpl)); // Non-owner cannot change vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ceaAdminRole) ); vm.prank(nonOwner); factory.setCEAImplementation(address(newImpl)); } function testPreventVaultFromChangingOwner() public { - // Vault cannot grant DEFAULT_ADMIN_ROLE (only admin can) + // With ADR, grantRole(DEFAULT_ADMIN_ROLE) always reverts address newOwner = makeAddr("newOwner"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); - vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, vault, adminRole) - ); + + vm.expectRevert(IAccessControlDefaultAdminRules.AccessControlEnforcedDefaultAdminRules.selector); vm.prank(vault); factory.grantRole(adminRole, newOwner); } @@ -1403,24 +1406,36 @@ contract CEAFactoryTest is Test { assertTrue(factory.paused()); } - function testUnpause_OnlyPauser() public { - bytes32 role = factory.PAUSER_ROLE(); + function testUnpause_OnlyOperator() public { + bytes32 operatorRole = factory.OPERATOR_ROLE(); vm.prank(pauser); factory.pause(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, role) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); factory.unpause(); } + function testUnpause_PauserCannotUnpause() public { + bytes32 operatorRole = factory.OPERATOR_ROLE(); + vm.prank(pauser); + factory.pause(); + + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, pauser, operatorRole) + ); + vm.prank(pauser); + factory.unpause(); + } + function testUnpause_HappyPath() public { vm.prank(pauser); factory.pause(); assertTrue(factory.paused()); - vm.prank(pauser); + vm.prank(owner); factory.unpause(); assertFalse(factory.paused()); } @@ -1437,7 +1452,7 @@ contract CEAFactoryTest is Test { function testDeployCEA_AfterUnpause_Works() public { vm.prank(pauser); factory.pause(); - vm.prank(pauser); + vm.prank(owner); factory.unpause(); address uea = makeAddr("unpausedUEA"); @@ -1446,13 +1461,13 @@ contract CEAFactoryTest is Test { assertTrue(factory.isCEA(cea)); } - function testGrantPauserRole_OnlyAdmin() public { + function testGrantPauserRole_OnlyRoleManager() public { address newPauser = makeAddr("newPauser"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 roleManagerRole = factory.ROLE_MANAGER_ROLE(); bytes32 pauserRole = factory.PAUSER_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, roleManagerRole) ); vm.prank(nonOwner); factory.grantRole(pauserRole, newPauser); @@ -1487,6 +1502,95 @@ contract CEAFactoryTest is Test { ); } + // ========================================================================= + // ADR & Role Hierarchy Tests + // ========================================================================= + + function testInitialize_SetsRoleAdmins() public { + assertEq(factory.getRoleAdmin(factory.CEA_ADMIN_ROLE()), factory.ROLE_MANAGER_ROLE()); + assertEq(factory.getRoleAdmin(factory.OPERATOR_ROLE()), factory.ROLE_MANAGER_ROLE()); + assertEq(factory.getRoleAdmin(factory.PAUSER_ROLE()), factory.ROLE_MANAGER_ROLE()); + assertEq(factory.getRoleAdmin(factory.ROLE_MANAGER_ROLE()), factory.DEFAULT_ADMIN_ROLE()); + } + + function testInitialize_GrantsAllRolesToAdmin() public { + assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(factory.hasRole(factory.ROLE_MANAGER_ROLE(), owner)); + assertTrue(factory.hasRole(factory.CEA_ADMIN_ROLE(), owner)); + assertTrue(factory.hasRole(factory.OPERATOR_ROLE(), owner)); + } + + function testGrantRole_DEFAULT_ADMIN_ROLE_Reverts() public { + address newAdmin = makeAddr("newAdmin"); + bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + vm.expectRevert(IAccessControlDefaultAdminRules.AccessControlEnforcedDefaultAdminRules.selector); + vm.prank(owner); + factory.grantRole(adminRole, newAdmin); + } + + function testBeginDefaultAdminTransfer_HappyPath() public { + address newAdmin = makeAddr("newAdmin"); + + vm.prank(owner); + factory.beginDefaultAdminTransfer(newAdmin); + + (address pending,) = factory.pendingDefaultAdmin(); + assertEq(pending, newAdmin); + + vm.warp(block.timestamp + 1 days + 1); + + vm.prank(newAdmin); + factory.acceptDefaultAdminTransfer(); + + assertEq(factory.defaultAdmin(), newAdmin); + assertFalse(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), newAdmin)); + } + + function testDefaultAdminDelay_Is2Days() public view { + assertEq(factory.defaultAdminDelay(), 1 days); + } + + function testOwner_ReturnsDefaultAdmin() public view { + assertEq(factory.owner(), owner); + } + + function testRoleManager_CanGrantOperator() public { + address newOperator = makeAddr("newOperator"); + bytes32 operatorRole = factory.OPERATOR_ROLE(); + vm.prank(owner); + factory.grantRole(operatorRole, newOperator); + assertTrue(factory.hasRole(operatorRole, newOperator)); + } + + function testRoleManager_CanGrantCEAAdmin() public { + address newCEAAdmin = makeAddr("newCEAAdmin"); + bytes32 ceaAdminRole = factory.CEA_ADMIN_ROLE(); + vm.prank(owner); + factory.grantRole(ceaAdminRole, newCEAAdmin); + assertTrue(factory.hasRole(ceaAdminRole, newCEAAdmin)); + } + + function testRoleManager_CannotBeGrantedByNonAdmin() public { + address attacker = makeAddr("attacker"); + bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 roleManagerRole = factory.ROLE_MANAGER_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, attacker, adminRole) + ); + vm.prank(attacker); + factory.grantRole(roleManagerRole, attacker); + } + + function testPause_OperatorCannotPause() public { + bytes32 pauserRole = factory.PAUSER_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, owner, pauserRole) + ); + vm.prank(owner); + factory.pause(); + } + // ========================================================================= // Helper Functions // ========================================================================= diff --git a/test/tests_cea/CEA_singleCall.t.sol b/test/tests_cea/CEA_singleCall.t.sol index 922c178..d4e6b4b 100644 --- a/test/tests_cea/CEA_singleCall.t.sol +++ b/test/tests_cea/CEA_singleCall.t.sol @@ -245,7 +245,7 @@ contract CEA_SingleCallTests is CEATest { // Set up migration contract CEA ceaV2 = new CEA(); CEAMigration migration = new CEAMigration(address(ceaV2)); - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -263,7 +263,7 @@ contract CEA_SingleCallTests is CEATest { // Set up migration contract CEA ceaV2 = new CEA(); CEAMigration migration = new CEAMigration(address(ceaV2)); - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); diff --git a/test/tests_ceaMigration/CEAFactory_Migration.t.sol b/test/tests_ceaMigration/CEAFactory_Migration.t.sol index f301768..a5f6eb4 100644 --- a/test/tests_ceaMigration/CEAFactory_Migration.t.sol +++ b/test/tests_ceaMigration/CEAFactory_Migration.t.sol @@ -57,42 +57,42 @@ contract CEAFactory_MigrationTest is Test { } // ========================================================================= - // setCEAMigrationContract Tests + // updateCEAMigrationContract Tests // ========================================================================= - function test_setCEAMigrationContract_Success() public { + function test_updateCEAMigrationContract_Success() public { // Initially should be zero assertEq(factory.CEA_MIGRATION_CONTRACT(), address(0), "Migration contract should be zero initially"); // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); // Verify set correctly assertEq(factory.CEA_MIGRATION_CONTRACT(), address(migration), "Migration contract should be set"); } - function test_setCEAMigrationContract_ZeroAddress() public { + function test_updateCEAMigrationContract_ZeroAddress() public { vm.expectRevert(abi.encodeWithSelector(CEAErrors.ZeroAddress.selector)); - factory.setCEAMigrationContract(address(0)); + factory.updateCEAMigrationContract(address(0)); } - function test_setCEAMigrationContract_NonOwner() public { + function test_updateCEAMigrationContract_NonOwner() public { vm.prank(nonOwner); vm.expectRevert(); // OwnableUnauthorizedAccount - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); } - function test_setCEAMigrationContract_Event() public { + function test_updateCEAMigrationContract_Event() public { // Expect CEAMigrationContractUpdated event vm.expectEmit(true, true, false, false); emit ICEAFactory.CEAMigrationContractUpdated(address(0), address(migration)); - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); } - function test_setCEAMigrationContract_UpdateExisting() public { + function test_updateCEAMigrationContract_UpdateExisting() public { // Set initial migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); // Deploy new migration contract CEA ceaV3 = new CEA(); @@ -103,7 +103,7 @@ contract CEAFactory_MigrationTest is Test { emit ICEAFactory.CEAMigrationContractUpdated(address(migration), address(migration2)); // Update migration contract - factory.setCEAMigrationContract(address(migration2)); + factory.updateCEAMigrationContract(address(migration2)); // Verify updated assertEq(factory.CEA_MIGRATION_CONTRACT(), address(migration2), "Migration contract should be updated"); @@ -129,7 +129,7 @@ contract CEAFactory_MigrationTest is Test { address ueaOnPush = makeAddr("ueaOnPush"); // Set migration contract in factory - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); // Deploy CEA vm.prank(vault); diff --git a/test/tests_ceaMigration/CEAMigration_Integration.t.sol b/test/tests_ceaMigration/CEAMigration_Integration.t.sol index c20cdf2..f1ae26e 100644 --- a/test/tests_ceaMigration/CEAMigration_Integration.t.sol +++ b/test/tests_ceaMigration/CEAMigration_Integration.t.sol @@ -72,7 +72,7 @@ contract CEAMigration_IntegrationTest is Test { migration = new CEAMigration(address(ceaV2Implementation)); // Set migration contract in factory - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); } // ========================================================================= @@ -347,7 +347,7 @@ contract CEAMigration_IntegrationTest is Test { // Deploy v3 and new migration contract CEA ceaV3Implementation = new CEA(); CEAMigration migration2 = new CEAMigration(address(ceaV3Implementation)); - factory.setCEAMigrationContract(address(migration2)); + factory.updateCEAMigrationContract(address(migration2)); // Migration 2: v2 → v3 bytes32 subTxId = generateTxID(1000); diff --git a/test/tests_ceaMigration/CEA_Migration.t.sol b/test/tests_ceaMigration/CEA_Migration.t.sol index 5c002d0..04051d5 100644 --- a/test/tests_ceaMigration/CEA_Migration.t.sol +++ b/test/tests_ceaMigration/CEA_Migration.t.sol @@ -106,7 +106,7 @@ contract CEA_MigrationTest is Test { function test_isMigration_True() public { // Set migration contract in factory - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -128,7 +128,7 @@ contract CEA_MigrationTest is Test { function test_handleMigration_TopLevelFormat_Succeeds() public { // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -145,7 +145,7 @@ contract CEA_MigrationTest is Test { } function test_handleMigration_NonZeroMsgValue_Reverts() public { - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -160,7 +160,7 @@ contract CEA_MigrationTest is Test { function test_handleMigration_MigrationInsideMulticall_Reverts() public { // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -197,7 +197,7 @@ contract CEA_MigrationTest is Test { function test_handleMulticall_MigrationInBatch() public { // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -220,7 +220,7 @@ contract CEA_MigrationTest is Test { function test_handleMulticall_MigrationInBatch_FirstPosition() public { // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -247,7 +247,7 @@ contract CEA_MigrationTest is Test { function test_handleExecution_StandaloneMigration() public { // Set migration contract - factory.setCEAMigrationContract(address(migration)); + factory.updateCEAMigrationContract(address(migration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); @@ -275,7 +275,7 @@ contract CEA_MigrationTest is Test { // We need a contract whose migrateCEA() will fail when delegatecalled. // Use a mock that reverts on migrateCEA(). FailingMigration failMigration = new FailingMigration(); - factory.setCEAMigrationContract(address(failMigration)); + factory.updateCEAMigrationContract(address(failMigration)); bytes32 subTxId = generateTxID(1); bytes32 universalTxID = generateUniversalTxID(1); diff --git a/test/tests_token_and_core/ForkUniversalCoreAMM.t.sol b/test/tests_token_and_core/ForkUniversalCoreAMM.t.sol index 30cdc39..67a875b 100644 --- a/test/tests_token_and_core/ForkUniversalCoreAMM.t.sol +++ b/test/tests_token_and_core/ForkUniversalCoreAMM.t.sol @@ -125,8 +125,8 @@ // // Setup auto-swap for PSOL // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // universalCore.setSlippageTolerance(PSOL_TOKEN, 300); // 3% // vm.stopPrank(); @@ -193,8 +193,8 @@ // function test_DepositPRC20WithAutoSwap_PETHToWPC() public { // // Setup auto-swap for PETH // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PETH_TOKEN, true); -// universalCore.setDefaultFeeTier(PETH_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PETH_TOKEN, true); +// universalCore.updateDefaultFeeTier(PETH_TOKEN, 500); // universalCore.setSlippageTolerance(PETH_TOKEN, 300); // 3% // vm.stopPrank(); @@ -250,8 +250,8 @@ // function test_DepositPRC20WithAutoSwap_USDTToWPC() public { // // Setup auto-swap for USDT // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(USDT_TOKEN, true); -// universalCore.setDefaultFeeTier(USDT_TOKEN, 500); +// universalCore.updateAutoSwapSupported(USDT_TOKEN, true); +// universalCore.updateDefaultFeeTier(USDT_TOKEN, 500); // universalCore.setSlippageTolerance(USDT_TOKEN, 500); // 5% // vm.stopPrank(); @@ -345,7 +345,7 @@ // // Enable auto-swap but don't set fee tier // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); // vm.stopPrank(); // // Verify auto-swap is enabled but fee tier is not set @@ -379,8 +379,8 @@ // // Setup auto-swap // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // // Verify configuration was set @@ -423,8 +423,8 @@ // // Setup auto-swap // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // // Pause the contract // universalCore.pause(); @@ -626,8 +626,8 @@ // function test_DepositPRC20WithAutoSwap_MinPCOutZero_UsesQuoter() public { // // Test when minPCOut=0, should go through quoter route // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // universalCore.setSlippageTolerance(PSOL_TOKEN, 300); // 3% // vm.stopPrank(); @@ -686,8 +686,8 @@ // function test_DepositPRC20WithAutoSwap_MinPCOutProvided_BypassesQuoter() public { // // Test when minPCOut>0, should bypass quoter // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 amount = 1e18; // 1 PSOL @@ -749,8 +749,8 @@ // function test_DepositPRC20WithAutoSwap_FeeZero_UsesDefault() public { // // Test when fee=0, should use default fee tier // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 amount = 1e18; @@ -812,8 +812,8 @@ // function test_DepositPRC20WithAutoSwap_FeeProvided_UsesProvided() public { // // Test when fee>0, should use provided fee (default set to 0.3% but we pass 0.05%) // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 3000); // Set default to 0.3% pool +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 3000); // Set default to 0.3% pool // vm.stopPrank(); // uint256 amount = 1e18; @@ -862,9 +862,9 @@ // function test_DepositPRC20WithAutoSwap_DeadlineZero_UsesDefault() public { // // Test when deadline=0, should use default deadline // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); -// universalCore.setDefaultDeadlineMins(30); // Set default to 30 minutes +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateDefaultDeadlineMins(30); // Set default to 30 minutes // vm.stopPrank(); // uint256 amount = 1e18; @@ -918,8 +918,8 @@ // function test_DepositPRC20WithAutoSwap_DeadlineProvided_UsesProvided() public { // // Test when deadline>0, should use provided deadline // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 amount = 1e18; @@ -979,8 +979,8 @@ // function test_CalculateMinOutput_SlippageZero_UsesDefault() public { // // Test calculateMinOutput when slippage tolerance=0, should use default 3% // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // // Don't set slippage tolerance (should default to 300 = 3%) // vm.stopPrank(); @@ -1035,8 +1035,8 @@ // function test_CalculateMinOutput_SlippageSet_UsesSet() public { // // Test calculateMinOutput when slippage tolerance is set // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // universalCore.setSlippageTolerance(PSOL_TOKEN, 500); // 5% // vm.stopPrank(); @@ -1092,8 +1092,8 @@ // function test_GetSwapQuote_QuoterV2Integration() public { // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 quote = universalCore.getSwapQuote(PSOL_TOKEN, WPC_TOKEN, 500, 1e18); @@ -1106,13 +1106,13 @@ // vm.startPrank(deployer); // vm.expectRevert(CommonErrors.ZeroAddress.selector); -// universalCore.setUniswapV3Addresses(address(0), address(1), address(1)); +// universalCore.updateUniswapV3Addresses(address(0), address(1), address(1)); // vm.expectRevert(CommonErrors.ZeroAddress.selector); -// universalCore.setUniswapV3Addresses(address(1), address(0), address(1)); +// universalCore.updateUniswapV3Addresses(address(1), address(0), address(1)); // vm.expectRevert(CommonErrors.ZeroAddress.selector); -// universalCore.setUniswapV3Addresses(address(1), address(1), address(0)); +// universalCore.updateUniswapV3Addresses(address(1), address(1), address(0)); // vm.stopPrank(); // } @@ -1123,7 +1123,7 @@ // address newQuoter = address(0x789); // vm.prank(deployer); -// universalCore.setUniswapV3Addresses(newFactory, newRouter, newQuoter); +// universalCore.updateUniswapV3Addresses(newFactory, newRouter, newQuoter); // assertEq(universalCore.uniswapV3FactoryAddress(), newFactory); // assertEq(universalCore.uniswapV3SwapRouterAddress(), newRouter); @@ -1133,14 +1133,14 @@ // function test_SetDefaultFeeTier_InvalidFeeTierReverts() public { // vm.prank(deployer); // vm.expectRevert(UniversalCoreErrors.InvalidFeeTier.selector); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 999); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 999); // } // function test_DeadlineExpired_Reverts() public { // // Test when deadline has already passed // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 amount = 1e18; @@ -1163,8 +1163,8 @@ // function test_PoolNotFound_Reverts() public { // // Test when pool doesn't exist for given fee tier // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // uint256 amount = 1e18; @@ -1185,8 +1185,8 @@ // function test_GetSwapQuote_QuoterV2ReturnsZero_ReturnsEstimate() public { // vm.startPrank(deployer); -// universalCore.setAutoSwapSupported(PSOL_TOKEN, true); -// universalCore.setDefaultFeeTier(PSOL_TOKEN, 500); +// universalCore.updateAutoSwapSupported(PSOL_TOKEN, true); +// universalCore.updateDefaultFeeTier(PSOL_TOKEN, 500); // vm.stopPrank(); // // Use a tiny amount to produce 0 output due to tick spacing diff --git a/test/tests_token_and_core/PRC20.t.sol b/test/tests_token_and_core/PRC20.t.sol index b4ba46b..7a4da00 100644 --- a/test/tests_token_and_core/PRC20.t.sol +++ b/test/tests_token_and_core/PRC20.t.sol @@ -67,23 +67,24 @@ contract PRC20Test is Test, UpgradeableContractHelper { // Create initialization data bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + address(this), + makeAddr("pauser"), mockWPC, mockUniswapFactory, - mockUniswapRouter, - makeAddr("pauser") + mockUniswapRouter ); // Deploy proxy and initialize address proxyAddress = deployUpgradeableContract(address(universalCoreImplementation), initData); universalCore = UniversalCore(payable(proxyAddress)); - // Grant MANAGER_ROLE to uExec so manager functions (e.g. setGasTokenPRC20) are callable - universalCore.grantRole(universalCore.MANAGER_ROLE(), uExec); + // Grant UVCORE_ADMIN_ROLE to uExec so config functions are callable + universalCore.grantRole(universalCore.UVCORE_ADMIN_ROLE(), uExec); // Configure universalCore vm.startPrank(uExec); universalCore.setChainMeta(SOURCE_CHAIN_NAMESPACE, GAS_PRICE, 0); - universalCore.setGasTokenPRC20(SOURCE_CHAIN_NAMESPACE, address(gasToken)); + universalCore.updateGasTokenPRC20(SOURCE_CHAIN_NAMESPACE, address(gasToken)); vm.stopPrank(); // Deploy PRC20 token implementation @@ -497,10 +498,10 @@ contract PRC20Test is Test, UpgradeableContractHelper { } function testDepositSucceedsAfterUnpause() public { - // Pause then unpause + // Pause then unpause: pauser has PAUSER_ROLE (can pause), admin/operator has OPERATOR_ROLE (can unpause) vm.prank(makeAddr("pauser")); universalCore.pause(); - vm.prank(makeAddr("pauser")); + // address(this) is the admin and holds OPERATOR_ROLE — only OPERATOR_ROLE can unpause universalCore.unpause(); // Deposit should succeed @@ -523,20 +524,18 @@ contract PRC20Test is Test, UpgradeableContractHelper { // Deploy new universalCore implementation UniversalCore newHandlerImpl = new UniversalCore(); - // Create initialization data bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + address(this), + makeAddr("pauser"), mockWPC, mockUniswapFactory, - mockUniswapRouter, - makeAddr("pauser") + mockUniswapRouter ); - // Deploy proxy and initialize address proxyAddress = deployUpgradeableContract(address(newHandlerImpl), initData); UniversalCore newHandler = UniversalCore(payable(proxyAddress)); - // Update universalCore contract from Universal Executor Module vm.prank(uExec); vm.expectEmit(false, false, false, true); @@ -558,16 +557,15 @@ contract PRC20Test is Test, UpgradeableContractHelper { // Deploy new universalCore implementation UniversalCore newHandlerImpl = new UniversalCore(); - // Create initialization data bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + address(this), + makeAddr("pauser"), mockWPC, mockUniswapFactory, - mockUniswapRouter, - makeAddr("pauser") + mockUniswapRouter ); - // Deploy proxy and initialize address proxyAddress = deployUpgradeableContract(address(newHandlerImpl), initData); UniversalCore newHandler = UniversalCore(payable(proxyAddress)); diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 8794f78..4ed393f 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -18,8 +18,10 @@ import "../../test/mocks/FalseReturningPRC20.sol"; import "../../test/mocks/RevertingTarget.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; contract UniversalCoreTest is Test, UpgradeableContractHelper { UniversalCore public universalCore; @@ -106,10 +108,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Deploy proxy and initialize bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + deployer, + pauser, address(mockWPC), address(mockFactory), - address(mockRouter), - pauser + address(mockRouter) ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); @@ -123,16 +126,16 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address pool = makeAddr("mockPool"); mockFactory.setPool(address(mockWPC), address(prc20Token), FEE_TIER, pool); - // Grant MANAGER_ROLE to UE Module for manager functions - universalCore.grantRole(universalCore.MANAGER_ROLE(), UNIVERSAL_EXECUTOR_MODULE); + // Grant UVCORE_ADMIN_ROLE to UE Module for config functions + universalCore.grantRole(universalCore.UVCORE_ADMIN_ROLE(), UNIVERSAL_EXECUTOR_MODULE); - // Configure gas token first, then gas price (setGasTokenPRC20 resets gas price to 0, + // Configure gas token first, then gas price (updateGasTokenPRC20 resets gas price to 0, // so setChainMeta must come after to preserve the configured gas price). vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(mockPRC20)); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, address(mockPRC20)); universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, BASE_GAS_LIMIT); - universalCore.setProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, BASE_GAS_LIMIT); + universalCore.updateProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); vm.stopPrank(); } @@ -144,30 +147,32 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { UniversalCore newHandler = new UniversalCore(); // Should not be able to call initialize on implementation directly vm.expectRevert(Initializable.InvalidInitialization.selector); - newHandler.initialize(address(mockWPC), address(mockFactory), address(mockRouter), pauser); + newHandler.initialize(deployer, pauser, address(mockWPC), address(mockFactory), address(mockRouter)); } - function test_Initialize_GrantsAdminRoleToDeployer() public { - // Deploy new universalCore with different deployer - address newDeployer = makeAddr("newDeployer"); - vm.startPrank(newDeployer); + function test_Initialize_GrantsAdminRoleToAdmin() public { + address admin = makeAddr("newAdmin"); + address newPauser = makeAddr("newPauser"); UniversalCore newImplementation = new UniversalCore(); - address newPauser = makeAddr("newPauser"); bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + admin, + newPauser, address(mockWPC), address(mockFactory), - address(mockRouter), - newPauser + address(mockRouter) ); address newProxyAddress = deployUpgradeableContract(address(newImplementation), initData); UniversalCore newHandler = UniversalCore(payable(newProxyAddress)); - // Check that deployer has admin role - assertTrue(newHandler.hasRole(newHandler.DEFAULT_ADMIN_ROLE(), newDeployer)); - vm.stopPrank(); + assertTrue(newHandler.hasRole(newHandler.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(newHandler.hasRole(newHandler.ROLE_MANAGER_ROLE(), admin)); + assertTrue(newHandler.hasRole(newHandler.UVCORE_ADMIN_ROLE(), admin)); + assertTrue(newHandler.hasRole(newHandler.OPERATOR_ROLE(), admin)); + assertTrue(newHandler.hasRole(newHandler.PAUSER_ROLE(), newPauser)); + assertFalse(newHandler.hasRole(newHandler.PAUSER_ROLE(), admin)); } function test_Initialize_SetsAddresses() public view { @@ -178,9 +183,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_Initialize_RevertsOnSecondCall() public { vm.expectRevert(Initializable.InvalidInitialization.selector); - universalCore.initialize( - address(mockWPC), address(mockFactory), address(mockRouter), pauser - ); + universalCore.initialize(deployer, pauser, address(mockWPC), address(mockFactory), address(mockRouter)); } function test_UniversalExecutorModule_IsImmutable() public view { @@ -198,17 +201,19 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // 1) Admin-specific (DEFAULT_ADMIN_ROLE) setters // ======================================== - function test_SetAutoSwapSupported_OnlyOwner() public { + function test_SetAutoSwapSupported_OnlyUCoreAdmin() public { address token = makeAddr("token"); - // Non-owner should revert + bytes32 ucoreAdminRole = universalCore.UVCORE_ADMIN_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setAutoSwapSupported(token, true); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ucoreAdminRole) + ); + universalCore.updateAutoSwapSupported(token, true); - // Deployer (who has admin role) should succeed + // Deployer (who has UVCORE_ADMIN_ROLE) should succeed vm.prank(deployer); - universalCore.setAutoSwapSupported(token, true); + universalCore.updateAutoSwapSupported(token, true); assertTrue(universalCore.isAutoSwapSupported(token)); } @@ -218,35 +223,37 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(true, false, false, true); emit SetAutoSwapSupported(token, true); - universalCore.setAutoSwapSupported(token, true); + universalCore.updateAutoSwapSupported(token, true); assertTrue(universalCore.isAutoSwapSupported(token)); // Test flipping to false vm.prank(deployer); vm.expectEmit(true, false, false, true); emit SetAutoSwapSupported(token, false); - universalCore.setAutoSwapSupported(token, false); + universalCore.updateAutoSwapSupported(token, false); assertFalse(universalCore.isAutoSwapSupported(token)); } function test_SetAutoSwapSupported_ZeroAddressAllowed() public { // Current implementation allows zero address vm.prank(deployer); - universalCore.setAutoSwapSupported(address(0), true); + universalCore.updateAutoSwapSupported(address(0), true); assertTrue(universalCore.isAutoSwapSupported(address(0))); } - function test_SetWPCContractAddress_OnlyOwner() public { + function test_SetWPCContractAddress_OnlyOperator() public { address newWPC = makeAddr("newWPC"); - // Non-owner should revert + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setWPC(newWPC); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) + ); + universalCore.updateWPC(newWPC); - // Deployer (who has admin role) should succeed + // Deployer (who has OPERATOR_ROLE) should succeed vm.prank(deployer); - universalCore.setWPC(newWPC); + universalCore.updateWPC(newWPC); assertEq(universalCore.WPC(), newWPC); } @@ -257,7 +264,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(true, true, false, true); emit SetWPC(oldWPC, newWPC); - universalCore.setWPC(newWPC); + universalCore.updateWPC(newWPC); assertEq(universalCore.WPC(), newWPC); } @@ -265,7 +272,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetWPCContractAddress_ZeroAddressReverts() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setWPC(address(0)); + universalCore.updateWPC(address(0)); } // ======================================== @@ -287,21 +294,21 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.startPrank(nonUEModule); vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonUEModule, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonUEModule, universalCore.UVCORE_ADMIN_ROLE() ) ); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); vm.stopPrank(); // UEM should succeed vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); } function test_SetGasPCPool_ZeroAddressReverts() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setGasPCPool(CHAIN_NAMESPACE, address(0), FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, address(0), FEE_TIER); } function test_SetGasPCPool_PoolNotFoundReverts() public { @@ -312,7 +319,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectRevert(UniversalCoreErrors.PoolNotFound.selector); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); } function test_SetGasPCPool_HappyPath() public { @@ -327,7 +334,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); assertEq(universalCore.gasPCPoolByChainNamespace(CHAIN_NAMESPACE), pool); } @@ -344,7 +351,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); assertEq(universalCore.gasPCPoolByChainNamespace(CHAIN_NAMESPACE), pool); } @@ -356,7 +363,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Change WPC first vm.prank(deployer); - universalCore.setWPC(newWPC); + universalCore.updateWPC(newWPC); // Setup pool with new WPC (both orderings) if (newWPC < gasToken) { @@ -366,7 +373,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); + universalCore.updateGasPCPool(CHAIN_NAMESPACE, gasToken, FEE_TIER); assertEq(universalCore.gasPCPoolByChainNamespace(CHAIN_NAMESPACE), pool); } @@ -378,29 +385,29 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.startPrank(nonUEModule); vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonUEModule, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonUEModule, universalCore.UVCORE_ADMIN_ROLE() ) ); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, prc20); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, prc20); vm.stopPrank(); // UEM should succeed vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, prc20); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, prc20); assertEq(universalCore.gasTokenPRC20ByChainNamespace(CHAIN_NAMESPACE), prc20); } function test_SetGasTokenPRC20_ZeroAddressReverts() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(0)); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, address(0)); } function test_SetGasTokenPRC20_HappyPath() public { address prc20 = makeAddr("prc20"); vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, prc20); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, prc20); assertEq(universalCore.gasTokenPRC20ByChainNamespace(CHAIN_NAMESPACE), prc20); } @@ -526,31 +533,35 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertTrue(universalCore.paused()); } - function test_Unpause_OnlyPauser() public { - bytes32 role = universalCore.PAUSER_ROLE(); + function test_Unpause_OnlyOperator() public { + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); - // First pause the contract vm.prank(pauser); universalCore.pause(); - // Non-pauser cannot unpause + // Non-operator cannot unpause vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, role) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); universalCore.unpause(); + + // Pauser also cannot unpause (different role) + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, pauser, operatorRole) + ); + vm.prank(pauser); + universalCore.unpause(); } function test_Unpause_HappyPath() public { - // First pause the contract vm.prank(pauser); universalCore.pause(); assertTrue(universalCore.paused()); - // Unpause - vm.prank(pauser); + // Deployer has OPERATOR_ROLE vm.expectEmit(true, true, true, true); - emit Unpaused(pauser); + emit Unpaused(deployer); universalCore.unpause(); assertFalse(universalCore.paused()); @@ -564,19 +575,20 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertFalse(universalCore.hasRole(universalCore.PAUSER_ROLE(), deployer)); } - function test_GrantPauserRole_OnlyAdmin() public { + function test_GrantPauserRole_OnlyRoleManager() public { address newPauser = makeAddr("newPauser"); - bytes32 role = universalCore.PAUSER_ROLE(); + bytes32 pauserRole = universalCore.PAUSER_ROLE(); + bytes32 roleManagerRole = universalCore.ROLE_MANAGER_ROLE(); vm.prank(nonOwner); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, bytes32(0)) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, roleManagerRole) ); - universalCore.grantRole(role, newPauser); + universalCore.grantRole(pauserRole, newPauser); - vm.prank(deployer); - universalCore.grantRole(role, newPauser); - assertTrue(universalCore.hasRole(role, newPauser)); + // Deployer has ROLE_MANAGER_ROLE + universalCore.grantRole(pauserRole, newPauser); + assertTrue(universalCore.hasRole(pauserRole, newPauser)); } function test_GrantPauserRole_NewPauserCanPause() public { @@ -604,7 +616,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_DepositPRC20WithAutoSwap_WhenPaused_Reverts() public { // Setup auto-swap support vm.prank(deployer); - universalCore.setAutoSwapSupported(address(mockPRC20), true); + universalCore.updateAutoSwapSupported(address(mockPRC20), true); // Pause the contract vm.prank(pauser); @@ -617,12 +629,10 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } function test_DepositPRC20Token_AfterUnpause_Works() public { - // Pause the contract vm.prank(pauser); universalCore.pause(); - // Unpause the contract - vm.prank(pauser); + // Deployer has OPERATOR_ROLE universalCore.unpause(); // Now deposit should work @@ -695,8 +705,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Set gas token and base gas limit, but never call setChainMeta → price stays 0 vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(newNs, address(mockPRC20)); - universalCore.setBaseGasLimitByChain(newNs, BASE_GAS_LIMIT); + universalCore.updateGasTokenPRC20(newNs, address(mockPRC20)); + universalCore.updateBaseGasLimitByChain(newNs, BASE_GAS_LIMIT); vm.stopPrank(); vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); @@ -747,7 +757,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 newBaseGasLimit = BASE_GAS_LIMIT * 2; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, newBaseGasLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, newBaseGasLimit); (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); @@ -760,7 +770,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 newProtocolFee = PROTOCOL_FEE * 2; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setProtocolFeeByToken(address(prc20Token), newProtocolFee); + universalCore.updateProtocolFeeByToken(address(prc20Token), newProtocolFee); (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); @@ -780,7 +790,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectEmit(false, false, false, true); emit SetBaseGasLimitByChain(CHAIN_NAMESPACE, newGasLimit); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, newGasLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, newGasLimit); assertEq(universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE), newGasLimit); } @@ -791,16 +801,16 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Non-manager should revert vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.UVCORE_ADMIN_ROLE() ) ); vm.prank(nonOwner); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, newGasLimit); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, newGasLimit); } function test_SetBaseGasLimitByChain_ZeroValueAllowed() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, 0); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, 0); assertEq(universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE), 0); } @@ -918,7 +928,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectEmit(false, false, false, true); emit SetRescueFundsGasLimitByChain(CHAIN_NAMESPACE, rescueGasLimit); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, rescueGasLimit); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, rescueGasLimit); assertEq(universalCore.rescueFundsGasLimitByChainNamespace(CHAIN_NAMESPACE), rescueGasLimit); } @@ -926,16 +936,16 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetRescueFundsGasLimitByChain_OnlyManagerRole() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.UVCORE_ADMIN_ROLE() ) ); vm.prank(nonOwner); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); } function test_SetRescueFundsGasLimitByChain_ZeroValueAllowed() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 0); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 0); assertEq(universalCore.rescueFundsGasLimitByChainNamespace(CHAIN_NAMESPACE), 0); } @@ -943,7 +953,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 rescueGasLimit = 300_000; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, rescueGasLimit); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, rescueGasLimit); ( address returnedGasToken, @@ -983,7 +993,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Set rescue gas limit but no gas token for this chain vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain("999", 300_000); + universalCore.updateRescueFundsGasLimitByChain("999", 300_000); vm.expectRevert(CommonErrors.ZeroAddress.selector); universalCore.getRescueFundsGasLimit(address(newToken)); @@ -1007,8 +1017,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Set rescue gas limit and gas token, but no gas price vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain("888", 300_000); - universalCore.setGasTokenPRC20("888", address(mockPRC20)); + universalCore.updateRescueFundsGasLimitByChain("888", 300_000); + universalCore.updateGasTokenPRC20("888", address(mockPRC20)); vm.stopPrank(); vm.expectRevert(UniversalCoreErrors.ZeroGasPrice.selector); @@ -1016,7 +1026,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { } // ======================================== - // 9) setUniversalGatewayPC Tests + // 9) updateUniversalGatewayPC Tests // ======================================== function test_SetUniversalGatewayPC_HappyPath() public { @@ -1026,24 +1036,27 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(true, true, false, true); emit SetUniversalGatewayPC(oldGateway, gateway); - universalCore.setUniversalGatewayPC(gateway); + universalCore.updateUniversalGatewayPC(gateway); assertEq(universalCore.universalGatewayPC(), gateway); } function test_SetUniversalGatewayPC_ZeroAddressReverts() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniversalGatewayPC(address(0)); + universalCore.updateUniversalGatewayPC(address(0)); } - function test_SetUniversalGatewayPC_OnlyAdmin() public { + function test_SetUniversalGatewayPC_OnlyOperator() public { + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setUniversalGatewayPC(makeAddr("gateway")); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) + ); + universalCore.updateUniversalGatewayPC(makeAddr("gateway")); } // ======================================== - // 10) setUniswapV3Addresses Tests + // 10) updateUniswapV3Addresses Tests // ======================================== function test_SetUniswapV3Addresses_HappyPath() public { @@ -1053,7 +1066,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(false, false, false, true); emit SetUniswapV3Addresses(f, r); - universalCore.setUniswapV3Addresses(f, r); + universalCore.updateUniswapV3Addresses(f, r); assertEq(universalCore.uniswapV3Factory(), f); assertEq(universalCore.uniswapV3SwapRouter(), r); @@ -1062,23 +1075,26 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetUniswapV3Addresses_RevertsZeroFactory() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(address(0), makeAddr("r")); + universalCore.updateUniswapV3Addresses(address(0), makeAddr("r")); } function test_SetUniswapV3Addresses_RevertsZeroRouter() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setUniswapV3Addresses(makeAddr("f"), address(0)); + universalCore.updateUniswapV3Addresses(makeAddr("f"), address(0)); } - function test_SetUniswapV3Addresses_OnlyAdmin() public { + function test_SetUniswapV3Addresses_OnlyOperator() public { + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setUniswapV3Addresses(makeAddr("f"), makeAddr("r")); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) + ); + universalCore.updateUniswapV3Addresses(makeAddr("f"), makeAddr("r")); } // ======================================== - // 11) setDefaultDeadlineMins Tests + // 11) updateDefaultDeadlineMins Tests // ======================================== event SetDefaultDeadlineMins(uint256 minutesValue); @@ -1087,18 +1103,21 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(false, false, false, true); emit SetDefaultDeadlineMins(30); - universalCore.setDefaultDeadlineMins(30); + universalCore.updateDefaultDeadlineMins(30); assertEq(universalCore.defaultDeadlineMins(), 30); } - function test_SetDefaultDeadlineMins_OnlyAdmin() public { + function test_SetDefaultDeadlineMins_OnlyUCoreAdmin() public { + bytes32 ucoreAdminRole = universalCore.UVCORE_ADMIN_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setDefaultDeadlineMins(30); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ucoreAdminRole) + ); + universalCore.updateDefaultDeadlineMins(30); } // ======================================== - // 12) setDefaultFeeTier Tests + // 12) updateDefaultFeeTier Tests // ======================================== function test_SetDefaultFeeTier_HappyPath_500() public { @@ -1106,21 +1125,21 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(deployer); vm.expectEmit(true, false, false, true); emit SetDefaultFeeTier(token, 500); - universalCore.setDefaultFeeTier(token, 500); + universalCore.updateDefaultFeeTier(token, 500); assertEq(universalCore.defaultFeeTier(token), 500); } function test_SetDefaultFeeTier_HappyPath_3000() public { address token = makeAddr("token"); vm.prank(deployer); - universalCore.setDefaultFeeTier(token, 3000); + universalCore.updateDefaultFeeTier(token, 3000); assertEq(universalCore.defaultFeeTier(token), 3000); } function test_SetDefaultFeeTier_HappyPath_10000() public { address token = makeAddr("token"); vm.prank(deployer); - universalCore.setDefaultFeeTier(token, 10000); + universalCore.updateDefaultFeeTier(token, 10000); assertEq(universalCore.defaultFeeTier(token), 10000); } @@ -1128,19 +1147,22 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { address token = makeAddr("token"); vm.prank(deployer); vm.expectRevert(UniversalCoreErrors.InvalidFeeTier.selector); - universalCore.setDefaultFeeTier(token, 200); + universalCore.updateDefaultFeeTier(token, 200); } function test_SetDefaultFeeTier_RevertsZeroAddress() public { vm.prank(deployer); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setDefaultFeeTier(address(0), 3000); + universalCore.updateDefaultFeeTier(address(0), 3000); } - function test_SetDefaultFeeTier_OnlyAdmin() public { + function test_SetDefaultFeeTier_OnlyUCoreAdmin() public { + bytes32 ucoreAdminRole = universalCore.UVCORE_ADMIN_ROLE(); vm.prank(nonOwner); - vm.expectRevert(CommonErrors.InvalidOwner.selector); - universalCore.setDefaultFeeTier(makeAddr("token"), 3000); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ucoreAdminRole) + ); + universalCore.updateDefaultFeeTier(makeAddr("token"), 3000); } // ======================================== @@ -1152,14 +1174,14 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 updatedLimit = 600_000; vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, initialLimit); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, initialLimit); vm.stopPrank(); (, uint256 gasFee1,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); assertEq(gasFee1, GAS_PRICE * initialLimit); vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, updatedLimit); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, updatedLimit); vm.stopPrank(); (, uint256 gasFee2,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); @@ -1178,7 +1200,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectEmit(false, false, false, true); emit SetMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); assertEq(universalCore.maxStalenessByChainNamespace(CHAIN_NAMESPACE), maxStaleness); } @@ -1186,16 +1208,16 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { function test_SetMaxStalenessByChain_OnlyManagerRole() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.UVCORE_ADMIN_ROLE() ) ); vm.prank(nonOwner); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 3600); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, 3600); } function test_SetMaxStalenessByChain_ZeroDisablesCheck() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 0); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, 0); assertEq(universalCore.maxStalenessByChainNamespace(CHAIN_NAMESPACE), 0); } @@ -1203,11 +1225,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // --- Default-off behaviour --- function test_StalenessDisabledByDefault_NoRevertEvenAfterLongWarp() public { - // No setMaxStalenessByChain call — staleness check is off for this namespace. + // No updateMaxStalenessByChain call — staleness check is off for this namespace. // Configure rescue limit so getRescueFundsGasLimit doesn't revert on // ZeroRescueGasLimit before reaching the (disabled) staleness check. vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); vm.warp(block.timestamp + 365 days); @@ -1224,15 +1246,13 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); vm.warp(observedAt + maxStaleness + 1); vm.expectRevert( - abi.encodeWithSelector( - UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness - ) + abi.encodeWithSelector(UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness) ); universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); } @@ -1241,7 +1261,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); // Warp to exactly observedAt + maxStaleness — still within window (strict >). uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); @@ -1255,15 +1275,13 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); vm.warp(observedAt + maxStaleness + 1); vm.expectRevert( - abi.encodeWithSelector( - UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness - ) + abi.encodeWithSelector(UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness) ); universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); } @@ -1274,17 +1292,15 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); vm.stopPrank(); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); vm.warp(observedAt + maxStaleness + 1); vm.expectRevert( - abi.encodeWithSelector( - UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness - ) + abi.encodeWithSelector(UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, maxStaleness) ); universalCore.getRescueFundsGasLimit(address(prc20Token)); } @@ -1293,8 +1309,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); - universalCore.setRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateRescueFundsGasLimitByChain(CHAIN_NAMESPACE, 300_000); vm.stopPrank(); uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); @@ -1310,7 +1326,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 maxStaleness = 300; vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, maxStaleness); // Warp past window — call should revert uint256 firstObservedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); @@ -1352,14 +1368,14 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { PRC20 freshPRC20 = PRC20(payable(proxyAddr)); vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(freshNs, address(mockPRC20)); - universalCore.setBaseGasLimitByChain(freshNs, 100_000); + universalCore.updateGasTokenPRC20(freshNs, address(mockPRC20)); + universalCore.updateBaseGasLimitByChain(freshNs, 100_000); // Deliberately skip setChainMeta — gasPrice stays 0. // We need gasPrice > 0 to reach the staleness check. Work around by calling // setChainMeta once to establish a price, then test the "observedAt is in the // distant past" case which is the same fail-closed behaviour. universalCore.setChainMeta(freshNs, GAS_PRICE, 0); - universalCore.setMaxStalenessByChain(freshNs, 60); + universalCore.updateMaxStalenessByChain(freshNs, 60); vm.stopPrank(); // Warp far past the observed window. observedAt is now in the past relative to block.timestamp. @@ -1367,9 +1383,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.warp(observedAt + 1 days); vm.expectRevert( - abi.encodeWithSelector( - UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(60) - ) + abi.encodeWithSelector(UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(60)) ); universalCore.getOutboundTxGasAndFees(address(freshPRC20), 0); } @@ -1396,11 +1410,11 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { PRC20 bPRC20 = PRC20(payable(proxyAddr)); vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(chainBNs, address(mockPRC20)); + universalCore.updateGasTokenPRC20(chainBNs, address(mockPRC20)); universalCore.setChainMeta(chainBNs, GAS_PRICE, 0); - universalCore.setBaseGasLimitByChain(chainBNs, BASE_GAS_LIMIT); + universalCore.updateBaseGasLimitByChain(chainBNs, BASE_GAS_LIMIT); // Configure maxStaleness only on chain A. - universalCore.setMaxStalenessByChain(CHAIN_NAMESPACE, 300); + universalCore.updateMaxStalenessByChain(CHAIN_NAMESPACE, 300); vm.stopPrank(); // Warp past A's window. @@ -1409,9 +1423,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { // Chain A reverts (maxStaleness enforced). vm.expectRevert( - abi.encodeWithSelector( - UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(300) - ) + abi.encodeWithSelector(UniversalCoreErrors.StaleGasData.selector, observedAt, block.timestamp, uint256(300)) ); universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); @@ -1442,9 +1454,9 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { PRC20 freshPRC20 = PRC20(payable(proxyAddr)); vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(freshNs, address(mockPRC20)); - universalCore.setBaseGasLimitByChain(freshNs, 100_000); - universalCore.setMaxStalenessByChain(freshNs, 60); + universalCore.updateGasTokenPRC20(freshNs, address(mockPRC20)); + universalCore.updateBaseGasLimitByChain(freshNs, 100_000); + universalCore.updateMaxStalenessByChain(freshNs, 60); // No setChainMeta → gasPrice is 0 → ZeroGasPrice revert must come before staleness. vm.stopPrank(); @@ -1490,11 +1502,14 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertEq(recipient.balance, stuckAmount); } - function test_RescueNativePC_OnlyAdmin() public { + function test_RescueNativePC_OnlyUCoreAdmin() public { vm.deal(address(universalCore), 1 ether); address nonAdmin = makeAddr("nonAdmin"); + bytes32 ucoreAdminRole = universalCore.UVCORE_ADMIN_ROLE(); - vm.expectRevert(CommonErrors.InvalidOwner.selector); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonAdmin, ucoreAdminRole) + ); vm.prank(nonAdmin); universalCore.rescueNativePC(payable(nonAdmin), 1 ether); } @@ -1523,4 +1538,89 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.expectRevert(CommonErrors.TransferFailed.selector); universalCore.rescueNativePC(payable(address(nonPayable)), 1 ether); } + + // ========================================================================= + // ADR (AccessControlDefaultAdminRules) Tests + // ========================================================================= + + function testADR_OwnerReturnsAdmin() public view { + assertEq(universalCore.owner(), deployer); + } + + function testADR_DefaultAdminDelay() public view { + assertEq(universalCore.defaultAdminDelay(), 1 days); + } + + function testADR_RoleAdminOfUCoreAdmin_IsRoleManager() public view { + assertEq(universalCore.getRoleAdmin(universalCore.UVCORE_ADMIN_ROLE()), universalCore.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfOperator_IsRoleManager() public view { + assertEq(universalCore.getRoleAdmin(universalCore.OPERATOR_ROLE()), universalCore.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfPauser_IsRoleManager() public view { + assertEq(universalCore.getRoleAdmin(universalCore.PAUSER_ROLE()), universalCore.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfRoleManager_IsDefaultAdmin() public view { + assertEq(universalCore.getRoleAdmin(universalCore.ROLE_MANAGER_ROLE()), universalCore.DEFAULT_ADMIN_ROLE()); + } + + function testADR_GrantDefaultAdminRole_Reverts() public { + bytes32 defaultAdminRole = universalCore.DEFAULT_ADMIN_ROLE(); + address newAdmin = makeAddr("adrNewAdmin"); + + vm.expectRevert(IAccessControlDefaultAdminRules.AccessControlEnforcedDefaultAdminRules.selector); + universalCore.grantRole(defaultAdminRole, newAdmin); + } + + function testADR_TransferFlow() public { + address newAdmin = makeAddr("adrNewAdmin"); + + universalCore.beginDefaultAdminTransfer(newAdmin); + + (address pendingAdmin, uint48 schedule) = universalCore.pendingDefaultAdmin(); + assertEq(pendingAdmin, newAdmin); + assertTrue(schedule > 0); + + // Cannot accept before delay + vm.expectRevert(); + vm.prank(newAdmin); + universalCore.acceptDefaultAdminTransfer(); + + // Warp past delay and accept + vm.warp(block.timestamp + 1 days + 1); + vm.prank(newAdmin); + universalCore.acceptDefaultAdminTransfer(); + + assertEq(universalCore.owner(), newAdmin); + assertTrue(universalCore.hasRole(universalCore.DEFAULT_ADMIN_ROLE(), newAdmin)); + assertFalse(universalCore.hasRole(universalCore.DEFAULT_ADMIN_ROLE(), deployer)); + } + + function testADR_GrantRoleManager() public { + address newRoleManager = makeAddr("newRoleManager"); + + universalCore.grantRole(universalCore.ROLE_MANAGER_ROLE(), newRoleManager); + assertTrue(universalCore.hasRole(universalCore.ROLE_MANAGER_ROLE(), newRoleManager)); + + // newRoleManager can now grant UVCORE_ADMIN_ROLE + address newUCoreAdmin = makeAddr("newUCoreAdmin"); + vm.prank(newRoleManager); + universalCore.grantRole(universalCore.UVCORE_ADMIN_ROLE(), newUCoreAdmin); + assertTrue(universalCore.hasRole(universalCore.UVCORE_ADMIN_ROLE(), newUCoreAdmin)); + } + + function testPauserCannotUnpause() public { + vm.prank(pauser); + universalCore.pause(); + + bytes32 operatorRole = universalCore.OPERATOR_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, pauser, operatorRole) + ); + vm.prank(pauser); + universalCore.unpause(); + } } diff --git a/test/tests_token_and_core/UniversalCoreRefund.t.sol b/test/tests_token_and_core/UniversalCoreRefund.t.sol index 3268f12..18dcf58 100644 --- a/test/tests_token_and_core/UniversalCoreRefund.t.sol +++ b/test/tests_token_and_core/UniversalCoreRefund.t.sol @@ -88,17 +88,18 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { UniversalCore implementation = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + address(this), + pauser, address(mockWPC), address(mockFactory), - address(mockRouter), - pauser + address(mockRouter) ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); universalCore = UniversalCore(payable(proxyAddress)); // Configure auto-swap support - universalCore.setAutoSwapSupported(address(gasTokenMock), true); - universalCore.setDefaultFeeTier(address(gasTokenMock), FEE_TIER); + universalCore.updateAutoSwapSupported(address(gasTokenMock), true); + universalCore.updateDefaultFeeTier(address(gasTokenMock), FEE_TIER); // Setup mock pool (gasToken <-> wPC) address pool = makeAddr("mockPool"); @@ -178,8 +179,8 @@ contract UniversalCoreRefundTest is Test, UpgradeableContractHelper { function test_RefundUnusedGas_WithSwap_NoPool_Reverts() public { MockPRC20 noPool = new MockPRC20(); - universalCore.setAutoSwapSupported(address(noPool), true); - universalCore.setDefaultFeeTier(address(noPool), FEE_TIER); + universalCore.updateAutoSwapSupported(address(noPool), true); + universalCore.updateDefaultFeeTier(address(noPool), FEE_TIER); vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectRevert(UniversalCoreErrors.PoolNotFound.selector); diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index 10fa830..f0cda8d 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -74,10 +74,11 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { UniversalCore implementation = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( UniversalCore.initialize.selector, + deployer, + pauser, address(mockWPC), address(mockFactory), - address(mockRouter), - pauser + address(mockRouter) ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); universalCore = UniversalCore(payable(proxyAddress)); @@ -87,22 +88,22 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { prc20Token.updateUniversalCore(address(universalCore)); // Set gateway - universalCore.setUniversalGatewayPC(gateway); + universalCore.updateUniversalGatewayPC(gateway); // Grant MANAGER_ROLE to UE Module for manager functions - universalCore.grantRole(universalCore.MANAGER_ROLE(), UNIVERSAL_EXECUTOR_MODULE); + universalCore.grantRole(universalCore.UVCORE_ADMIN_ROLE(), UNIVERSAL_EXECUTOR_MODULE); - // Configure gas token first, then gas price (setGasTokenPRC20 resets gas price to 0, + // Configure gas token first, then gas price (updateGasTokenPRC20 resets gas price to 0, // so setChainMeta must come after to preserve the configured gas price). vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(gasTokenMock)); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, address(gasTokenMock)); universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); - universalCore.setBaseGasLimitByChain(CHAIN_NAMESPACE, 500_000); - universalCore.setProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); + universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, 500_000); + universalCore.updateProtocolFeeByToken(address(prc20Token), PROTOCOL_FEE); vm.stopPrank(); // Set default fee tier for gas token - universalCore.setDefaultFeeTier(address(gasTokenMock), FEE_TIER); + universalCore.updateDefaultFeeTier(address(gasTokenMock), FEE_TIER); // Setup mock pool (wPC <-> gasToken) address pool = makeAddr("mockPool"); @@ -138,7 +139,7 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { address newGateway = makeAddr("newGateway"); vm.deal(newGateway, 1 ether); - universalCore.setUniversalGatewayPC(newGateway); + universalCore.updateUniversalGatewayPC(newGateway); vm.prank(newGateway); (uint256 gasTokenOut,) = @@ -242,8 +243,8 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { // Setup BSC chain MockPRC20 bscGasToken = new MockPRC20(); vm.prank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20("eip155:56", address(bscGasToken)); - universalCore.setDefaultFeeTier(address(bscGasToken), FEE_TIER); + universalCore.updateGasTokenPRC20("eip155:56", address(bscGasToken)); + universalCore.updateDefaultFeeTier(address(bscGasToken), FEE_TIER); // Setup BSC pool address bscPool = makeAddr("bscPool"); @@ -383,7 +384,7 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { } // ======================================== - // 8) setProtocolFeeByToken + // 8) updateProtocolFeeByToken // ======================================== function test_SetProtocolFeeByToken_HappyPath() public { @@ -393,7 +394,7 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectEmit(true, false, false, true); emit SetProtocolFeeByToken(token, fee); - universalCore.setProtocolFeeByToken(token, fee); + universalCore.updateProtocolFeeByToken(token, fee); assertEq(universalCore.protocolFeeByToken(token), fee); } @@ -404,17 +405,17 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonManager, universalCore.MANAGER_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, nonManager, universalCore.UVCORE_ADMIN_ROLE() ) ); vm.prank(nonManager); - universalCore.setProtocolFeeByToken(token, 1000); + universalCore.updateProtocolFeeByToken(token, 1000); } function test_SetProtocolFeeByToken_ZeroAddressReverts() public { vm.prank(UNIVERSAL_EXECUTOR_MODULE); vm.expectRevert(CommonErrors.ZeroAddress.selector); - universalCore.setProtocolFeeByToken(address(0), 1000); + universalCore.updateProtocolFeeByToken(address(0), 1000); } // ======================================== @@ -422,15 +423,14 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { // ======================================== function test_SwapAndBurnGas_FalseBurnReturn_Reverts() public { - FalseReturningPRC20 falseGasToken = - new FalseReturningPRC20(CHAIN_NAMESPACE, SOURCE_TOKEN_ADDRESS); + FalseReturningPRC20 falseGasToken = new FalseReturningPRC20(CHAIN_NAMESPACE, SOURCE_TOKEN_ADDRESS); vm.startPrank(UNIVERSAL_EXECUTOR_MODULE); - universalCore.setGasTokenPRC20(CHAIN_NAMESPACE, address(falseGasToken)); + universalCore.updateGasTokenPRC20(CHAIN_NAMESPACE, address(falseGasToken)); universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); vm.stopPrank(); - universalCore.setDefaultFeeTier(address(falseGasToken), FEE_TIER); + universalCore.updateDefaultFeeTier(address(falseGasToken), FEE_TIER); address pool = makeAddr("falsePool"); if (address(mockWPC) < address(falseGasToken)) { @@ -441,8 +441,6 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { vm.prank(gateway); vm.expectRevert(UniversalCoreErrors.PRC20OperationFailed.selector); - universalCore.swapAndBurnGas{value: 1 ether}( - address(falseGasToken), FEE_TIER, GAS_FEE, 0, user - ); + universalCore.swapAndBurnGas{value: 1 ether}(address(falseGasToken), FEE_TIER, GAS_FEE, 0, user); } } diff --git a/test/tests_ueaMigration/BaseTest.t.sol b/test/tests_ueaMigration/BaseTest.t.sol index c9fa457..908166d 100644 --- a/test/tests_ueaMigration/BaseTest.t.sol +++ b/test/tests_ueaMigration/BaseTest.t.sol @@ -179,7 +179,7 @@ contract BaseTest is Test { factory = UEAFactory(address(factoryProxy)); // Set UEA proxy implementation in factory - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); } function _deployMigrationContract() internal { @@ -189,7 +189,7 @@ contract BaseTest is Test { assertEq(migration.UEA_SVM_IMPLEMENTATION(), address(ueaSVMImplV2), "Migration SVM implementation mismatch"); // Set migration contract in factory so UEAs can fetch it - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); } function _setupChainRegistrations() internal { diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index 4974c3a..fd326dc 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -13,6 +13,8 @@ import {UEAErrors as Errors} from "../../src/libraries/Errors.sol"; import {IUEA} from "../../src/interfaces/IUEA.sol"; import {IUEAFactory} from "../../src/Interfaces/IUEAFactory.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {IAccessControlDefaultAdminRules} from + "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UEAProxy} from "../../src/uea/UEAProxy.sol"; @@ -65,7 +67,7 @@ contract UEAFactoryTest is Test { factory = UEAFactory(address(proxy)); // Set UEAProxy implementation after initialization - factory.setUEAProxyImplementation(ueaProxyImpl); + factory.updateUEAProxyImplementation(ueaProxyImpl); // Set up user and keys (owner,) = makeAddrAndKey("owner"); @@ -123,25 +125,24 @@ contract UEAFactoryTest is Test { assertEq(factory.getUEA(moveChainHash), address(moveImpl)); } - function testSetUEAMigrationContractOnlyOwner() public { + function testSetUEAMigrationContractOnlyUEAAdmin() public { UEAMigration migration = new UEAMigration(address(ueaEVMImpl), address(ueaSVMImpl)); - // Non-owner should revert - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) ); vm.prank(nonOwner); - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); - // Owner can set and value is stored - factory.setUEAMigrationContract(address(migration)); + // Owner (has UEA_ADMIN_ROLE) can set and value is stored + factory.updateUEAMigrationContract(address(migration)); assertEq(factory.UEA_MIGRATION_CONTRACT(), address(migration)); } function testSetUEAMigrationContractZeroAddressReverts() public { vm.expectRevert(Errors.InvalidInputArgs.selector); - factory.setUEAMigrationContract(address(0)); + factory.updateUEAMigrationContract(address(0)); } function testRegisterMultipleUEA() public { @@ -402,36 +403,34 @@ contract UEAFactoryTest is Test { function testSetUEAProxyImplementation_RevertsOnZeroAddress() public { vm.expectRevert(Errors.InvalidInputArgs.selector); - factory.setUEAProxyImplementation(address(0)); + factory.updateUEAProxyImplementation(address(0)); } - function testSetUEAProxyImplementation_OnlyOwner() public { - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + function testSetUEAProxyImplementation_OnlyUEAAdmin() public { + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) ); vm.prank(nonOwner); - factory.setUEAProxyImplementation(address(ueaEVMImpl)); + factory.updateUEAProxyImplementation(address(ueaEVMImpl)); } function testOwnershipFunctions() public { - // Test that only owner can register implementations - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) ); vm.prank(nonOwner); bytes32 chainHash = keccak256(abi.encode("APTOS", "1")); factory.registerNewChain(chainHash, MOVE_VM_HASH); - // Test that owner can register implementations + // Owner (has UEA_ADMIN_ROLE) can register factory.registerNewChain(chainHash, MOVE_VM_HASH); UEA_EVM newImpl = new UEA_EVM(); factory.registerUEA(chainHash, MOVE_VM_HASH, address(newImpl)); - // Verify the implementation was registered assertEq(address(factory.getUEA(chainHash)), address(newImpl)); } @@ -509,34 +508,26 @@ contract UEAFactoryTest is Test { assertTrue(ethUEA != polyUEA); } - function testOwnershipTransfer() public { - address newOwner = makeAddr("newOwner"); + function testOwnershipTransfer_ADR() public { + address newAdmin = makeAddr("newAdmin"); - // Grant DEFAULT_ADMIN_ROLE to new owner, then revoke from old owner - factory.grantRole(factory.DEFAULT_ADMIN_ROLE(), newOwner); - factory.revokeRole(factory.DEFAULT_ADMIN_ROLE(), address(this)); + // ADR blocks direct grantRole for DEFAULT_ADMIN_ROLE + // Cache role before vm.expectRevert — argument evaluation is a staticcall that + // would otherwise be consumed as the "next call" by vm.expectRevert. + bytes32 defaultAdminRole = factory.DEFAULT_ADMIN_ROLE(); + vm.expectRevert(IAccessControlDefaultAdminRules.AccessControlEnforcedDefaultAdminRules.selector); + factory.grantRole(defaultAdminRole, newAdmin); - // Verify new owner has role, old does not - assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), newOwner)); - assertFalse(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), address(this))); - - // Try to register a chain with old owner — should fail - bytes32 chainHash = keccak256(abi.encode("TestChain", "123")); - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), factory.DEFAULT_ADMIN_ROLE() - ) - ); - factory.registerNewChain(chainHash, MOVE_VM_HASH); - - // New owner should be able to register a chain - vm.prank(newOwner); - factory.registerNewChain(chainHash, MOVE_VM_HASH); + // Must use 2-step transfer: begin → wait → accept + // OZ _hasSchedulePassed uses strict "<", so warp must be > schedule, not ==. + factory.beginDefaultAdminTransfer(newAdmin); + vm.warp(block.timestamp + 1 days + 1); + vm.prank(newAdmin); + factory.acceptDefaultAdminTransfer(); - // Verify chain is registered - (bytes32 vmHash, bool isRegistered) = factory.getVMType(chainHash); - assertEq(vmHash, MOVE_VM_HASH); - assertTrue(isRegistered); + assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), newAdmin)); + assertFalse(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), address(this))); + assertEq(factory.owner(), newAdmin); } function testFactoryLifecycle() public { @@ -1005,16 +996,24 @@ contract UEAFactoryTest is Test { assertTrue(factory.paused()); } - function testUnpause_OnlyPauser() public { - bytes32 role = factory.PAUSER_ROLE(); + function testUnpause_OnlyOperator() public { + bytes32 operatorRole = factory.OPERATOR_ROLE(); vm.prank(pauser); factory.pause(); + // nonOwner cannot unpause vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, role) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, operatorRole) ); vm.prank(nonOwner); factory.unpause(); + + // pauser also cannot unpause (different role now) + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, pauser, operatorRole) + ); + vm.prank(pauser); + factory.unpause(); } function testUnpause_HappyPath() public { @@ -1022,7 +1021,7 @@ contract UEAFactoryTest is Test { factory.pause(); assertTrue(factory.paused()); - vm.prank(pauser); + // deployer has OPERATOR_ROLE factory.unpause(); assertFalse(factory.paused()); } @@ -1042,7 +1041,7 @@ contract UEAFactoryTest is Test { function testDeployUEA_AfterUnpause_Works() public { vm.prank(pauser); factory.pause(); - vm.prank(pauser); + // deployer has OPERATOR_ROLE factory.unpause(); bytes memory testOwnerBytes = abi.encodePacked(makeAddr("unpausedOwner")); @@ -1053,17 +1052,18 @@ contract UEAFactoryTest is Test { assertTrue(factory.hasCode(ueaAddress)); } - function testGrantPauserRole_OnlyAdmin() public { + function testGrantPauserRole_OnlyRoleManager() public { address newPauser = makeAddr("newPauser"); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 roleManagerRole = factory.ROLE_MANAGER_ROLE(); bytes32 pauserRole = factory.PAUSER_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, roleManagerRole) ); vm.prank(nonOwner); factory.grantRole(pauserRole, newPauser); + // deployer has ROLE_MANAGER_ROLE factory.grantRole(pauserRole, newPauser); assertTrue(factory.hasRole(pauserRole, newPauser)); } @@ -1117,12 +1117,12 @@ contract UEAFactoryTest is Test { assertEq(factory.UEA_VM(SVM_HASH), address(newImpl)); } - function testUpdateUEAImplementation_OnlyAdmin() public { + function testUpdateUEAImplementation_OnlyUEAAdmin() public { UEA_EVM newImpl = new UEA_EVM(); - bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, adminRole) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) ); vm.prank(nonOwner); factory.updateUEAImplementation(EVM_HASH, address(newImpl)); @@ -1184,23 +1184,22 @@ contract UEAFactoryTest is Test { } function test_SetPushChainId_HappyPath() public { - factory.setPushChainId("9999"); + factory.updatePushChainId("9999"); assertEq(factory.pushChainId(), "9999"); } - function test_SetPushChainId_OnlyAdmin() public { + function test_SetPushChainId_OnlyUEAAdmin() public { + bytes32 ueaAdminRole = factory.UEA_ADMIN_ROLE(); vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, factory.DEFAULT_ADMIN_ROLE() - ) + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, ueaAdminRole) ); vm.prank(nonOwner); - factory.setPushChainId("9999"); + factory.updatePushChainId("9999"); } function test_SetPushChainId_RevertsOnEmptyString() public { vm.expectRevert(Errors.InvalidInputArgs.selector); - factory.setPushChainId(""); + factory.updatePushChainId(""); } function test_GetOriginForUEA_FallbackUsesConfiguredChainId() public { @@ -1224,7 +1223,7 @@ contract UEAFactoryTest is Test { assertEq(beforeAcc.chainId, "42101"); // Update pushChainId - factory.setPushChainId("1"); + factory.updatePushChainId("1"); // After update: fallback returns new chainId (UniversalAccountId memory afterAcc, bool afterIsUEA) = factory.getOriginForUEA(randomAddr); @@ -1233,4 +1232,96 @@ contract UEAFactoryTest is Test { assertEq(afterAcc.chainId, "1"); assertEq(afterAcc.owner, bytes(abi.encodePacked(randomAddr))); } + + // ========================================================================= + // ADR (AccessControlDefaultAdminRules) Tests + // ========================================================================= + + function testADR_OwnerReturnsAdmin() public view { + assertEq(factory.owner(), deployer); + } + + function testADR_DefaultAdminDelay() public view { + assertEq(factory.defaultAdminDelay(), 1 days); + } + + function testADR_RoleAdminOfUEAAdmin_IsRoleManager() public view { + assertEq(factory.getRoleAdmin(factory.UEA_ADMIN_ROLE()), factory.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfOperator_IsRoleManager() public view { + assertEq(factory.getRoleAdmin(factory.OPERATOR_ROLE()), factory.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfPauser_IsRoleManager() public view { + assertEq(factory.getRoleAdmin(factory.PAUSER_ROLE()), factory.ROLE_MANAGER_ROLE()); + } + + function testADR_RoleAdminOfRoleManager_IsDefaultAdmin() public view { + assertEq(factory.getRoleAdmin(factory.ROLE_MANAGER_ROLE()), factory.DEFAULT_ADMIN_ROLE()); + } + + function testADR_InitialRolesGrantedToAdmin() public view { + assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), deployer)); + assertTrue(factory.hasRole(factory.ROLE_MANAGER_ROLE(), deployer)); + assertTrue(factory.hasRole(factory.UEA_ADMIN_ROLE(), deployer)); + assertTrue(factory.hasRole(factory.OPERATOR_ROLE(), deployer)); + } + + function testADR_PauserRoleGrantedToPauser() public view { + assertTrue(factory.hasRole(factory.PAUSER_ROLE(), pauser)); + assertFalse(factory.hasRole(factory.PAUSER_ROLE(), deployer)); + } + + function testADR_TransferFlow() public { + address newAdmin = makeAddr("adrNewAdmin"); + + factory.beginDefaultAdminTransfer(newAdmin); + + (address pendingAdmin, uint48 schedule) = factory.pendingDefaultAdmin(); + assertEq(pendingAdmin, newAdmin); + assertTrue(schedule > 0); + + // Cannot accept before delay + // vm.expectRevert must come before vm.prank — prank is consumed by the very next call. + vm.expectRevert(); + vm.prank(newAdmin); + factory.acceptDefaultAdminTransfer(); + + // Warp past delay and accept. + // OZ _hasSchedulePassed uses strict "<", so warp must be strictly > schedule. + vm.warp(block.timestamp + 1 days + 1); + vm.prank(newAdmin); + factory.acceptDefaultAdminTransfer(); + + assertEq(factory.owner(), newAdmin); + assertTrue(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), newAdmin)); + assertFalse(factory.hasRole(factory.DEFAULT_ADMIN_ROLE(), deployer)); + } + + function testADR_GrantRoleManager() public { + address newRoleManager = makeAddr("newRoleManager"); + + // deployer has DEFAULT_ADMIN_ROLE which administers ROLE_MANAGER_ROLE + factory.grantRole(factory.ROLE_MANAGER_ROLE(), newRoleManager); + assertTrue(factory.hasRole(factory.ROLE_MANAGER_ROLE(), newRoleManager)); + + // newRoleManager can now grant UEA_ADMIN_ROLE + address newUEAAdmin = makeAddr("newUEAAdmin"); + vm.prank(newRoleManager); + factory.grantRole(factory.UEA_ADMIN_ROLE(), newUEAAdmin); + assertTrue(factory.hasRole(factory.UEA_ADMIN_ROLE(), newUEAAdmin)); + } + + function testPauserCannotUnpause() public { + vm.prank(pauser); + factory.pause(); + + bytes32 operatorRole = factory.OPERATOR_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, pauser, operatorRole) + ); + vm.prank(pauser); + factory.unpause(); + } } diff --git a/test/tests_uea_and_factory/UEAProxyCalls.t.sol b/test/tests_uea_and_factory/UEAProxyCalls.t.sol index f365855..de50541 100644 --- a/test/tests_uea_and_factory/UEAProxyCalls.t.sol +++ b/test/tests_uea_and_factory/UEAProxyCalls.t.sol @@ -57,7 +57,7 @@ contract ProxyCallTest is Test { factory = UEAFactory(address(proxy)); // Set UEAProxy implementation after initialization - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); bytes32 evmChainHash = keccak256(abi.encode("eip155", "1")); factory.registerNewChain(evmChainHash, EVM_HASH); diff --git a/test/tests_uea_and_factory/UEA_EVM.t.sol b/test/tests_uea_and_factory/UEA_EVM.t.sol index ad023f7..e91e99d 100644 --- a/test/tests_uea_and_factory/UEA_EVM.t.sol +++ b/test/tests_uea_and_factory/UEA_EVM.t.sol @@ -59,7 +59,7 @@ contract UEA_EVMTest is Test { factory = UEAFactory(address(proxy)); // Set UEAProxy implementation after initialization - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); // NOW deploy UEA implementations with factory address ueaEVMImpl = new UEA_EVM(); @@ -893,7 +893,7 @@ contract UEA_EVMTest is Test { function test_SuccessfulMigrationUpdatesImplementation() public deployEvmSmartAccount { // Set migration contract in factory - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); MigrationPayload memory payload = MigrationPayload({migration: address(migration), nonce: 0, deadline: block.timestamp + 1000}); @@ -929,7 +929,7 @@ contract UEA_EVMTest is Test { } function testMigration_RevertsWhenValueNonZero() public deployEvmSmartAccount { - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); UniversalPayload memory payload = UniversalPayload({ to: address(evmSmartAccountInstance), @@ -952,7 +952,7 @@ contract UEA_EVMTest is Test { } function testMigration_RevertsWhenTargetNotSelf() public deployEvmSmartAccount { - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); UniversalPayload memory payload = UniversalPayload({ to: address(target), diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index 0a1cc21..83d8daf 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -50,7 +50,7 @@ contract UEASVMTest is Test { factory = UEAFactory(address(proxy)); // Set UEAProxy implementation after initialization - factory.setUEAProxyImplementation(address(ueaProxyImpl)); + factory.updateUEAProxyImplementation(address(ueaProxyImpl)); // Deploy SVM implementation svmSmartAccountImpl = new UEA_SVM(); @@ -830,7 +830,7 @@ contract UEASVMTest is Test { function test_SuccessfulMigrationUpdatesImplementation() public deploySvmSmartAccount { // Set migration contract in factory - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); MigrationPayload memory payload = MigrationPayload({migration: address(migration), nonce: 0, deadline: block.timestamp + 1000}); @@ -874,7 +874,7 @@ contract UEASVMTest is Test { } function testMigration_RevertsWhenValueNonZero() public deploySvmSmartAccount { - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); UniversalPayload memory payload = UniversalPayload({ to: address(svmSmartAccountInstance), @@ -894,7 +894,7 @@ contract UEASVMTest is Test { } function testMigration_RevertsWhenTargetNotSelf() public deploySvmSmartAccount { - factory.setUEAMigrationContract(address(migration)); + factory.updateUEAMigrationContract(address(migration)); UniversalPayload memory payload = UniversalPayload({ to: address(target), From 51843b3486ffa0a8f4c8682ecf63805cc797f48b Mon Sep 17 00:00:00 2001 From: Zaryab Date: Mon, 27 Apr 2026 18:33:37 +0530 Subject: [PATCH 46/56] F-2026-15648-UVCore resolves gasLimitUsed --- src/Interfaces/IUniversalCore.sol | 10 ++++++- src/UniversalCore.sol | 10 ++++++- test/fuzz/UniversalCore_Fuzz.t.sol | 6 ++--- test/mocks/MockGasToken.sol | 2 +- test/tests_token_and_core/UniversalCore.t.sol | 26 ++++++++++++------- .../UniversalCoreSwapFee.t.sol | 26 ++++++++++++++----- 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 17b7a6a..0df2af3 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -146,10 +146,18 @@ interface IUniversalCore { /// @return protocolFee Protocol fee in native PC from protocolFeeByToken mapping /// @return gasPrice Gas price on the external chain /// @return chainNamespace Source chain namespace + /// @return gasLimitUsed Effective gas limit used to compute gasFee function getOutboundTxGasAndFees(address _prc20, uint256 gasLimitWithBaseLimit) external view - returns (address gasToken, uint256 gasFee, uint256 protocolFee, uint256 gasPrice, string memory chainNamespace); + returns ( + address gasToken, + uint256 gasFee, + uint256 protocolFee, + uint256 gasPrice, + string memory chainNamespace, + uint256 gasLimitUsed + ); /// @notice Get rescue funds gas limit, fee, and related config for a PRC20 token. /// @param _prc20 PRC20 address diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 8926edf..501838c 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -280,7 +280,14 @@ contract UniversalCore is function getOutboundTxGasAndFees(address _prc20, uint256 gasLimitWithBaseLimit) public view - returns (address gasToken, uint256 gasFee, uint256 protocolFee, uint256 gasPrice, string memory chainNamespace) + returns ( + address gasToken, + uint256 gasFee, + uint256 protocolFee, + uint256 gasPrice, + string memory chainNamespace, + uint256 gasLimitUsed + ) { chainNamespace = IPRC20(_prc20).SOURCE_CHAIN_NAMESPACE(); uint256 baseLimit = baseGasLimitByChainNamespace[chainNamespace]; @@ -302,6 +309,7 @@ contract UniversalCore is gasFee = gasPrice * gasLimitWithBaseLimit; protocolFee = protocolFeeByToken[_prc20]; + gasLimitUsed = gasLimitWithBaseLimit; } /// @inheritdoc IUniversalCore diff --git a/test/fuzz/UniversalCore_Fuzz.t.sol b/test/fuzz/UniversalCore_Fuzz.t.sol index 5efc86a..0cd45f0 100644 --- a/test/fuzz/UniversalCore_Fuzz.t.sol +++ b/test/fuzz/UniversalCore_Fuzz.t.sol @@ -84,7 +84,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { vm.prank(uExec); universalCore.updateBaseGasLimitByChain(CHAIN_NS, baseLimit); - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), gasLimit); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), gasLimit); assertEq(gasFee, uint256(gasPrice) * uint256(gasLimit)); } @@ -100,7 +100,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { universalCore.updateBaseGasLimitByChain(CHAIN_NS, baseLimit); // gasLimitWithBaseLimit == 0 → uses baseLimit - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); assertEq(gasFee, uint256(gasPrice) * uint256(baseLimit)); } @@ -453,7 +453,7 @@ contract UniversalCore_Fuzz is Test, UpgradeableContractHelper { uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NS); vm.warp(uint256(observedAt) + uint256(timePast)); - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20), 0); assertGt(gasFee, 0, "should succeed within the staleness window"); } diff --git a/test/mocks/MockGasToken.sol b/test/mocks/MockGasToken.sol index f4a6934..cd5d715 100644 --- a/test/mocks/MockGasToken.sol +++ b/test/mocks/MockGasToken.sol @@ -126,7 +126,7 @@ contract MockGasToken is IPRC20 { function getOutboundTxGasAndFees(address, uint256) external pure - returns (address, uint256, uint256, uint256, string memory) + returns (address, uint256, uint256, uint256, string memory, uint256) { revert("Not implemented"); } diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 4ed393f..6162e8e 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -652,7 +652,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 gasFee, uint256 protocolFee, uint256 gasPrice, - string memory chainNamespace + string memory chainNamespace, + uint256 gasLimitUsed ) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertEq(returnedGasToken, address(mockPRC20)); @@ -664,6 +665,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertEq(gasFee, gasPrice * actualBaseGasLimit); assertEq(protocolFee, actualProtocolFee); assertEq(keccak256(bytes(chainNamespace)), keccak256(bytes(CHAIN_NAMESPACE))); + assertEq(gasLimitUsed, actualBaseGasLimit); + assertEq(gasFee, gasPrice * gasLimitUsed); } function testWithdrawGasFeeWithGasLimitHappyPath() public view { @@ -674,7 +677,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 gasFee, uint256 protocolFee, uint256 gasPrice, - string memory chainNamespace + string memory chainNamespace, + uint256 gasLimitUsed ) = universalCore.getOutboundTxGasAndFees(address(prc20Token), customGasLimit); assertEq(returnedGasToken, address(mockPRC20)); @@ -682,6 +686,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { assertEq(gasFee, gasPrice * customGasLimit); assertEq(protocolFee, PROTOCOL_FEE); assertEq(keccak256(bytes(chainNamespace)), keccak256(bytes(CHAIN_NAMESPACE))); + assertEq(gasLimitUsed, customGasLimit); + assertEq(gasFee, gasPrice * gasLimitUsed); } function testWithdrawGasFeeZeroGasPrice() public { @@ -745,7 +751,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.setChainMeta(CHAIN_NAMESPACE, newGasPrice, 0); - (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee, uint256 protocolFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); uint256 actualBaseGasLimit = universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE); uint256 expectedGasFee = newGasPrice * actualBaseGasLimit; @@ -759,7 +765,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.updateBaseGasLimitByChain(CHAIN_NAMESPACE, newBaseGasLimit); - (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee, uint256 protocolFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); uint256 actualGasPrice = universalCore.gasPriceByChainNamespace(CHAIN_NAMESPACE); assertEq(gasFee, actualGasPrice * newBaseGasLimit); @@ -772,7 +778,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.updateProtocolFeeByToken(address(prc20Token), newProtocolFee); - (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee, uint256 protocolFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); uint256 actualGasPrice = universalCore.gasPriceByChainNamespace(CHAIN_NAMESPACE); uint256 actualBaseGasLimit = universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE); @@ -862,7 +868,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.setChainMeta(CHAIN_NAMESPACE, newPrice, 100); - (, uint256 gasFee, uint256 protocolFee,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee, uint256 protocolFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertEq(gasFee, newPrice * universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE)); assertEq(protocolFee, universalCore.protocolFeeByToken(address(prc20Token))); } @@ -1233,7 +1239,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.warp(block.timestamp + 365 days); - (, uint256 outboundFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 outboundFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertGt(outboundFee, 0, "outbound fee should quote even after a year when check is disabled"); (, uint256 rescueFee,,,) = universalCore.getRescueFundsGasLimit(address(prc20Token)); @@ -1267,7 +1273,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { uint256 observedAt = universalCore.timestampObservedAtByChainNamespace(CHAIN_NAMESPACE); vm.warp(observedAt + maxStaleness); - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertGt(gasFee, 0, "should succeed exactly at the boundary"); } @@ -1343,7 +1349,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(UNIVERSAL_EXECUTOR_MODULE); universalCore.setChainMeta(CHAIN_NAMESPACE, GAS_PRICE, 0); - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertGt(gasFee, 0, "should succeed after refresh"); } @@ -1428,7 +1434,7 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); // Chain B succeeds (no maxStaleness set for chainBNs). - (, uint256 gasFee,,,) = universalCore.getOutboundTxGasAndFees(address(bPRC20), 0); + (, uint256 gasFee,,,,) = universalCore.getOutboundTxGasAndFees(address(bPRC20), 0); assertGt(gasFee, 0, "chain B should not be affected by chain A's staleness config"); } diff --git a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol index f0cda8d..ac16aba 100644 --- a/test/tests_token_and_core/UniversalCoreSwapFee.t.sol +++ b/test/tests_token_and_core/UniversalCoreSwapFee.t.sol @@ -346,27 +346,41 @@ contract UniversalCoreSwapFeeTest is Test, UpgradeableContractHelper { // 6) getOutboundTxGasAndFees // ======================================== - function test_WithdrawGasFee_Returns5Values() public view { - (address gasToken, uint256 gasFee, uint256 protocolFee, uint256 gasPrice, string memory chainNamespace) = - universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); + function test_WithdrawGasFee_Returns6Values() public view { + ( + address gasToken, + uint256 gasFee, + uint256 protocolFee, + uint256 gasPrice, + string memory chainNamespace, + uint256 gasLimitUsed + ) = universalCore.getOutboundTxGasAndFees(address(prc20Token), 0); assertEq(gasToken, address(gasTokenMock)); assertEq(gasPrice, GAS_PRICE); assertEq(gasFee, gasPrice * universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE)); assertEq(protocolFee, universalCore.protocolFeeByToken(address(prc20Token))); assertEq(keccak256(bytes(chainNamespace)), keccak256(bytes(CHAIN_NAMESPACE))); + assertEq(gasLimitUsed, universalCore.baseGasLimitByChainNamespace(CHAIN_NAMESPACE)); } - function test_WithdrawGasFeeWithGasLimit_Returns5Values() public view { + function test_WithdrawGasFeeWithGasLimit_Returns6Values() public view { uint256 customGasLimit = 600_000; - (address gasToken, uint256 gasFee, uint256 protocolFee, uint256 gasPrice, string memory chainNamespace) = - universalCore.getOutboundTxGasAndFees(address(prc20Token), customGasLimit); + ( + address gasToken, + uint256 gasFee, + uint256 protocolFee, + uint256 gasPrice, + string memory chainNamespace, + uint256 gasLimitUsed + ) = universalCore.getOutboundTxGasAndFees(address(prc20Token), customGasLimit); assertEq(gasToken, address(gasTokenMock)); assertEq(gasPrice, GAS_PRICE); assertEq(gasFee, gasPrice * customGasLimit); assertEq(protocolFee, PROTOCOL_FEE); assertEq(keccak256(bytes(chainNamespace)), keccak256(bytes(CHAIN_NAMESPACE))); + assertEq(gasLimitUsed, customGasLimit); } // ======================================== From 2b70c5c2af61512c25040c0b432ca7a1c729204d Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 28 Apr 2026 10:48:24 +0530 Subject: [PATCH 47/56] F-2026-15561: reset ERC-20 approval to zero after gateway call in CEA --- src/cea/CEA.sol | 1 + test/tests_cea/CEA.t.sol | 43 +++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/cea/CEA.sol b/src/cea/CEA.sol index 3944771..0a5ba68 100644 --- a/src/cea/CEA.sol +++ b/src/cea/CEA.sol @@ -141,6 +141,7 @@ contract CEA is ICEA, ReentrancyGuard { } IERC20(token).approve(gateway, amount); IUniversalGateway(gateway).sendUniversalTxFromCEA(req); + IERC20(token).approve(gateway, 0); } } else { IUniversalGateway(gateway).sendUniversalTxFromCEA(req); diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index a979fe6..102f89c 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -1055,11 +1055,10 @@ contract CEATest is Test { ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - // Approval persists after gateway call (gateway consumes via transferFrom in production) assertEq( token.allowance(address(ceaInstance), address(mockUniversalGateway)), - 500 ether, - "Approval should persist (mock gateway doesn't consume)" + 0, + "Approval should be reset to zero after gateway call" ); } @@ -1077,9 +1076,10 @@ contract CEATest is Test { ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); - // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), amount, "Approval should persist" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 0, + "Approval should be reset to zero after gateway call" ); } @@ -1125,9 +1125,10 @@ contract CEATest is Test { // Mock gateway doesn't transfer tokens, so balance unchanged uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); - // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, "Approval should persist" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 0, + "Approval should be reset to zero after gateway call" ); } @@ -1245,9 +1246,33 @@ contract CEATest is Test { // Mock gateway doesn't transfer tokens, so balance unchanged uint256 balanceAfter = token.balanceOf(address(ceaInstance)); assertEq(balanceAfter, balanceBefore, "Balance should remain same (mock doesn't transfer)"); - // Approval persists after gateway call (mock gateway doesn't consume) assertEq( - token.allowance(address(ceaInstance), address(mockUniversalGateway)), sendAmount, "Approval should persist" + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 0, + "Approval should be reset to zero after gateway call" + ); + } + + function testSendUniversalTxToUEA_ResetsApprovalToZeroAfterGatewayCall() public deployCEA { + MockGasToken token = new MockGasToken(); + fundCEAWithTokens(address(token), 2000 ether); + + vm.prank(address(ceaInstance)); + token.approve(address(mockUniversalGateway), 1000 ether); + assertEq(token.allowance(address(ceaInstance), address(mockUniversalGateway)), 1000 ether); + + bytes32 subTxId = generateTxID(1); + bytes32 universalTxID = generateUniversalTxID(1); + uint256 sendAmount = 500 ether; + + vm.prank(vault); + bytes memory multicallPayload = buildSendToUEAMulticallPayload(address(token), sendAmount, true); + ceaInstance.executeUniversalTx(subTxId, universalTxID, ueaOnPush, address(0), multicallPayload); + + assertEq( + token.allowance(address(ceaInstance), address(mockUniversalGateway)), + 0, + "Pre-existing approval should be zeroed after gateway call" ); } From daf74825d22121896964e2d542201e623c4e994c Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 5 May 2026 14:57:41 +0530 Subject: [PATCH 48/56] F-2026-15525: enforce payload.nonce == storage nonce in UEA execution --- src/libraries/Errors.sol | 1 + src/uea/UEA_EVM.sol | 4 ++++ src/uea/UEA_SVM.sol | 4 ++++ test/fuzz/UEA_EVM_Fuzz.t.sol | 21 +++++++++++++---- .../tests_uea_and_factory/UEAProxyCalls.t.sol | 3 ++- test/tests_uea_and_factory/UEA_EVM.t.sol | 6 ++--- test/tests_uea_and_factory/UEA_SVM.t.sol | 23 +++++++++++++++++++ 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index a397c04..2213dbc 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -78,6 +78,7 @@ library UEAErrors { error InvalidInputArgs(); error InvalidEVMSignature(); error InvalidSVMSignature(); + error NonceMismatch(uint256 expected, uint256 provided); error PrecompileCallFailed(); error AccountAlreadyExists(); error UEAAlreadyRegistered(); diff --git a/src/uea/UEA_EVM.sol b/src/uea/UEA_EVM.sol index 5501bff..4d12e77 100644 --- a/src/uea/UEA_EVM.sol +++ b/src/uea/UEA_EVM.sol @@ -135,6 +135,10 @@ contract UEA_EVM is ReentrancyGuard, IUEA { /// @inheritdoc IUEA function executeUniversalTx(UniversalPayload calldata payload, bytes calldata signature) external nonReentrant { + if (payload.nonce != nonce) { + revert UEAErrors.NonceMismatch(nonce, payload.nonce); + } + if (msg.sender != UNIVERSAL_EXECUTOR_MODULE) { bytes32 payloadHash = getUniversalPayloadHash(payload); if (!verifyUniversalPayloadSignature(payloadHash, signature)) { diff --git a/src/uea/UEA_SVM.sol b/src/uea/UEA_SVM.sol index b60d566..31cb0fe 100644 --- a/src/uea/UEA_SVM.sol +++ b/src/uea/UEA_SVM.sol @@ -141,6 +141,10 @@ contract UEA_SVM is ReentrancyGuard, IUEA { /// @inheritdoc IUEA function executeUniversalTx(UniversalPayload calldata payload, bytes calldata signature) external nonReentrant { + if (payload.nonce != nonce) { + revert UEAErrors.NonceMismatch(nonce, payload.nonce); + } + if (msg.sender != UNIVERSAL_EXECUTOR_MODULE) { bytes32 payloadHash = getUniversalPayloadHash(payload); if (!verifyUniversalPayloadSignature(payloadHash, signature)) { diff --git a/test/fuzz/UEA_EVM_Fuzz.t.sol b/test/fuzz/UEA_EVM_Fuzz.t.sol index 690a6ca..64468d2 100644 --- a/test/fuzz/UEA_EVM_Fuzz.t.sol +++ b/test/fuzz/UEA_EVM_Fuzz.t.sol @@ -239,9 +239,9 @@ contract UEA_EVM_FuzzTest is Test { evmSmartAccountInstance.executeUniversalTx(firstPayload, firstSig); assertEq(evmSmartAccountInstance.nonce(), 1); - // Replay the old signature (signed at nonce 0) — the hash no longer matches - // because the contract nonce is now 1, so signature verification fails - vm.expectRevert(UEAErrors.InvalidEVMSignature.selector); + // Replay the old payload (nonce=0) — the account expects nonce 1 now, + // so the nonce check fires before signature verification + vm.expectRevert(abi.encodeWithSelector(UEAErrors.NonceMismatch.selector, 1, 0)); evmSmartAccountInstance.executeUniversalTx(firstPayload, firstSig); } @@ -338,9 +338,20 @@ contract UEA_EVM_FuzzTest is Test { // Now test migration selector goes to migration path // Migration requires payload.to == address(this), so it will revert with InvalidCall - // when targeting a different address — confirming migration path was taken + // when targeting a different address — confirming migration path was taken. + // Use nonce=1 since the first transaction above incremented the account nonce. bytes memory migData = abi.encodePacked(MIGRATION_SELECTOR); - UniversalPayload memory migPayload = _buildPayload(address(target), 0, migData, 0); + UniversalPayload memory migPayload = UniversalPayload({ + to: address(target), + value: 0, + data: migData, + gasLimit: 1_000_000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 1, + deadline: 0, + vType: VerificationType(0) + }); bytes memory migSig = _signPayload(evmSmartAccountInstance, migPayload, ownerPK); vm.expectRevert(UEAErrors.InvalidCall.selector); evmSmartAccountInstance.executeUniversalTx(migPayload, migSig); diff --git a/test/tests_uea_and_factory/UEAProxyCalls.t.sol b/test/tests_uea_and_factory/UEAProxyCalls.t.sol index de50541..6c97f17 100644 --- a/test/tests_uea_and_factory/UEAProxyCalls.t.sol +++ b/test/tests_uea_and_factory/UEAProxyCalls.t.sol @@ -261,7 +261,8 @@ contract ProxyCallTest is Test { user1UEAInstance.executeUniversalTx(payload, signature); - vm.expectRevert(Errors.InvalidEVMSignature.selector); + // Nonce check fires first (expected=1, got=0) before signature verification + vm.expectRevert(abi.encodeWithSelector(Errors.NonceMismatch.selector, 1, 0)); user1UEAInstance.executeUniversalTx(payload, signature); } diff --git a/test/tests_uea_and_factory/UEA_EVM.t.sol b/test/tests_uea_and_factory/UEA_EVM.t.sol index e91e99d..99c01a3 100644 --- a/test/tests_uea_and_factory/UEA_EVM.t.sol +++ b/test/tests_uea_and_factory/UEA_EVM.t.sol @@ -705,7 +705,7 @@ contract UEA_EVMTest is Test { bytes memory signature = abi.encodePacked(r, s, v); // The execution should fail because the account expects nonce to be 0, not 100 - vm.expectRevert(Errors.InvalidEVMSignature.selector); + vm.expectRevert(abi.encodeWithSelector(Errors.NonceMismatch.selector, 0, 100)); evmSmartAccountInstance.executeUniversalTx(payload, signature); // Verify state hasn't changed @@ -737,8 +737,8 @@ contract UEA_EVMTest is Test { uint256 previousNonce = evmSmartAccountInstance.nonce(); - // Try to execute with same nonce again - vm.expectRevert(Errors.InvalidEVMSignature.selector); + // Try to execute with same nonce again — nonce check fires first (expected=1, got=0) + vm.expectRevert(abi.encodeWithSelector(Errors.NonceMismatch.selector, 1, 0)); evmSmartAccountInstance.executeUniversalTx(payload, signature); // Verify state hasn't changed diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index 83d8daf..596b5a7 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -1264,6 +1264,29 @@ contract UEASVMTest is Test { // Verify execution succeeded assertEq(target.getMagicNumber(), 999, "Execution should succeed with valid signature"); } + + function testRevertWhenIncorrectNonce() public deploySvmSmartAccount { + uint256 previousNonce = svmSmartAccountInstance.nonce(); + + UniversalPayload memory payload = UniversalPayload({ + to: address(target), + value: 0, + data: abi.encodeWithSignature("setMagicNumber(uint256)", 786), + gasLimit: 1000000, + maxFeePerGas: 0, + nonce: 100, + deadline: block.timestamp + 1000, + maxPriorityFeePerGas: 0, + vType: VerificationType(0) + }); + + bytes memory signature = hex"00"; + + vm.expectRevert(abi.encodeWithSelector(Errors.NonceMismatch.selector, 0, 100)); + svmSmartAccountInstance.executeUniversalTx(payload, signature); + + assertEq(previousNonce, svmSmartAccountInstance.nonce(), "Nonce should not have changed"); + } } // Helper contracts for testing reverts From 460f851daf743143dc9b517e1b604fe0c568e8aa Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 5 May 2026 17:49:51 +0530 Subject: [PATCH 49/56] synced PRC20 testnet version --- src/testnetV0/PRC20V0.sol | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/testnetV0/PRC20V0.sol b/src/testnetV0/PRC20V0.sol index 03ffb82..8558d7e 100644 --- a/src/testnetV0/PRC20V0.sol +++ b/src/testnetV0/PRC20V0.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {IPRC20} from "../interfaces/IPRC20.sol"; import {PRC20Errors, CommonErrors} from "../libraries/Errors.sol"; @@ -172,8 +173,6 @@ contract PRC20 is IPRC20, Initializable { address recipient, uint256 amount ) external returns (bool) { - _transfer(sender, recipient, amount); - uint256 currentAllowance = _allowances[sender][msg.sender]; if (currentAllowance < amount) revert PRC20Errors.LowAllowance(); unchecked { @@ -183,6 +182,8 @@ contract PRC20 is IPRC20, Initializable { sender, msg.sender, _allowances[sender][msg.sender] ); + _transfer(sender, recipient, amount); + return true; } @@ -207,11 +208,14 @@ contract PRC20 is IPRC20, Initializable { ) { revert PRC20Errors.InvalidSender(); } + if (PausableUpgradeable(UNIVERSAL_CORE).paused()) { + revert PRC20Errors.CorePaused(); + } _mint(to, amount); emit Deposit( - abi.encodePacked(UNIVERSAL_EXECUTOR_MODULE), to, amount + abi.encodePacked(msg.sender), to, amount ); return true; } @@ -235,7 +239,9 @@ contract PRC20 is IPRC20, Initializable { function setName( string memory newName ) external onlyUniversalExecutor { + string memory oldName = _name; _name = newName; + emit NameUpdated(oldName, newName); } /// @notice Update token symbol. @@ -243,7 +249,9 @@ contract PRC20 is IPRC20, Initializable { function setSymbol( string memory newSymbol ) external onlyUniversalExecutor { + string memory oldSymbol = _symbol; _symbol = newSymbol; + emit SymbolUpdated(oldSymbol, newSymbol); } // ========================= From f12772e61f5e331e8036716391e0ce07dcb045d8 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 5 May 2026 18:58:04 +0530 Subject: [PATCH 50/56] synced uvcore and ueaFactory --- src/testnetV0/IUniversalCoreV0.sol | 21 ++- src/testnetV0/UEAFactoryV0.sol | 114 +++++++++------- src/testnetV0/UniversalCoreV0.sol | 201 +++++++++++++++++++++-------- 3 files changed, 224 insertions(+), 112 deletions(-) diff --git a/src/testnetV0/IUniversalCoreV0.sol b/src/testnetV0/IUniversalCoreV0.sol index 1168ec5..c71edb1 100644 --- a/src/testnetV0/IUniversalCoreV0.sol +++ b/src/testnetV0/IUniversalCoreV0.sol @@ -15,9 +15,15 @@ interface IUniversalCoreV0 { uint256 chainHeight, uint256 observedAt ); - event SetGasPrice(string chainNamespace, uint256 price); event SetGasToken(string chainNamespace, address prc20); event SetDefaultDeadlineMins(uint256 minutesValue); + event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); + event SetAutoSwapSupported(address indexed token, bool supported); + event SetWPC(address indexed oldAddr, address indexed newAddr); + event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); + event SetUniswapV3Addresses(address factory, address swapRouter); + event SetDefaultFeeTier(address indexed token, uint24 feeTier); + event RescueNativePC(address indexed to, uint256 amount); event SetGasPCPool( string chainNamespace, address pool, uint24 fee ); @@ -98,15 +104,6 @@ interface IUniversalCoreV0 { uint256 minPCOut ) external; - /// @notice Set gas price for a chain. - /// @dev To Be Removed — use setChainMeta instead. - /// @param chainNamespace Chain Namespace - /// @param price New gas price - function setGasPrice( - string memory chainNamespace, - uint256 price - ) external; - // ========================= // UCV0_2: GATEWAY FUNCTIONS // ========================= @@ -170,6 +167,7 @@ interface IUniversalCoreV0 { /// @return protocolFee Protocol fee in native PC from protocolFeeByToken mapping /// @return gasPrice Gas price on the external chain /// @return chainNamespace Source chain namespace + /// @return gasLimitUsed Effective gas limit used in calculation function getOutboundTxGasAndFees( address _prc20, uint256 gasLimitWithBaseLimit @@ -181,7 +179,8 @@ interface IUniversalCoreV0 { uint256 gasFee, uint256 protocolFee, uint256 gasPrice, - string memory chainNamespace + string memory chainNamespace, + uint256 gasLimitUsed ); /// @notice Get rescue funds gas limit, fee, and related config for a PRC20 token. diff --git a/src/testnetV0/UEAFactoryV0.sol b/src/testnetV0/UEAFactoryV0.sol index d6d2701..3833987 100644 --- a/src/testnetV0/UEAFactoryV0.sol +++ b/src/testnetV0/UEAFactoryV0.sol @@ -9,35 +9,37 @@ import {UEAProxy} from "../uea/UEAProxy.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; /** * @title UEAFactoryV0 (TESTNET ONLY — DO NOT DEPLOY TO MAINNET) * @notice Testnet version of UEAFactory, preserved as-is from the live deployment * on Push Chain Donut Testnet. - * Note: Testnet Version includes OwneableUpgradeable but mainnet uses AccessControlUpgradeable. + * + * Access control: AccessControlDefaultAdminRulesUpgradeable (1-day delay). + * Roles: DEFAULT_ADMIN_ROLE (root), ROLE_MANAGER_ROLE (grants operational roles), + * UEA_ADMIN_ROLE (implementation + chain config), OPERATOR_ROLE (unpause), + * PAUSER_ROLE (pause only). */ -contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, IUEAFactory { +contract UEAFactoryV0 is Initializable, AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable, IUEAFactory { using Clones for address; // ========================= - // UF: EVENTS (V0-only, removed from IUEAFactory in V1) + // UF: ROLES // ========================= - event PauserRoleGranted(address indexed pauser); + bytes32 public constant ROLE_MANAGER_ROLE = keccak256("ROLE_MANAGER_ROLE"); + bytes32 public constant UEA_ADMIN_ROLE = keccak256("UEA_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ========================= // UF: STATE VARIABLES // ========================= - /// @notice Role that can pause and unpause UEA deployments. - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - - /** - * @notice Maps role hash to the address holding that role. - * @dev STORAGE SLOT 0 — DEAD SLOT — DO NOT REMOVE OR REORDER. - */ + /// @dev DEAD SLOT — preserved for storage layout compatibility. Do not use. mapping(bytes32 => address) public roles; /// @notice Maps VM type hashes to their UEA implementation addresses. @@ -70,15 +72,6 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, _disableInitializers(); } - // ========================= - // UF: MODIFIERS - // ========================= - - modifier onlyPauser() { - if (roles[PAUSER_ROLE] != msg.sender) revert UEAErrors.InvalidInputArgs(); - _; - } - // ========================= // UF: INITIALIZER // ========================= @@ -88,10 +81,26 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, /// @param initialPauser Address granted the PAUSER_ROLE function initialize(address initialOwner, address initialPauser) public initializer { if (initialOwner == address(0) || initialPauser == address(0)) revert UEAErrors.InvalidInputArgs(); - __Ownable_init(initialOwner); __Pausable_init(); roles[PAUSER_ROLE] = initialPauser; - emit PauserRoleGranted(initialPauser); + } + + /// @dev Reinitializer to migrate from OwnableUpgradeable to RBAC. + /// @param _admin Admin address — granted DEFAULT_ADMIN_ROLE + all operational roles + /// @param _pauser Address granted the PAUSER_ROLE + function initializeV2(address _admin, address _pauser) public reinitializer(2) { + if (_admin == address(0) || _pauser == address(0)) revert UEAErrors.InvalidInputArgs(); + + __AccessControlDefaultAdminRules_init(1 days, _admin); + + _setRoleAdmin(UEA_ADMIN_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(OPERATOR_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(PAUSER_ROLE, ROLE_MANAGER_ROLE); + + _grantRole(ROLE_MANAGER_ROLE, _admin); + _grantRole(UEA_ADMIN_ROLE, _admin); + _grantRole(OPERATOR_ROLE, _admin); + _grantRole(PAUSER_ROLE, _pauser); } // ========================= @@ -213,49 +222,41 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, // ========================= /// @notice Pause UEA deployments. Only callable by PAUSER_ROLE. - function pause() external onlyPauser { + function pause() external onlyRole(PAUSER_ROLE) { _pause(); } - /// @notice Unpause UEA deployments. Only callable by PAUSER_ROLE. - function unpause() external onlyPauser { + /// @notice Unpause UEA deployments. Only callable by OPERATOR_ROLE. + function unpause() external onlyRole(OPERATOR_ROLE) { _unpause(); } - /// @notice Grant PAUSER_ROLE to a new address. Only callable by owner. - /// @param newPauser Address to grant pauser role to - function setPauserRole(address newPauser) external onlyOwner { - if (newPauser == address(0)) revert UEAErrors.InvalidInputArgs(); - roles[PAUSER_ROLE] = newPauser; - emit PauserRoleGranted(newPauser); - } - - /// @notice Sets the UEAProxy implementation address. + /// @notice Sets the UEAProxy implementation address. Only callable by UEA_ADMIN_ROLE. /// @param ueaProxyImplementation New UEAProxy implementation address - function updateUEAProxyImplementation(address ueaProxyImplementation) external onlyOwner { + function updateUEAProxyImplementation(address ueaProxyImplementation) external onlyRole(UEA_ADMIN_ROLE) { if (ueaProxyImplementation == address(0)) { revert UEAErrors.InvalidInputArgs(); } UEA_PROXY_IMPLEMENTATION = ueaProxyImplementation; } - /// @notice Sets the UEA migration contract address. + /// @notice Sets the UEA migration contract address. Only callable by UEA_ADMIN_ROLE. /// @param ueaMigrationContract New migration contract address - function updateUEAMigrationContract(address ueaMigrationContract) external onlyOwner { + function updateUEAMigrationContract(address ueaMigrationContract) external onlyRole(UEA_ADMIN_ROLE) { if (ueaMigrationContract == address(0)) { revert UEAErrors.InvalidInputArgs(); } UEA_MIGRATION_CONTRACT = ueaMigrationContract; } - /// @notice Update `pushChainId`. Reverts on empty string. - function updatePushChainId(string memory _pushChainId) external onlyOwner { + /// @notice Update `pushChainId`. Reverts on empty string. Only callable by UEA_ADMIN_ROLE. + function updatePushChainId(string memory _pushChainId) external onlyRole(UEA_ADMIN_ROLE) { if (bytes(_pushChainId).length == 0) revert UEAErrors.InvalidInputArgs(); pushChainId = _pushChainId; } /// @inheritdoc IUEAFactory - function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyOwner { + function registerNewChain(bytes32 _chainHash, bytes32 _vmHash) external onlyRole(UEA_ADMIN_ROLE) { (, bool isRegistered) = getVMType(_chainHash); if (isRegistered) { revert UEAErrors.InvalidInputArgs(); @@ -268,19 +269,24 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, /// @inheritdoc IUEAFactory function registerMultipleUEA(bytes32[] memory _chainHashes, bytes32[] memory _vmHashes, address[] memory _UEA) external - onlyOwner + onlyRole(UEA_ADMIN_ROLE) { if (_UEA.length != _vmHashes.length || _UEA.length != _chainHashes.length) { revert UEAErrors.InvalidInputArgs(); } for (uint256 i = 0; i < _UEA.length; i++) { - registerUEA(_chainHashes[i], _vmHashes[i], _UEA[i]); + _registerUEA(_chainHashes[i], _vmHashes[i], _UEA[i]); } } /// @inheritdoc IUEAFactory - function registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) public onlyOwner { + function registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) public onlyRole(UEA_ADMIN_ROLE) { + _registerUEA(_chainHash, _vmHash, _UEA); + } + + /// @dev Internal registration logic with overwrite protection. + function _registerUEA(bytes32 _chainHash, bytes32 _vmHash, address _UEA) internal { if (_UEA == address(0)) { revert UEAErrors.InvalidInputArgs(); } @@ -290,10 +296,30 @@ contract UEAFactoryV0 is Initializable, OwnableUpgradeable, PausableUpgradeable, revert UEAErrors.InvalidInputArgs(); } + if (UEA_VM[_vmHash] != address(0)) { + revert UEAErrors.UEAAlreadyRegistered(); + } + UEA_VM[_vmHash] = _UEA; emit UEARegistered(_chainHash, _UEA, _vmHash); } + /// @notice Replace the registered UEA implementation for a VM hash. + /// @param _vmHash VM hash whose implementation is being updated + /// @param _newUEA New UEA implementation address (must be non-zero) + function updateUEAImplementation(bytes32 _vmHash, address _newUEA) external onlyRole(UEA_ADMIN_ROLE) { + if (_newUEA == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + address previous = UEA_VM[_vmHash]; + if (previous == address(0)) { + revert UEAErrors.InvalidInputArgs(); + } + + UEA_VM[_vmHash] = _newUEA; + emit UEAImplementationUpdated(_vmHash, previous, _newUEA); + } + // ========================= // UF_4: PUBLIC HELPERS // ========================= diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index c6201cf..af76cc2 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + AccessControlDefaultAdminRulesUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; @@ -30,7 +32,7 @@ contract UniversalCoreV0 is IUniversalCoreV0, Initializable, ReentrancyGuardUpgradeable, - AccessControlUpgradeable, + AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable { using SafeERC20 for IERC20; @@ -87,6 +89,11 @@ contract UniversalCoreV0 is /// @notice Role for managing gas-related configurations. bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant ROLE_MANAGER_ROLE = keccak256("ROLE_MANAGER_ROLE"); + bytes32 public constant UVCORE_ADMIN_ROLE = keccak256("UVCORE_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + // -- Uniswap V3 fee tiers -- uint24 public constant FEE_TIER_LOWEST = 100; uint24 public constant FEE_TIER_LOW = 500; @@ -121,6 +128,9 @@ contract UniversalCoreV0 is /// @notice Rescue funds gas limit per chain namespace. mapping(string => uint256) public rescueFundsGasLimitByChainNamespace; + /// @notice Maximum acceptable age (seconds) of gas data before quotes are rejected as stale. + mapping(string => uint256) public maxStalenessByChainNamespace; + // ========================= // UCV0: MODIFIERS // ========================= @@ -139,13 +149,6 @@ contract UniversalCoreV0 is _; } - modifier onlyAdmin() { - if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { - revert CommonErrors.InvalidOwner(); - } - _; - } - // ========================= // UCV0: CONSTRUCTOR // ========================= @@ -165,7 +168,6 @@ contract UniversalCoreV0 is initializer { __ReentrancyGuard_init(); - __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); @@ -175,14 +177,37 @@ contract UniversalCoreV0 is __deprecated_uniswapV3Quoter = uniswapV3Quoter_; } + /// @dev Reinitializer to migrate to granular RBAC with admin transfer delay. + /// @param _admin Admin address — granted DEFAULT_ADMIN_ROLE + all operational roles + /// @param _pauser Address granted the PAUSER_ROLE + function initializeV2(address _admin, address _pauser) public reinitializer(2) { + if (_admin == address(0) || _pauser == address(0)) revert CommonErrors.ZeroAddress(); + + __AccessControlDefaultAdminRules_init(1 days, _admin); + + _setRoleAdmin(UVCORE_ADMIN_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(OPERATOR_ROLE, ROLE_MANAGER_ROLE); + _setRoleAdmin(PAUSER_ROLE, ROLE_MANAGER_ROLE); + + _grantRole(ROLE_MANAGER_ROLE, _admin); + _grantRole(UVCORE_ADMIN_ROLE, _admin); + _grantRole(OPERATOR_ROLE, _admin); + _grantRole(PAUSER_ROLE, _pauser); + } + // ========================= // UCV0_1: UE MODULE ACTIONS // ========================= /// @inheritdoc IUniversalCoreV0 - function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule whenNotPaused { + function depositPRC20Token(address prc20, uint256 amount, address recipient) + external + onlyUEModule + whenNotPaused + nonReentrant + { _validateParams(prc20, amount, recipient); - IPRC20(prc20).deposit(recipient, amount); + if (!IPRC20(prc20).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } /// @inheritdoc IUniversalCoreV0 @@ -215,7 +240,7 @@ contract UniversalCoreV0 is uint256 pcOut; if (!withSwap) { - IPRC20(gasToken).deposit(recipient, amount); + if (!IPRC20(gasToken).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } else { if (minPCOut == 0) { revert UniversalCoreErrors.MinPCOutRequired(); @@ -260,7 +285,7 @@ contract UniversalCoreV0 is IWPC(WPC).deposit{value: msg.value}(); - IERC20(WPC).approve(uniswapV3SwapRouter, msg.value); + IERC20(WPC).forceApprove(uniswapV3SwapRouter, msg.value); ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ tokenIn: WPC, @@ -274,9 +299,9 @@ contract UniversalCoreV0 is }); uint256 amountInUsed = ISwapRouter(uniswapV3SwapRouter).exactOutputSingle(params); - IERC20(WPC).approve(uniswapV3SwapRouter, 0); + IERC20(WPC).forceApprove(uniswapV3SwapRouter, 0); - IPRC20(gasToken).burn(gasFee); + if (!IPRC20(gasToken).burn(gasFee)) revert UniversalCoreErrors.PRC20OperationFailed(); gasTokenOut = gasFee; refund = msg.value - amountInUsed; @@ -297,10 +322,18 @@ contract UniversalCoreV0 is function getOutboundTxGasAndFees(address _prc20, uint256 gasLimitWithBaseLimit) public view - returns (address gasToken, uint256 gasFee, uint256 protocolFee, uint256 gasPrice, string memory chainNamespace) + returns ( + address gasToken, + uint256 gasFee, + uint256 protocolFee, + uint256 gasPrice, + string memory chainNamespace, + uint256 gasLimitUsed + ) { chainNamespace = IPRC20(_prc20).SOURCE_CHAIN_NAMESPACE(); uint256 baseLimit = baseGasLimitByChainNamespace[chainNamespace]; + if (baseLimit == 0) revert UniversalCoreErrors.ZeroBaseGasLimit(); if (gasLimitWithBaseLimit == 0) { gasLimitWithBaseLimit = baseLimit; @@ -314,8 +347,11 @@ contract UniversalCoreV0 is gasPrice = gasPriceByChainNamespace[chainNamespace]; if (gasPrice == 0) revert UniversalCoreErrors.ZeroGasPrice(); + _validateGasDataFreshness(chainNamespace); + gasFee = gasPrice * gasLimitWithBaseLimit; protocolFee = protocolFeeByToken[_prc20]; + gasLimitUsed = gasLimitWithBaseLimit; } /// @inheritdoc IUniversalCoreV0 @@ -343,6 +379,8 @@ contract UniversalCoreV0 is gasPrice = gasPriceByChainNamespace[chainNamespace]; if (gasPrice == 0) revert UniversalCoreErrors.ZeroGasPrice(); + _validateGasDataFreshness(chainNamespace); + gasFee = gasPrice * rescueGasLimit; } @@ -353,17 +391,20 @@ contract UniversalCoreV0 is /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function updateProtocolFeeByToken(address token, uint256 fee) external onlyRole(MANAGER_ROLE) { + function updateProtocolFeeByToken(address token, uint256 fee) external onlyRole(UVCORE_ADMIN_ROLE) { if (token == address(0)) revert CommonErrors.ZeroAddress(); protocolFeeByToken[token] = fee; emit SetProtocolFeeByToken(token, fee); } - /// @notice Set the gas PC pool for a chain. + /// @notice Set the gas PC pool for a chain (informational — not enforced at runtime). /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasToken Gas coin address /// @param fee Uniswap V3 fee tier - function updateGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) external onlyRole(MANAGER_ROLE) { + function updateGasPCPool(string memory chainNamespace, address gasToken, uint24 fee) + external + onlyRole(UVCORE_ADMIN_ROLE) + { if (gasToken == address(0)) revert CommonErrors.ZeroAddress(); address pool = IUniswapV3Factory(uniswapV3Factory) @@ -374,25 +415,14 @@ contract UniversalCoreV0 is emit SetGasPCPool(chainNamespace, pool, fee); } - /// @notice To Be Removed — use setChainMeta instead. - /// @dev Fungible module updates the gas price oracle periodically. - /// @param chainNamespace Chain Namespace - /// @param price New gas price - function setGasPrice(string memory chainNamespace, uint256 price) external onlyUEModule { - gasPriceByChainNamespace[chainNamespace] = price; - emit SetGasPrice(chainNamespace, price); - } - /// @notice Set gas price, chain height, and observation timestamp for a chain. /// @dev `observedAt` is set to `block.timestamp` of the Push Chain block /// in which this call is included. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param price Gas price on the external chain /// @param chainHeight Block height observed on the external chain - function setChainMeta(string memory chainNamespace, uint256 price, uint256 chainHeight) - external - onlyUEModule - { + function setChainMeta(string memory chainNamespace, uint256 price, uint256 chainHeight) external onlyUEModule { + if (price == 0) revert UniversalCoreErrors.ZeroGasPrice(); gasPriceByChainNamespace[chainNamespace] = price; chainHeightByChainNamespace[chainNamespace] = chainHeight; timestampObservedAtByChainNamespace[chainNamespace] = block.timestamp; @@ -402,9 +432,10 @@ contract UniversalCoreV0 is /// @notice Setter for gasTokenPRC20ByChainNamespace map. /// @param chainNamespace Chain Namespace /// @param prc20 PRC20 address - function updateGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(MANAGER_ROLE) { + function updateGasTokenPRC20(string memory chainNamespace, address prc20) external onlyRole(UVCORE_ADMIN_ROLE) { if (prc20 == address(0)) revert CommonErrors.ZeroAddress(); gasTokenPRC20ByChainNamespace[chainNamespace] = prc20; + gasPriceByChainNamespace[chainNamespace] = 0; emit SetGasToken(chainNamespace, prc20); } @@ -416,59 +447,71 @@ contract UniversalCoreV0 is /// @param prc20 PRC20 address for deposit /// @param amount Amount to deposit /// @param recipient Address to deposit tokens to - function mintPRCTokensviaAdmin(address prc20, uint256 amount, address recipient) external onlyAdmin whenNotPaused { + function mintPRCTokensviaAdmin(address prc20, uint256 amount, address recipient) + external + onlyRole(UVCORE_ADMIN_ROLE) + whenNotPaused + { _validateParams(prc20, amount, recipient); - IPRC20(prc20).deposit(recipient, amount); + if (!IPRC20(prc20).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } /// @notice Set auto-swap support for a token. /// @param token Token address /// @param supported Whether the token supports auto-swap - function updateAutoSwapSupported(address token, bool supported) external onlyAdmin { + function updateAutoSwapSupported(address token, bool supported) external onlyRole(UVCORE_ADMIN_ROLE) { isAutoSwapSupported[token] = supported; + emit SetAutoSwapSupported(token, supported); } /// @notice Set the wrapped PC address. /// @param addr WPC new address - function updateWPC(address addr) external onlyAdmin { + function updateWPC(address addr) external onlyRole(OPERATOR_ROLE) { if (addr == address(0)) revert CommonErrors.ZeroAddress(); + address oldAddr = WPC; WPC = addr; + emit SetWPC(oldAddr, addr); } /// @notice Set the UniversalGatewayPC address. /// @param addr UniversalGatewayPC address - function updateUniversalGatewayPC(address addr) external onlyAdmin { + function updateUniversalGatewayPC(address addr) external onlyRole(OPERATOR_ROLE) { if (addr == address(0)) revert CommonErrors.ZeroAddress(); + address oldAddr = universalGatewayPC; universalGatewayPC = addr; + emit SetUniversalGatewayPC(oldAddr, addr); } /// @notice Setter for Uniswap V3 addresses. /// @param factory Uniswap V3 Factory address /// @param swapRouter Uniswap V3 SwapRouter address - /// @param quoter Uniswap V3 Quoter address - function updateUniswapV3Addresses(address factory, address swapRouter, address quoter) external onlyAdmin { - if (factory == address(0) || swapRouter == address(0) || quoter == address(0)) { + function updateUniswapV3Addresses(address factory, address swapRouter) external onlyRole(OPERATOR_ROLE) { + if (factory == address(0) || swapRouter == address(0)) { revert CommonErrors.ZeroAddress(); } uniswapV3Factory = factory; uniswapV3SwapRouter = swapRouter; - __deprecated_uniswapV3Quoter = quoter; + emit SetUniswapV3Addresses(factory, swapRouter); } /// @notice Set default fee tier for a token. /// @param token Token address - /// @param feeTier Fee tier (500, 3000, 10000) - function updateDefaultFeeTier(address token, uint24 feeTier) external onlyAdmin { + /// @param feeTier Fee tier (100, 500, 3000, 10000) + function updateDefaultFeeTier(address token, uint24 feeTier) external onlyRole(UVCORE_ADMIN_ROLE) { if (token == address(0)) revert CommonErrors.ZeroAddress(); - if (feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM && feeTier != FEE_TIER_HIGH) { + if ( + feeTier != FEE_TIER_LOWEST && feeTier != FEE_TIER_LOW && feeTier != FEE_TIER_MEDIUM + && feeTier != FEE_TIER_HIGH + ) { revert UniversalCoreErrors.InvalidFeeTier(); } defaultFeeTier[token] = feeTier; + emit SetDefaultFeeTier(token, feeTier); } /// @notice Set default deadline in minutes. /// @param minutesValue Default deadline in minutes - function updateDefaultDeadlineMins(uint256 minutesValue) external onlyAdmin { + function updateDefaultDeadlineMins(uint256 minutesValue) external onlyRole(UVCORE_ADMIN_ROLE) { defaultDeadlineMins = minutesValue; emit SetDefaultDeadlineMins(minutesValue); } @@ -476,7 +519,10 @@ contract UniversalCoreV0 is /// @notice Set base gas limit for a specific chain. /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) /// @param gasLimit Base gas limit for the chain - function updateBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external onlyRole(MANAGER_ROLE) { + function updateBaseGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + external + onlyRole(UVCORE_ADMIN_ROLE) + { baseGasLimitByChainNamespace[chainNamespace] = gasLimit; emit SetBaseGasLimitByChain(chainNamespace, gasLimit); } @@ -486,26 +532,52 @@ contract UniversalCoreV0 is /// @param gasLimit Rescue funds gas limit for the chain function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external - onlyRole(MANAGER_ROLE) + onlyRole(UVCORE_ADMIN_ROLE) { rescueFundsGasLimitByChainNamespace[chainNamespace] = gasLimit; emit SetRescueFundsGasLimitByChain(chainNamespace, gasLimit); } - /// @notice Pause the contract - stops all deposit functions. - function pause() external onlyAdmin { + /// @notice Set the maximum acceptable age (seconds) of gas data for a chain. + /// @dev A value of `0` disables the staleness check for that chain (opt-in). + /// When set, `getOutboundTxGasAndFees` and `getRescueFundsGasLimit` + /// revert with `StaleGasData` if the chain's observed timestamp is + /// older than `block.timestamp - maxStaleness`. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param maxStaleness Maximum acceptable age of gas data in seconds (0 disables) + function updateMaxStalenessByChain(string memory chainNamespace, uint256 maxStaleness) + external + onlyRole(UVCORE_ADMIN_ROLE) + { + maxStalenessByChainNamespace[chainNamespace] = maxStaleness; + emit SetMaxStalenessByChain(chainNamespace, maxStaleness); + } + + /// @notice Pause the contract - stops all deposit functions. Only callable by PAUSER_ROLE. + function pause() external onlyRole(PAUSER_ROLE) { _pause(); } - /// @notice Unpause the contract - resumes all deposit functions. - function unpause() external onlyAdmin { + /// @notice Unpause the contract - resumes all deposit functions. Only callable by OPERATOR_ROLE. + function unpause() external onlyRole(OPERATOR_ROLE) { _unpause(); } + /// @notice Rescue native PC stuck in the contract. Only callable by UVCORE_ADMIN_ROLE. + /// @param to Recipient address for the rescued PC + /// @param amount Amount of native PC to rescue + function rescueNativePC(address payable to, uint256 amount) external onlyRole(UVCORE_ADMIN_ROLE) { + if (to == address(0)) revert CommonErrors.ZeroAddress(); + if (amount == 0) revert CommonErrors.ZeroAmount(); + if (amount > address(this).balance) revert CommonErrors.InsufficientBalance(); + (bool ok,) = to.call{value: amount}(""); + if (!ok) revert CommonErrors.TransferFailed(); + emit RescueNativePC(to, amount); + } + // ========================= // UCV0_6: PRIVATE HELPERS // ========================= - /// @dev Shared input validation for deposit/refund functions. /// @param token Token address to validate /// @param amount Amount to validate (must be > 0) @@ -519,6 +591,21 @@ contract UniversalCoreV0 is if (amount == 0) revert CommonErrors.ZeroAmount(); } + /// @dev Enforces that gas data for `chainNamespace` is within the configured freshness + /// window. No-op when `maxStalenessByChainNamespace[chainNamespace]` is `0` + /// (check disabled). Reverts with `StaleGasData` when the data is older than + /// the configured max age, carrying the observed timestamp, current timestamp, + /// and max age in the revert data. + /// @param chainNamespace Chain namespace whose freshness is being validated + function _validateGasDataFreshness(string memory chainNamespace) private view { + uint256 maxAge = maxStalenessByChainNamespace[chainNamespace]; + if (maxAge == 0) return; + uint256 observedAt = timestampObservedAtByChainNamespace[chainNamespace]; + if (block.timestamp > observedAt + maxAge) { + revert UniversalCoreErrors.StaleGasData(observedAt, block.timestamp, maxAge); + } + } + /// @dev Swap PRC20 to native PC via Uniswap V3 and send to recipient. /// @param prc20 PRC20 token address to swap /// @param amount Amount of PRC20 to swap @@ -553,8 +640,8 @@ contract UniversalCoreV0 is if (minPCOut == 0) revert CommonErrors.ZeroAmount(); - IPRC20(prc20).deposit(address(this), amount); - IPRC20(prc20).approve(uniswapV3SwapRouter, amount); + if (!IPRC20(prc20).deposit(address(this), amount)) revert UniversalCoreErrors.PRC20OperationFailed(); + IERC20(prc20).forceApprove(uniswapV3SwapRouter, amount); ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: prc20, @@ -570,7 +657,7 @@ contract UniversalCoreV0 is pcOut = ISwapRouter(uniswapV3SwapRouter).exactInputSingle(params); if (pcOut < minPCOut) revert UniversalCoreErrors.SlippageExceeded(); - IPRC20(prc20).approve(uniswapV3SwapRouter, 0); + IERC20(prc20).forceApprove(uniswapV3SwapRouter, 0); IWPC(WPC).withdraw(pcOut); (bool ok,) = recipient.call{value: pcOut}(""); From a2b61850d765679aa9e1e39998a47a348404bc08 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 7 May 2026 12:55:01 +0530 Subject: [PATCH 51/56] bug-fix-l1 setter functions --- src/Interfaces/IUniversalCore.sol | 12 ++ src/UniversalCore.sol | 25 ++++ src/testnetV0/IUniversalCoreV0.sol | 117 ++++++------------ src/testnetV0/UniversalCoreV0.sol | 25 ++++ test/tests_token_and_core/UniversalCore.t.sol | 60 +++++++++ 5 files changed, 159 insertions(+), 80 deletions(-) diff --git a/src/Interfaces/IUniversalCore.sol b/src/Interfaces/IUniversalCore.sol index 0df2af3..70d014d 100644 --- a/src/Interfaces/IUniversalCore.sol +++ b/src/Interfaces/IUniversalCore.sol @@ -21,6 +21,8 @@ interface IUniversalCore { event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); + event SetL1GasFeeByChain(string chainNamespace, uint256 l1GasFee); + event SetTssFundMigrationGasLimitByChain(string chainNamespace, uint256 gasLimit); event RefundUnusedGas( address indexed gasToken, uint256 amount, address indexed recipient, bool swapped, uint256 pcOut ); @@ -192,6 +194,16 @@ interface IUniversalCore { /// @param gasLimit Rescue funds gas limit for the chain function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; + /// @notice Set L1 gas fee for a specific chain. + /// @param chainNamespace Chain Namespace + /// @param l1GasFee L1 gas fee for the chain (in gas token units) + function setL1GasFeeByChain(string memory chainNamespace, uint256 l1GasFee) external; + + /// @notice Set TSS migration gas limit for a specific chain. + /// @param chainNamespace Chain Namespace + /// @param gasLimit TSS migration gas limit for the chain + function setTssFundMigrationGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; + /// @notice Get the UniversalGatewayPC address. function universalGatewayPC() external view returns (address); } diff --git a/src/UniversalCore.sol b/src/UniversalCore.sol index 501838c..33af5de 100644 --- a/src/UniversalCore.sol +++ b/src/UniversalCore.sol @@ -93,6 +93,12 @@ contract UniversalCore is mapping(address => uint24) public defaultFeeTier; uint256 public defaultDeadlineMins; + /// @notice L1 gas fee per chain namespace (in gas token units). + mapping(string => uint256) public l1GasFeeByChainNamespace; + + /// @notice TSS fund migration gas limit per chain namespace. + mapping(string => uint256) public tssFundMigrationGasLimitByChainNamespace; + // ========================= // UC: MODIFIERS // ========================= @@ -500,6 +506,25 @@ contract UniversalCore is emit SetMaxStalenessByChain(chainNamespace, maxStaleness); } + /// @notice Set L1 gas fee for a specific chain. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param l1GasFee L1 gas fee for the chain (in gas token units) + function setL1GasFeeByChain(string memory chainNamespace, uint256 l1GasFee) external onlyRole(UVCORE_ADMIN_ROLE) { + l1GasFeeByChainNamespace[chainNamespace] = l1GasFee; + emit SetL1GasFeeByChain(chainNamespace, l1GasFee); + } + + /// @notice Set TSS migration gas limit for a specific chain. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param gasLimit TSS migration gas limit for the chain + function setTssFundMigrationGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + external + onlyRole(UVCORE_ADMIN_ROLE) + { + tssFundMigrationGasLimitByChainNamespace[chainNamespace] = gasLimit; + emit SetTssFundMigrationGasLimitByChain(chainNamespace, gasLimit); + } + /// @notice Pause the contract - stops all deposit functions. Only callable by PAUSER_ROLE. function pause() external onlyRole(PAUSER_ROLE) { _pause(); diff --git a/src/testnetV0/IUniversalCoreV0.sol b/src/testnetV0/IUniversalCoreV0.sol index c71edb1..e7731f9 100644 --- a/src/testnetV0/IUniversalCoreV0.sol +++ b/src/testnetV0/IUniversalCoreV0.sol @@ -9,52 +9,28 @@ interface IUniversalCoreV0 { // UCV0: EVENTS // ========================= - event SetChainMeta( - string chainNamespace, - uint256 price, - uint256 chainHeight, - uint256 observedAt - ); + event SetChainMeta(string chainNamespace, uint256 price, uint256 chainHeight, uint256 observedAt); event SetGasToken(string chainNamespace, address prc20); event SetDefaultDeadlineMins(uint256 minutesValue); event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); + event SetL1GasFeeByChain(string chainNamespace, uint256 l1GasFee); + event SetTssFundMigrationGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetAutoSwapSupported(address indexed token, bool supported); event SetWPC(address indexed oldAddr, address indexed newAddr); event SetUniversalGatewayPC(address indexed oldAddr, address indexed newAddr); event SetUniswapV3Addresses(address factory, address swapRouter); event SetDefaultFeeTier(address indexed token, uint24 feeTier); event RescueNativePC(address indexed to, uint256 amount); - event SetGasPCPool( - string chainNamespace, address pool, uint24 fee - ); + event SetGasPCPool(string chainNamespace, address pool, uint24 fee); event DepositPRC20WithAutoSwap( - address prc20, - uint256 amountIn, - address pcToken, - uint256 amountOut, - uint24 fee, - address recipient - ); - event SwapAndBurnGas( - address indexed gasToken, - uint256 pcIn, - uint256 gasFee, - uint24 fee, - address indexed caller + address prc20, uint256 amountIn, address pcToken, uint256 amountOut, uint24 fee, address recipient ); + event SwapAndBurnGas(address indexed gasToken, uint256 pcIn, uint256 gasFee, uint24 fee, address indexed caller); event SetProtocolFeeByToken(address indexed token, uint256 fee); - event SetBaseGasLimitByChain( - string chainNamespace, uint256 gasLimit - ); - event SetRescueFundsGasLimitByChain( - string chainNamespace, uint256 gasLimit - ); + event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); + event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); event RefundUnusedGas( - address indexed gasToken, - uint256 amount, - address indexed recipient, - bool swapped, - uint256 pcOut + address indexed gasToken, uint256 amount, address indexed recipient, bool swapped, uint256 pcOut ); // ========================= @@ -65,11 +41,7 @@ interface IUniversalCoreV0 { /// @param prc20 PRC20 address for deposit /// @param amount Amount to deposit /// @param recipient Address to deposit tokens to - function depositPRC20Token( - address prc20, - uint256 amount, - address recipient - ) external; + function depositPRC20Token(address prc20, uint256 amount, address recipient) external; /// @notice Deposits PRC20 tokens and automatically swaps them to /// native PC before sending to recipient. @@ -116,13 +88,10 @@ interface IUniversalCoreV0 { /// @param caller Address to receive unused PC refund /// @return gasTokenOut Total gas token swapped (gasFee) /// @return refund Unused PC refunded to caller - function swapAndBurnGas( - address gasToken, - uint24 fee, - uint256 gasFee, - uint256 deadline, - address caller - ) external payable returns (uint256 gasTokenOut, uint256 refund); + function swapAndBurnGas(address gasToken, uint24 fee, uint256 gasFee, uint256 deadline, address caller) + external + payable + returns (uint256 gasTokenOut, uint256 refund); // ========================= // UCV0_3: PUBLIC GETTERS @@ -131,30 +100,25 @@ interface IUniversalCoreV0 { /// @notice Get gas token PRC20 address for a chain. /// @param chainNamespace Chain Namespace /// @return gasToken Gas token address - function gasTokenPRC20ByChainNamespace( - string memory chainNamespace - ) external view returns (address gasToken); + function gasTokenPRC20ByChainNamespace(string memory chainNamespace) external view returns (address gasToken); /// @notice Get gas price for a chain. /// @param chainNamespace Chain Namespace /// @return price Gas price - function gasPriceByChainNamespace( - string memory chainNamespace - ) external view returns (uint256 price); + function gasPriceByChainNamespace(string memory chainNamespace) external view returns (uint256 price); /// @notice Get base gas limit for a chain. /// @param chainNamespace Chain Namespace /// @return baseGasLimit Base gas limit for the chain - function baseGasLimitByChainNamespace( - string memory chainNamespace - ) external view returns (uint256 baseGasLimit); + function baseGasLimitByChainNamespace(string memory chainNamespace) external view returns (uint256 baseGasLimit); /// @notice Get rescue funds gas limit for a chain. /// @param chainNamespace Chain Namespace /// @return rescueGasLimit Rescue funds gas limit for the chain - function rescueFundsGasLimitByChainNamespace( - string memory chainNamespace - ) external view returns (uint256 rescueGasLimit); + function rescueFundsGasLimitByChainNamespace(string memory chainNamespace) + external + view + returns (uint256 rescueGasLimit); /// @notice Get gas fee for a PRC20 token, split into gasFee and protocolFee. /// @dev When gasLimitWithBaseLimit is 0, falls back to per-chain base gas limit. @@ -168,10 +132,7 @@ interface IUniversalCoreV0 { /// @return gasPrice Gas price on the external chain /// @return chainNamespace Source chain namespace /// @return gasLimitUsed Effective gas limit used in calculation - function getOutboundTxGasAndFees( - address _prc20, - uint256 gasLimitWithBaseLimit - ) + function getOutboundTxGasAndFees(address _prc20, uint256 gasLimitWithBaseLimit) external view returns ( @@ -190,9 +151,7 @@ interface IUniversalCoreV0 { /// @return rescueGasLimit Rescue funds gas limit for the chain /// @return gasPrice Gas price on the external chain /// @return chainNamespace Source chain namespace - function getRescueFundsGasLimit( - address _prc20 - ) + function getRescueFundsGasLimit(address _prc20) external view returns ( @@ -206,25 +165,27 @@ interface IUniversalCoreV0 { /// @notice Get the protocol fee (in native PC) for a given token. /// @param token Token address /// @return Protocol fee amount in native PC - function protocolFeeByToken( - address token - ) external view returns (uint256); + function protocolFeeByToken(address token) external view returns (uint256); /// @notice Set protocol fee (in native PC) for a token. /// @param token Token address /// @param fee Protocol fee amount in native PC - function updateProtocolFeeByToken( - address token, - uint256 fee - ) external; + function updateProtocolFeeByToken(address token, uint256 fee) external; /// @notice Set rescue funds gas limit for a specific chain. /// @param chainNamespace Chain Namespace /// @param gasLimit Rescue funds gas limit for the chain - function updateRescueFundsGasLimitByChain( - string memory chainNamespace, - uint256 gasLimit - ) external; + function updateRescueFundsGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; + + /// @notice Set L1 gas fee for a specific chain. + /// @param chainNamespace Chain Namespace + /// @param l1GasFee L1 gas fee for the chain (in gas token units) + function setL1GasFeeByChain(string memory chainNamespace, uint256 l1GasFee) external; + + /// @notice Set TSS migration gas limit for a specific chain. + /// @param chainNamespace Chain Namespace + /// @param gasLimit TSS migration gas limit for the chain + function setTssFundMigrationGasLimitByChain(string memory chainNamespace, uint256 gasLimit) external; /// @notice Get the UniversalGatewayPC address. function universalGatewayPC() external view returns (address); @@ -237,9 +198,5 @@ interface IUniversalCoreV0 { /// @param prc20 PRC20 address to mint /// @param amount Amount to mint /// @param recipient Address to receive minted tokens - function mintPRCTokensviaAdmin( - address prc20, - uint256 amount, - address recipient - ) external; + function mintPRCTokensviaAdmin(address prc20, uint256 amount, address recipient) external; } diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index af76cc2..2e1cf2b 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -131,6 +131,12 @@ contract UniversalCoreV0 is /// @notice Maximum acceptable age (seconds) of gas data before quotes are rejected as stale. mapping(string => uint256) public maxStalenessByChainNamespace; + /// @notice L1 gas fee per chain namespace (in gas token units). + mapping(string => uint256) public l1GasFeeByChainNamespace; + + /// @notice TSS fund migration gas limit per chain namespace. + mapping(string => uint256) public tssFundMigrationGasLimitByChainNamespace; + // ========================= // UCV0: MODIFIERS // ========================= @@ -553,6 +559,25 @@ contract UniversalCoreV0 is emit SetMaxStalenessByChain(chainNamespace, maxStaleness); } + /// @notice Set L1 gas fee for a specific chain. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param l1GasFee L1 gas fee for the chain (in gas token units) + function setL1GasFeeByChain(string memory chainNamespace, uint256 l1GasFee) external onlyRole(UVCORE_ADMIN_ROLE) { + l1GasFeeByChainNamespace[chainNamespace] = l1GasFee; + emit SetL1GasFeeByChain(chainNamespace, l1GasFee); + } + + /// @notice Set TSS migration gas limit for a specific chain. + /// @param chainNamespace Chain Namespace (e.g. "eip155:1" for Ethereum Mainnet) + /// @param gasLimit TSS migration gas limit for the chain + function setTssFundMigrationGasLimitByChain(string memory chainNamespace, uint256 gasLimit) + external + onlyRole(UVCORE_ADMIN_ROLE) + { + tssFundMigrationGasLimitByChainNamespace[chainNamespace] = gasLimit; + emit SetTssFundMigrationGasLimitByChain(chainNamespace, gasLimit); + } + /// @notice Pause the contract - stops all deposit functions. Only callable by PAUSER_ROLE. function pause() external onlyRole(PAUSER_ROLE) { _pause(); diff --git a/test/tests_token_and_core/UniversalCore.t.sol b/test/tests_token_and_core/UniversalCore.t.sol index 6162e8e..71d0ef1 100644 --- a/test/tests_token_and_core/UniversalCore.t.sol +++ b/test/tests_token_and_core/UniversalCore.t.sol @@ -68,6 +68,8 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { event SetBaseGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetRescueFundsGasLimitByChain(string chainNamespace, uint256 gasLimit); event SetMaxStalenessByChain(string chainNamespace, uint256 maxStaleness); + event SetL1GasFeeByChain(string chainNamespace, uint256 l1GasFee); + event SetTssFundMigrationGasLimitByChain(string chainNamespace, uint256 gasLimit); event RescueNativePC(address indexed to, uint256 amount); function setUp() public { @@ -1629,4 +1631,62 @@ contract UniversalCoreTest is Test, UpgradeableContractHelper { vm.prank(pauser); universalCore.unpause(); } + + // ============================================ + // L1 GAS FEE & TSS MIGRATION GAS LIMIT + // ============================================ + + function test_SetL1GasFeeByChain_HappyPath() public { + uint256 l1Fee = 0.001 ether; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + vm.expectEmit(false, false, false, true); + emit SetL1GasFeeByChain(CHAIN_NAMESPACE, l1Fee); + universalCore.setL1GasFeeByChain(CHAIN_NAMESPACE, l1Fee); + + assertEq(universalCore.l1GasFeeByChainNamespace(CHAIN_NAMESPACE), l1Fee); + } + + function test_SetL1GasFeeByChain_OnlyUVCoreAdmin() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.UVCORE_ADMIN_ROLE() + ) + ); + vm.prank(nonOwner); + universalCore.setL1GasFeeByChain(CHAIN_NAMESPACE, 0.001 ether); + } + + function test_SetL1GasFeeByChain_ZeroValueAllowed() public { + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setL1GasFeeByChain(CHAIN_NAMESPACE, 0); + assertEq(universalCore.l1GasFeeByChainNamespace(CHAIN_NAMESPACE), 0); + } + + function test_SetTssFundMigrationGasLimitByChain_HappyPath() public { + uint256 gasLimit = 1_000_000; + + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + vm.expectEmit(false, false, false, true); + emit SetTssFundMigrationGasLimitByChain(CHAIN_NAMESPACE, gasLimit); + universalCore.setTssFundMigrationGasLimitByChain(CHAIN_NAMESPACE, gasLimit); + + assertEq(universalCore.tssFundMigrationGasLimitByChainNamespace(CHAIN_NAMESPACE), gasLimit); + } + + function test_SetTssFundMigrationGasLimitByChain_OnlyUVCoreAdmin() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, nonOwner, universalCore.UVCORE_ADMIN_ROLE() + ) + ); + vm.prank(nonOwner); + universalCore.setTssFundMigrationGasLimitByChain(CHAIN_NAMESPACE, 1_000_000); + } + + function test_SetTssFundMigrationGasLimitByChain_ZeroValueAllowed() public { + vm.prank(UNIVERSAL_EXECUTOR_MODULE); + universalCore.setTssFundMigrationGasLimitByChain(CHAIN_NAMESPACE, 0); + assertEq(universalCore.tssFundMigrationGasLimitByChainNamespace(CHAIN_NAMESPACE), 0); + } } From 750da6209ba135e2a4946f66e77445fc3343849a Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 7 May 2026 13:41:42 +0530 Subject: [PATCH 52/56] naming issue fi --- foundry.toml | 5 +++- ...UniversalCoreV0.sol => IUniversalCore.sol} | 8 +++---- src/testnetV0/UniversalCoreV0.sol | 24 +++++++++---------- test/fork/ForkUniversalCore.t.sol | 7 +----- test/tests_cea/CEA.t.sol | 15 +++--------- test/tests_ueaMigration/BaseTest.t.sol | 3 ++- test/tests_uea_and_factory/UEAFactory.t.sol | 5 ++-- .../tests_uea_and_factory/UEAProxyCalls.t.sol | 3 ++- test/tests_uea_and_factory/UEA_SVM.t.sol | 5 ++-- 9 files changed, 33 insertions(+), 42 deletions(-) rename src/testnetV0/{IUniversalCoreV0.sol => IUniversalCore.sol} (98%) diff --git a/foundry.toml b/foundry.toml index 0e8ad26..3515680 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,10 +16,13 @@ auto_detect_solc = false evm_version = "shanghai" via_ir = true -no_match_coverage = "(PRC20V0\\.sol|UniversalCoreV0\\.sol|ReceiverExample\\.sol|src/(libraries|[Ii]nterfaces|mocks)/|test/)" +no_match_coverage = "(PRC20V0\\.sol|testnetV0/UniversalCore\\.sol|ReceiverExample\\.sol|src/(libraries|[Ii]nterfaces|mocks)/|test/)" fs_permissions = [{ access = "read-write", path = "deployments/" }] +[fmt] +ignore = ["src/**"] + [fuzz] runs = 1024 max_test_rejects = 65536 diff --git a/src/testnetV0/IUniversalCoreV0.sol b/src/testnetV0/IUniversalCore.sol similarity index 98% rename from src/testnetV0/IUniversalCoreV0.sol rename to src/testnetV0/IUniversalCore.sol index e7731f9..c60979f 100644 --- a/src/testnetV0/IUniversalCoreV0.sol +++ b/src/testnetV0/IUniversalCore.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -/// @title IUniversalCoreV0 -/// @notice Interface for the UniversalCoreV0 (testnet) contract. -/// @dev Standalone interface dedicated to UniversalCoreV0. -interface IUniversalCoreV0 { +/// @title IUniversalCore +/// @notice Interface for the UniversalCore (testnet) contract. +/// @dev Standalone interface dedicated to testnet UniversalCore. +interface IUniversalCore { // ========================= // UCV0: EVENTS // ========================= diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index 2e1cf2b..a25c04b 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -11,25 +11,25 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {IPRC20} from "../interfaces/IPRC20.sol"; -import {IUniversalCoreV0} from "./IUniversalCoreV0.sol"; +import {IUniversalCore} from "./IUniversalCore.sol"; import {IUniswapV3Factory, ISwapRouter} from "../interfaces/uniswapv3/IUniswapV3.sol"; import {IWPC} from "../interfaces/IWPC.sol"; import {UniversalCoreErrors, CommonErrors} from "../libraries/Errors.sol"; /** - * @title UniversalCoreV0 + * @title UniversalCore (testnet) * @notice Temporary UniversalCore contract for Push Chain TESTNET. - * The UniversalCoreV0 acts as the core contract for all functionalities + * The UniversalCore acts as the core contract for all functionalities * needed by the interoperability feature of Push Chain. - * @dev The UniversalCoreV0 primarily handles the following functionalities: + * @dev The UniversalCore primarily handles the following functionalities: * - Generation of supported PRC-20 tokens, and transferring it to accurate recipients. * - Setting up the gas tokens for each chain. * - Setting up the gas price for each chain. * - Maintaining a registry of Uniswap V3 pools for each token pair. * @dev All imperative functionalities are handled by the Universal Executor Module. */ -contract UniversalCoreV0 is - IUniversalCoreV0, +contract UniversalCore is + IUniversalCore, Initializable, ReentrancyGuardUpgradeable, AccessControlDefaultAdminRulesUpgradeable, @@ -205,7 +205,7 @@ contract UniversalCoreV0 is // UCV0_1: UE MODULE ACTIONS // ========================= - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function depositPRC20Token(address prc20, uint256 amount, address recipient) external onlyUEModule @@ -216,7 +216,7 @@ contract UniversalCoreV0 is if (!IPRC20(prc20).deposit(recipient, amount)) revert UniversalCoreErrors.PRC20OperationFailed(); } - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function depositPRC20WithAutoSwap( address prc20, uint256 amount, @@ -232,7 +232,7 @@ contract UniversalCoreV0 is emit DepositPRC20WithAutoSwap(prc20, amount, WPC, pcOut, resolvedFee, recipient); } - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function refundUnusedGas( address gasToken, uint256 amount, @@ -261,7 +261,7 @@ contract UniversalCoreV0 is // UCV0_2: GATEWAY ACTIONS // ========================= - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function swapAndBurnGas(address gasToken, uint24 fee, uint256 gasFee, uint256 deadline, address caller) external payable @@ -324,7 +324,7 @@ contract UniversalCoreV0 is // UCV0_3: PUBLIC GETTERS // ========================= - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function getOutboundTxGasAndFees(address _prc20, uint256 gasLimitWithBaseLimit) public view @@ -360,7 +360,7 @@ contract UniversalCoreV0 is gasLimitUsed = gasLimitWithBaseLimit; } - /// @inheritdoc IUniversalCoreV0 + /// @inheritdoc IUniversalCore function getRescueFundsGasLimit(address _prc20) public view diff --git a/test/fork/ForkUniversalCore.t.sol b/test/fork/ForkUniversalCore.t.sol index 5339598..adc8a46 100644 --- a/test/fork/ForkUniversalCore.t.sol +++ b/test/fork/ForkUniversalCore.t.sol @@ -62,12 +62,7 @@ contract ForkUniversalCoreTest is Test, UpgradeableContractHelper, PushChainAddr // Deploy UniversalCore behind proxy UniversalCore implementation = new UniversalCore(); bytes memory initData = abi.encodeWithSelector( - UniversalCore.initialize.selector, - deployer, - makeAddr("pauser"), - WPC_TOKEN, - UNISWAP_FACTORY, - UNISWAP_ROUTER + UniversalCore.initialize.selector, deployer, makeAddr("pauser"), WPC_TOKEN, UNISWAP_FACTORY, UNISWAP_ROUTER ); address proxyAddress = deployUpgradeableContract(address(implementation), initData); universalCore = UniversalCore(payable(proxyAddress)); diff --git a/test/tests_cea/CEA.t.sol b/test/tests_cea/CEA.t.sol index 102f89c..58fde63 100644 --- a/test/tests_cea/CEA.t.sol +++ b/test/tests_cea/CEA.t.sol @@ -918,11 +918,7 @@ contract CEATest is Test { // Create multicall with wrong selector (try to call initializeCEA) Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( - address(ceaInstance), - 0, - abi.encodeWithSignature( - "initializeCEA(address,address)", address(0), address(0) - ) + address(ceaInstance), 0, abi.encodeWithSignature("initializeCEA(address,address)", address(0), address(0)) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1371,11 +1367,7 @@ contract CEATest is Test { // Create multicall with wrong selector (try to call initializeCEA) Multicall[] memory calls = new Multicall[](1); calls[0] = makeCall( - address(ceaInstance), - 0, - abi.encodeWithSignature( - "initializeCEA(address,address)", address(0), address(0) - ) + address(ceaInstance), 0, abi.encodeWithSignature("initializeCEA(address,address)", address(0), address(0)) ); bytes memory multicallPayload = encodeCalls(calls); @@ -1755,8 +1747,7 @@ contract CEATest is Test { function testInitializeCEA_CannotBeCalledAgainAfterProxyDeployment() public deployCEA { vm.expectRevert(Errors.AlreadyInitialized.selector); - CEA(payable(address(ceaInstance))) - .initializeCEA(ueaOnPush, address(factory)); + CEA(payable(address(ceaInstance))).initializeCEA(ueaOnPush, address(factory)); } function testReceive_DirectETHTransferSucceeds() public deployCEA { diff --git a/test/tests_ueaMigration/BaseTest.t.sol b/test/tests_ueaMigration/BaseTest.t.sol index 908166d..298dd02 100644 --- a/test/tests_ueaMigration/BaseTest.t.sol +++ b/test/tests_ueaMigration/BaseTest.t.sol @@ -174,7 +174,8 @@ contract BaseTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser"), "42101"); + bytes memory initData = + abi.encodeWithSelector(UEAFactory.initialize.selector, deployer, makeAddr("pauser"), "42101"); ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(factoryProxy)); diff --git a/test/tests_uea_and_factory/UEAFactory.t.sol b/test/tests_uea_and_factory/UEAFactory.t.sol index fd326dc..df9b638 100644 --- a/test/tests_uea_and_factory/UEAFactory.t.sol +++ b/test/tests_uea_and_factory/UEAFactory.t.sol @@ -13,8 +13,9 @@ import {UEAErrors as Errors} from "../../src/libraries/Errors.sol"; import {IUEA} from "../../src/interfaces/IUEA.sol"; import {IUEAFactory} from "../../src/Interfaces/IUEAFactory.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; -import {IAccessControlDefaultAdminRules} from - "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UEAProxy} from "../../src/uea/UEAProxy.sol"; diff --git a/test/tests_uea_and_factory/UEAProxyCalls.t.sol b/test/tests_uea_and_factory/UEAProxyCalls.t.sol index 6c97f17..414d01a 100644 --- a/test/tests_uea_and_factory/UEAProxyCalls.t.sol +++ b/test/tests_uea_and_factory/UEAProxyCalls.t.sol @@ -52,7 +52,8 @@ contract ProxyCallTest is Test { UEAFactory factoryImpl = new UEAFactory(); - bytes memory initData = abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser"), "42101"); + bytes memory initData = + abi.encodeWithSelector(UEAFactory.initialize.selector, admin, makeAddr("pauser"), "42101"); ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImpl), initData); factory = UEAFactory(address(proxy)); diff --git a/test/tests_uea_and_factory/UEA_SVM.t.sol b/test/tests_uea_and_factory/UEA_SVM.t.sol index 596b5a7..cfeae69 100644 --- a/test/tests_uea_and_factory/UEA_SVM.t.sol +++ b/test/tests_uea_and_factory/UEA_SVM.t.sol @@ -958,9 +958,8 @@ contract UEASVMTest is Test { // This test verifies that the DOMAIN_SEPARATOR_TYPEHASH_SVM constant matches the expected hash // If the EIP712Domain_SVM struct definition changes, this test will fail - bytes32 expectedHash = keccak256( - "EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)" - ); + bytes32 expectedHash = + keccak256("EIP712Domain_SVM(string version,string chainId,address verifyingContract,bytes32 salt)"); // Access the constant from the deployed instance bytes32 actualHash = svmSmartAccountInstance.DOMAIN_SEPARATOR_TYPEHASH_SVM(); From 45d359831cae0c5f226113559b4a90295aa3f121 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 7 May 2026 13:45:06 +0530 Subject: [PATCH 53/56] naming issue fix --- src/testnetV0/UniversalCoreV0.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index a25c04b..2e22084 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -72,7 +72,7 @@ contract UniversalCore is address public uniswapV3SwapRouter; /// @dev Deprecated. Slot retained for storage layout compatibility with deployed testnet proxy. - address private __deprecated_uniswapV3Quoter; + address private uniswapV3Quoter; /// @notice Address of the wrapped PC to interact with Uniswap V3. address public WPC; @@ -180,7 +180,7 @@ contract UniversalCore is WPC = wpc_; uniswapV3Factory = uniswapV3Factory_; uniswapV3SwapRouter = uniswapV3SwapRouter_; - __deprecated_uniswapV3Quoter = uniswapV3Quoter_; + uniswapV3Quoter = uniswapV3Quoter_; } /// @dev Reinitializer to migrate to granular RBAC with admin transfer delay. From daad043cb2fe04b6323a760059d06ffa3de81673 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 12 May 2026 15:51:31 +0530 Subject: [PATCH 54/56] post-audit-cea-addresses --- docs/addresses/arbitrum_sepolia.md | 12 ++++++++++++ docs/addresses/base_sepolia.md | 12 ++++++++++++ docs/addresses/bsc_testnet.md | 21 +++++++++++---------- docs/addresses/eth_sepolia.md | 12 ++++++++++++ docs/addresses/sepolia.md | 0 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 docs/addresses/arbitrum_sepolia.md create mode 100644 docs/addresses/base_sepolia.md create mode 100644 docs/addresses/eth_sepolia.md delete mode 100644 docs/addresses/sepolia.md diff --git a/docs/addresses/arbitrum_sepolia.md b/docs/addresses/arbitrum_sepolia.md new file mode 100644 index 0000000..01c1bdb --- /dev/null +++ b/docs/addresses/arbitrum_sepolia.md @@ -0,0 +1,12 @@ +# Arbitrum Sepolia (Chain ID: 421614) + +| Contract | Address | +| --------------------------- | -------------------------------------------- | +| CEA (logic) | `0x2c933Ff6FBcD479055F344691bc628F51DcE871A` | +| CEAProxy (clone template) | `0x512d1B8C185a0Fd533a69f8973A3DD6513233009` | +| CEAFactory (implementation) | `0xd8335e762E42b7f9610293707d6d8A6b97578bFb` | +| ProxyAdmin | `0x6349546d872d483A35bdD165c9ef85757e064D4E` | +| CEAFactory (proxy) | `0x88DC189275078Cf509E4Cc773F089c8ad07b7EA2` | +| CEA_V2 (logic) | `0xe23741BffF1dAac6f98cEC84ce3EBAfeF1Cd5965` | +| CEAMigration | `0x81f33160020AaDF47000E85915d332943b69F9f9` | +| CEA (post-audit) | `0x0D74144cED066a1f3BA94887ED9a6443F7bD26c5` | diff --git a/docs/addresses/base_sepolia.md b/docs/addresses/base_sepolia.md new file mode 100644 index 0000000..25da623 --- /dev/null +++ b/docs/addresses/base_sepolia.md @@ -0,0 +1,12 @@ +# Base Sepolia (Chain ID: 84532) + +| Contract | Address | +| --------------------------- | -------------------------------------------- | +| CEA (logic) | `0x733078bA1dFDDDB68A9E082696A256AEcBFb26b8` | +| CEAProxy (clone template) | `0x6c9Cfef12155bEE91ecD6d0C8f516cABA2890656` | +| CEAFactory (implementation) | `0xd26E793Ef931EB62AeBc6e87DE1FEEF4fDbA01F5` | +| ProxyAdmin | `0x413A39fFA85657A25768799f7fd64A917eceDe48` | +| CEAFactory (proxy) | `0x0A75ca7736b488Eb41675ADc3b3156BACF659F55` | +| CEA_V2 (logic) | `0x6085C657A6d12F789a388D96730D248b74437730` | +| CEAMigration | `0x95c453fDFf55Afc5754c1fA95Ad6607273D71B20` | +| CEA (post-audit) | `0xF9EAf33bAB2f7f21bEe1712Ce9688Cf29053BfFb` | diff --git a/docs/addresses/bsc_testnet.md b/docs/addresses/bsc_testnet.md index aeaee3c..202cea1 100644 --- a/docs/addresses/bsc_testnet.md +++ b/docs/addresses/bsc_testnet.md @@ -1,11 +1,12 @@ - # BSC Testnet (Chain ID: 97) +# BSC Testnet (Chain ID: 97) - | Contract | Address | - | --------------------------- | -------------------------------------------- | - | CEA (logic) | `0xdC3A3a18a17EB4FDa9cF34a8CEee8540e6F2b5Fd` | - | CEAProxy (clone template) | `0xBDF06996BA23AE797a4aA9C8C5994D313D763a7c` | - | CEAFactory (implementation) | `0xC0D35725Dd054B09931740DC231cDea89B0FEd3b` | - | ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | - | CEAFactory (proxy) | `0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719` | - | CEA_V2 (logic) | `0x102B1652ABEDC1c1761355F1Fc71c8487c3a9168` | - | CEAMigration | `0x2a06BF2A9C19dacbb38852f846B42e278e82e855` | +| Contract | Address | +| --------------------------- | -------------------------------------------- | +| CEA (logic) | `0xdC3A3a18a17EB4FDa9cF34a8CEee8540e6F2b5Fd` | +| CEAProxy (clone template) | `0xBDF06996BA23AE797a4aA9C8C5994D313D763a7c` | +| CEAFactory (implementation) | `0xC0D35725Dd054B09931740DC231cDea89B0FEd3b` | +| ProxyAdmin | `0xf33CBb6a1c1D511dF40764063a11978D640C41A7` | +| CEAFactory (proxy) | `0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719` | +| CEA_V2 (logic) | `0x102B1652ABEDC1c1761355F1Fc71c8487c3a9168` | +| CEAMigration | `0x2a06BF2A9C19dacbb38852f846B42e278e82e855` | +| CEA (post-audit) | `0x8FAB1Da91Bd45F4DaF3D50C47A38b49bE9afEff7` | diff --git a/docs/addresses/eth_sepolia.md b/docs/addresses/eth_sepolia.md new file mode 100644 index 0000000..39032ed --- /dev/null +++ b/docs/addresses/eth_sepolia.md @@ -0,0 +1,12 @@ +# Ethereum Sepolia (Chain ID: 11155111) + +| Contract | Address | +| --------------------------- | -------------------------------------------- | +| CEA (logic) | `0x1939376ce03998F638b8760c7a13C9A379A053C0` | +| CEAProxy (clone template) | `0x0a4F7be62B56830070266be81114edB93e68DA09` | +| CEAFactory (implementation) | `0xe5B51807f2252A5Ea9B591fE02285954446c8cAD` | +| ProxyAdmin | `0xF920e3D1420885A117Cb59830d0474aD5690dd82` | +| CEAFactory (proxy) | `0x8ED594A83301FEc545fC6c19fc12cF7111777029` | +| CEA_V2 (logic) | `0x2235df0189F720E9dbF1E31685eb7b6221E0fdD7` | +| CEAMigration | `0x97BCEba9c6f13B0E12Fde0E4D2697F74A79899de` | +| CEA (post-audit) | `0x2d10cdB85989a199a0255E5dc55491B1bdd95A45` | diff --git a/docs/addresses/sepolia.md b/docs/addresses/sepolia.md deleted file mode 100644 index e69de29..0000000 From c92cdc396982427c8221116971ec1f6da9641900 Mon Sep 17 00:00:00 2001 From: Zaryab Date: Tue, 12 May 2026 18:46:07 +0530 Subject: [PATCH 55/56] private turned public --- src/testnetV0/UniversalCoreV0.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testnetV0/UniversalCoreV0.sol b/src/testnetV0/UniversalCoreV0.sol index 2e22084..2bf9b1c 100644 --- a/src/testnetV0/UniversalCoreV0.sol +++ b/src/testnetV0/UniversalCoreV0.sol @@ -72,7 +72,7 @@ contract UniversalCore is address public uniswapV3SwapRouter; /// @dev Deprecated. Slot retained for storage layout compatibility with deployed testnet proxy. - address private uniswapV3Quoter; + address public uniswapV3Quoter; /// @notice Address of the wrapped PC to interact with Uniswap V3. address public WPC; From fd6fb82bf270942959c1c22a31a3af530c31fd1a Mon Sep 17 00:00:00 2001 From: Zaryab Date: Thu, 25 Jun 2026 15:35:40 +0400 Subject: [PATCH 56/56] added audit-report --- ...r2026_P-2025-1876_4_20260625 11_10 (1).pdf | Bin 0 -> 3286346 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audits/Hacken_Push Chain_[SCA] Push Chain _ Core Contracts _ Mar2026_P-2025-1876_4_20260625 11_10 (1).pdf diff --git a/audits/Hacken_Push Chain_[SCA] Push Chain _ Core Contracts _ Mar2026_P-2025-1876_4_20260625 11_10 (1).pdf b/audits/Hacken_Push Chain_[SCA] Push Chain _ Core Contracts _ Mar2026_P-2025-1876_4_20260625 11_10 (1).pdf new file mode 100644 index 0000000000000000000000000000000000000000..9c37b947b539433555cb9e63956229c9ba8dc606 GIT binary patch literal 3286346 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