From 49f7c131bc915fbb0cd62d62eb10132c4f1c8d3e Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 6 Feb 2026 13:39:27 +0800 Subject: [PATCH 01/10] Add multiple contract calls --- contracts/src/AgentExecutor.sol | 12 +++ contracts/src/Gateway.sol | 2 + contracts/src/v2/Handlers.sol | 7 ++ contracts/src/v2/Types.sol | 2 + contracts/test/GatewayV2.t.sol | 170 ++++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 9197b20c0..a7e1fa040 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.33; import {IERC20} from "./interfaces/IERC20.sol"; import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol"; import {Call} from "./utils/Call.sol"; +import {CallContractParams} from "./v2/Types.sol"; /// @title Code which will run within an `Agent` using `delegatecall`. /// @dev This is a singleton contract, meaning that all agents will execute the same code. @@ -29,4 +30,15 @@ contract AgentExecutor { revert(); } } + + // Call multiple contracts with Ether values; reverts on the first failure + function callContracts(CallContractParams[] calldata params) external { + uint256 len = params.length; + for (uint256 i; i < len; ++i) { + bool success = Call.safeCall(params[i].target, params[i].data, params[i].value); + if (!success) { + revert(); + } + } + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index d3c51f3e0..ec8a76a9a 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -523,6 +523,8 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra HandlersV2.mintForeignToken(command.payload); } else if (command.kind == CommandKind.CallContract) { HandlersV2.callContract(origin, AGENT_EXECUTOR, command.payload); + } else if (command.kind == CommandKind.CallContracts) { + HandlersV2.callContracts(origin, AGENT_EXECUTOR, command.payload); } else { revert IGatewayV2.InvalidCommand(); } diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index c8ce0669a..932192815 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -71,4 +71,11 @@ library HandlersV2 { abi.encodeCall(AgentExecutor.callContract, (params.target, params.data, params.value)); Functions.invokeOnAgent(agent, executor, call); } + + function callContracts(bytes32 origin, address executor, bytes calldata data) external { + CallContractParams[] memory params = abi.decode(data, (CallContractParams[])); + address agent = Functions.ensureAgent(origin); + bytes memory call = abi.encodeCall(AgentExecutor.callContracts, (params)); + Functions.invokeOnAgent(agent, executor, call); + } } diff --git a/contracts/src/v2/Types.sol b/contracts/src/v2/Types.sol index 95fb43d8e..6d7052a89 100644 --- a/contracts/src/v2/Types.sol +++ b/contracts/src/v2/Types.sol @@ -36,6 +36,8 @@ library CommandKind { uint8 constant MintForeignToken = 4; // Call an arbitrary solidity contract uint8 constant CallContract = 5; + // Call multiple arbitrary solidity contracts + uint8 constant CallContracts = 6; } // Payload for outbound messages destined for Polkadot diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index 63f3a73b0..d2eca1694 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -319,6 +319,29 @@ contract GatewayV2Test is Test { return commands; } + function makeCallContractsCommand(CallContractParams[] memory params) + public + pure + returns (CommandV2[] memory) + { + bytes memory payload = abi.encode(params); + CommandV2[] memory commands = new CommandV2[](1); + commands[0] = + CommandV2({kind: CommandKind.CallContracts, gas: 500_000, payload: payload}); + return commands; + } + + function makeCallContractsCommandWithInsufficientGas(CallContractParams[] memory params) + public + pure + returns (CommandV2[] memory) + { + bytes memory payload = abi.encode(params); + CommandV2[] memory commands = new CommandV2[](1); + commands[0] = CommandV2({kind: CommandKind.CallContracts, gas: 1, payload: payload}); + return commands; + } + /** * Message Verification */ @@ -617,6 +640,140 @@ contract GatewayV2Test is Test { ); } + function testAgentCallContractsSuccess() public { + bytes32 topic = keccak256("topic"); + + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "First"), + value: 0.05 ether + }); + params[1] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "Second"), + value: 0.05 ether + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.InboundMessageDispatched(1, topic, true, relayerRewardAddress); + + vm.deal(assetHubAgent, 1 ether); + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommand(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + } + + function testAgentCallContractsRevertedOnFirstFailure() public { + bytes32 topic = keccak256("topic"); + + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("revertUnauthorized()"), + value: 0 + }); + params[1] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "Second"), + value: 0 + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.CommandFailed(1, 0); + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + + vm.deal(assetHubAgent, 1 ether); + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommand(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + } + + function testAgentCallContractsRevertedOnSecondFailure() public { + bytes32 topic = keccak256("topic"); + + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "First"), + value: 0 + }); + params[1] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("revertUnauthorized()"), + value: 0 + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.CommandFailed(1, 0); + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + + vm.deal(assetHubAgent, 1 ether); + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommand(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + } + + function testAgentCallContractsRevertedForInsufficientGas() public { + bytes32 topic = keccak256("topic"); + + CallContractParams[] memory params = new CallContractParams[](1); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "World"), + value: 0.1 ether + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.CommandFailed(1, 0); + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + + vm.deal(assetHubAgent, 1 ether); + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommandWithInsufficientGas(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + } + function testCreateAgent() public { bytes32 origin = bytes32(uint256(1)); vm.expectEmit(true, false, false, false); @@ -934,6 +1091,19 @@ contract GatewayV2Test is Test { assertTrue(!ok, "callContract with missing agent should return false"); } + function testCallContractsAgentDoesNotExistReturnsFalse() public { + CallContractParams[] memory params = new CallContractParams[](1); + params[0] = + CallContractParams({target: address(0xdead), data: "", value: uint256(0)}); + bytes memory payload = abi.encode(params); + + CommandV2 memory cmd = + CommandV2({kind: CommandKind.CallContracts, gas: uint64(200_000), payload: payload}); + + bool ok = gatewayLogic.callDispatch(cmd, bytes32(uint256(0x9999))); + assertTrue(!ok, "callContracts with missing agent should return false"); + } + function testInsufficientGasReverts() public { bytes memory payload = ""; // Use an extremely large gas value to trigger InsufficientGasLimit revert in _dispatchCommand From b3d621df28af788364ec910abd1743eeef63cd08 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 9 Feb 2026 18:18:29 +0800 Subject: [PATCH 02/10] Sweep to recipient --- contracts/src/AgentExecutor.sol | 17 +++++++++++++++++ contracts/src/Gateway.sol | 9 ++++++++- contracts/src/v2/Handlers.sol | 17 ++++++++++++++--- contracts/src/v2/Types.sol | 10 ++++++++++ contracts/test/GatewayV2.t.sol | 15 +++++++++++++-- 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index a7e1fa040..e06e561d6 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -41,4 +41,21 @@ contract AgentExecutor { } } } + + function sweep(address recipient, address[] calldata tokens) external { + for (uint256 i; i < tokens.length; ++i) { + address token = tokens[i]; + if (token == address(0)) { + uint256 balance = address(this).balance; + if (balance > 0) { + payable(recipient).safeNativeTransfer(balance); + } + } else { + uint256 balance = IERC20(token).balanceOf(address(this)); + if (balance > 0) { + IERC20(token).safeTransfer(recipient, balance); + } + } + } + } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index ec8a76a9a..1ec467f01 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -25,7 +25,7 @@ import { IGatewayV1, IGatewayV2 } from "./Types.sol"; -import {Network} from "./v2/Types.sol"; +import {Network, CallContractsParams} from "./v2/Types.sol"; import {Upgrade} from "./Upgrade.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; import {IUpgradable} from "./interfaces/IUpgradable.sol"; @@ -500,6 +500,13 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra catch { success = false; emit IGatewayV2.CommandFailed(nonce, i); + if (command.kind == CommandKind.CallContracts) { + CallContractsParams memory params = + abi.decode(command.payload, (CallContractsParams)); + if (params.sweepRecipient != address(0) && params.tokensToSweep.length > 0) { + HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, command.payload); + } + } } } return (false, success); diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index 932192815..3005bfd61 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -19,7 +19,8 @@ import { UnlockNativeTokenParams, RegisterForeignTokenParams, MintForeignTokenParams, - CallContractParams + CallContractParams, + CallContractsParams } from "./Types.sol"; library HandlersV2 { @@ -73,9 +74,19 @@ library HandlersV2 { } function callContracts(bytes32 origin, address executor, bytes calldata data) external { - CallContractParams[] memory params = abi.decode(data, (CallContractParams[])); + CallContractsParams memory params = abi.decode(data, (CallContractsParams)); address agent = Functions.ensureAgent(origin); - bytes memory call = abi.encodeCall(AgentExecutor.callContracts, (params)); + bytes memory call = abi.encodeCall(AgentExecutor.callContracts, params.calls); + Functions.invokeOnAgent(agent, executor, call); + } + + function sweepAfterCallContracts(bytes32 origin, address executor, bytes calldata data) + external + { + CallContractsParams memory params = abi.decode(data, (CallContractsParams)); + address agent = Functions.ensureAgent(origin); + bytes memory call = + abi.encodeCall(AgentExecutor.sweep, (params.sweepRecipient, params.tokensToSweep)); Functions.invokeOnAgent(agent, executor, call); } } diff --git a/contracts/src/v2/Types.sol b/contracts/src/v2/Types.sol index 6d7052a89..987f99ac2 100644 --- a/contracts/src/v2/Types.sol +++ b/contracts/src/v2/Types.sol @@ -187,6 +187,16 @@ struct CallContractParams { uint256 value; } +// Payload for CallContracts command. Best-effort execution + optional sweep. +struct CallContractsParams { + // Sub-calls to execute (best-effort: continues on failure) + CallContractParams[] calls; + // Recipient for optional sweep after all calls succeed; address(0) = no sweep + address sweepRecipient; + // Tokens to sweep full balance to sweepRecipient; address(0) = sweep ETH + address[] tokensToSweep; +} + enum Network { Polkadot } diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index d2eca1694..46357fb4b 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -39,6 +39,7 @@ import { RegisterForeignTokenParams, MintForeignTokenParams, CallContractParams, + CallContractsParams, Payload, Asset, makeNativeAsset, @@ -324,7 +325,12 @@ contract GatewayV2Test is Test { pure returns (CommandV2[] memory) { - bytes memory payload = abi.encode(params); + CallContractsParams memory p = CallContractsParams({ + calls: params, + sweepRecipient: address(0), + tokensToSweep: new address[](0) + }); + bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); commands[0] = CommandV2({kind: CommandKind.CallContracts, gas: 500_000, payload: payload}); @@ -336,7 +342,12 @@ contract GatewayV2Test is Test { pure returns (CommandV2[] memory) { - bytes memory payload = abi.encode(params); + CallContractsParams memory p = CallContractsParams({ + calls: params, + sweepRecipient: address(0), + tokensToSweep: new address[](0) + }); + bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); commands[0] = CommandV2({kind: CommandKind.CallContracts, gas: 1, payload: payload}); return commands; From 4017a4582370a4fee9f3b0b175ec2e984d5a8974 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 9 Feb 2026 18:25:47 +0800 Subject: [PATCH 03/10] Fix error catch --- contracts/src/AgentExecutor.sol | 1 + contracts/src/Gateway.sol | 20 ++++++++--- contracts/src/v2/Types.sol | 6 ++-- contracts/test/GatewayV2.t.sol | 60 +++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index e06e561d6..1a6ebf2de 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -42,6 +42,7 @@ contract AgentExecutor { } } + // Sweep remaining assets when specified function sweep(address recipient, address[] calldata tokens) external { for (uint256 i; i < tokens.length; ++i) { address token = tokens[i]; diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 1ec467f01..5c3dafbbb 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -501,11 +501,8 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra success = false; emit IGatewayV2.CommandFailed(nonce, i); if (command.kind == CommandKind.CallContracts) { - CallContractsParams memory params = - abi.decode(command.payload, (CallContractsParams)); - if (params.sweepRecipient != address(0) && params.tokensToSweep.length > 0) { - HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, command.payload); - } + try this.sweepAfterCallContractsIfConfigured(origin, command.payload) {} + catch {} } } } @@ -537,6 +534,19 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra } } + /// @dev Decode CallContractsParams and run sweep when configured; used in catch block. + /// Wrapped in try-catch so malformed payloads or sweep failures don't revert dispatch. + function sweepAfterCallContractsIfConfigured(bytes32 origin, bytes calldata payload) + external + onlySelf + { + CallContractsParams memory params = abi.decode(payload, (CallContractsParams)); + if (params.sweepRecipient != address(0) && params.tokensToSweep.length > 0) { + try HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, payload) {} + catch {} + } + } + /** * Upgrades */ diff --git a/contracts/src/v2/Types.sol b/contracts/src/v2/Types.sol index 987f99ac2..63b04236f 100644 --- a/contracts/src/v2/Types.sol +++ b/contracts/src/v2/Types.sol @@ -187,11 +187,11 @@ struct CallContractParams { uint256 value; } -// Payload for CallContracts command. Best-effort execution + optional sweep. +// Payload for CallContracts command. Reverts on first call failure; optional sweep when calls fail. struct CallContractsParams { - // Sub-calls to execute (best-effort: continues on failure) + // Sub-calls to execute (reverts on first failure) CallContractParams[] calls; - // Recipient for optional sweep after all calls succeed; address(0) = no sweep + // Recipient for sweep when calls fail; address(0) = no sweep address sweepRecipient; // Tokens to sweep full balance to sweepRecipient; address(0) = sweep ETH address[] tokensToSweep; diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index 46357fb4b..8d9fa8cb7 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -353,6 +353,23 @@ contract GatewayV2Test is Test { return commands; } + function makeCallContractsCommandWithSweep( + CallContractParams[] memory params, + address sweepRecipient, + address[] memory tokensToSweep + ) public pure returns (CommandV2[] memory) { + CallContractsParams memory p = CallContractsParams({ + calls: params, + sweepRecipient: sweepRecipient, + tokensToSweep: tokensToSweep + }); + bytes memory payload = abi.encode(p); + CommandV2[] memory commands = new CommandV2[](1); + commands[0] = + CommandV2({kind: CommandKind.CallContracts, gas: 500_000, payload: payload}); + return commands; + } + /** * Message Verification */ @@ -785,6 +802,49 @@ contract GatewayV2Test is Test { ); } + function testAgentCallContractsSweepRunsOnFailure() public { + bytes32 topic = keccak256("topic"); + + // First call reverts; sweep is configured to recover agent's ETH + CallContractParams[] memory params = new CallContractParams[](1); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("revertUnauthorized()"), + value: 0 + }); + + address sweepRecipient = address(new PayableRecipient()); + address[] memory tokensToSweep = new address[](1); + tokensToSweep[0] = address(0); // sweep ETH + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.CommandFailed(1, 0); + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + + vm.deal(assetHubAgent, 0.5 ether); + uint256 recipientBalanceBefore = sweepRecipient.balance; + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommandWithSweep( + params, + sweepRecipient, + tokensToSweep + ) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + + assertEq(sweepRecipient.balance, recipientBalanceBefore + 0.5 ether, "sweep should have transferred ETH"); + } + function testCreateAgent() public { bytes32 origin = bytes32(uint256(1)); vm.expectEmit(true, false, false, false); From 6beeda5597c646d9f36ed2e60208f6cc0f2de57c Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 9 Feb 2026 18:51:59 +0800 Subject: [PATCH 04/10] Add event for SweepAfterCallContractsFailed --- contracts/src/Gateway.sol | 15 ++++++++------- contracts/src/v2/IGateway.sol | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 5c3dafbbb..6196d8a3d 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -501,8 +501,10 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra success = false; emit IGatewayV2.CommandFailed(nonce, i); if (command.kind == CommandKind.CallContracts) { - try this.sweepAfterCallContractsIfConfigured(origin, command.payload) {} - catch {} + try this.trySweepOnFailure(origin, command.payload) {} + catch { + emit IGatewayV2.SweepAfterCallContractsFailed(nonce, i); + } } } } @@ -534,16 +536,15 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra } } - /// @dev Decode CallContractsParams and run sweep when configured; used in catch block. - /// Wrapped in try-catch so malformed payloads or sweep failures don't revert dispatch. - function sweepAfterCallContractsIfConfigured(bytes32 origin, bytes calldata payload) + /// @dev Decode CallContractsParams and run sweep when configured; used in CallContracts catch block. + /// Caller wraps in try-catch so malformed payloads or sweep failures don't revert dispatch. + function trySweepOnFailure(bytes32 origin, bytes calldata payload) external onlySelf { CallContractsParams memory params = abi.decode(payload, (CallContractsParams)); if (params.sweepRecipient != address(0) && params.tokensToSweep.length > 0) { - try HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, payload) {} - catch {} + HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, payload); } } diff --git a/contracts/src/v2/IGateway.sol b/contracts/src/v2/IGateway.sol index 829ed3f13..761548e91 100644 --- a/contracts/src/v2/IGateway.sol +++ b/contracts/src/v2/IGateway.sol @@ -38,6 +38,9 @@ interface IGatewayV2 { /// Emitted when a command at `index` within an inbound message identified by `nonce` fails to execute event CommandFailed(uint64 indexed nonce, uint256 index); + /// Emitted when sweep-after-CallContracts fails (e.g. malformed payload or sweep revert) + event SweepAfterCallContractsFailed(uint64 indexed nonce, uint256 index); + /// Emitted when an outbound message has been accepted for delivery to a Polkadot parachain event OutboundMessageAccepted(uint64 nonce, Payload payload); From c1aa3d1806707c1e18b327c9a39732020acd9627 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 15 Jun 2026 23:52:09 +0800 Subject: [PATCH 05/10] refactor: remove optional sweep-on-failure from multiple contract calls execution Simplifies the CallContracts command by removing the optional sweep-on-failure feature. Multiple contract calls will now execute atomically, reverting on the first failure and marking the command as failed, without attempt to recover and sweep remaining funds within the gateway loop. This eliminates significant complexity across the gateway, handlers, executor, types, events, and tests. --- contracts/src/AgentExecutor.sol | 18 --------- contracts/src/Gateway.sol | 20 +--------- contracts/src/v2/Handlers.sol | 10 ----- contracts/src/v2/IGateway.sol | 3 -- contracts/src/v2/Types.sol | 6 +-- contracts/test/GatewayV2.t.sol | 66 +-------------------------------- 6 files changed, 4 insertions(+), 119 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 5222e4883..0e33a543a 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -41,22 +41,4 @@ contract AgentExecutor { } } } - - // Sweep remaining assets when specified - function sweep(address recipient, address[] calldata tokens) external { - for (uint256 i; i < tokens.length; ++i) { - address token = tokens[i]; - if (token == address(0)) { - uint256 balance = address(this).balance; - if (balance > 0) { - payable(recipient).safeNativeTransfer(balance); - } - } else { - uint256 balance = IERC20(token).balanceOf(address(this)); - if (balance > 0) { - IERC20(token).safeTransfer(recipient, balance); - } - } - } - } } diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 0349c9084..938959243 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -24,7 +24,7 @@ import { IGatewayV1, IGatewayV2 } from "./Types.sol"; -import {Network, CallContractsParams} from "./v2/Types.sol"; +import {Network} from "./v2/Types.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; import {IUpgradable} from "./interfaces/IUpgradable.sol"; import {ERC1967} from "./utils/ERC1967.sol"; @@ -491,12 +491,6 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra catch { success = false; emit IGatewayV2.CommandFailed(nonce, i); - if (command.kind == CommandKind.CallContracts) { - try this.trySweepOnFailure(origin, command.payload) {} - catch { - emit IGatewayV2.SweepAfterCallContractsFailed(nonce, i); - } - } } } return (false, success); @@ -527,18 +521,6 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra } } - /// @dev Decode CallContractsParams and run sweep when configured; used in CallContracts catch block. - /// Caller wraps in try-catch so malformed payloads or sweep failures don't revert dispatch. - function trySweepOnFailure(bytes32 origin, bytes calldata payload) - external - onlySelf - { - CallContractsParams memory params = abi.decode(payload, (CallContractsParams)); - if (params.sweepRecipient != address(0) && params.tokensToSweep.length > 0) { - HandlersV2.sweepAfterCallContracts(origin, AGENT_EXECUTOR, payload); - } - } - /** * Upgrades */ diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index 187a34689..68d70c82c 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -78,14 +78,4 @@ library HandlersV2 { bytes memory call = abi.encodeCall(AgentExecutor.callContracts, params.calls); Functions.invokeOnAgent(agent, executor, call); } - - function sweepAfterCallContracts(bytes32 origin, address executor, bytes calldata data) - external - { - CallContractsParams memory params = abi.decode(data, (CallContractsParams)); - address agent = Functions.ensureAgent(origin); - bytes memory call = - abi.encodeCall(AgentExecutor.sweep, (params.sweepRecipient, params.tokensToSweep)); - Functions.invokeOnAgent(agent, executor, call); - } } diff --git a/contracts/src/v2/IGateway.sol b/contracts/src/v2/IGateway.sol index 9c575f587..15b6208f5 100644 --- a/contracts/src/v2/IGateway.sol +++ b/contracts/src/v2/IGateway.sol @@ -38,9 +38,6 @@ interface IGatewayV2 { /// Emitted when a command at `index` within an inbound message identified by `nonce` fails to execute event CommandFailed(uint64 indexed nonce, uint256 index); - /// Emitted when sweep-after-CallContracts fails (e.g. malformed payload or sweep revert) - event SweepAfterCallContractsFailed(uint64 indexed nonce, uint256 index); - /// Emitted when an outbound message has been accepted for delivery to a Polkadot parachain event OutboundMessageAccepted(uint64 nonce, Payload payload); diff --git a/contracts/src/v2/Types.sol b/contracts/src/v2/Types.sol index 591fdacba..49b11386f 100644 --- a/contracts/src/v2/Types.sol +++ b/contracts/src/v2/Types.sol @@ -187,14 +187,10 @@ struct CallContractParams { uint256 value; } -// Payload for CallContracts command. Reverts on first call failure; optional sweep when calls fail. +// Payload for CallContracts command. Reverts on first call failure. struct CallContractsParams { // Sub-calls to execute (reverts on first failure) CallContractParams[] calls; - // Recipient for sweep when calls fail; address(0) = no sweep - address sweepRecipient; - // Tokens to sweep full balance to sweepRecipient; address(0) = sweep ETH - address[] tokensToSweep; } enum Network { diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index b43e483af..7270769e1 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -304,9 +304,7 @@ contract GatewayV2Test is Test { returns (CommandV2[] memory) { CallContractsParams memory p = CallContractsParams({ - calls: params, - sweepRecipient: address(0), - tokensToSweep: new address[](0) + calls: params }); bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); @@ -321,9 +319,7 @@ contract GatewayV2Test is Test { returns (CommandV2[] memory) { CallContractsParams memory p = CallContractsParams({ - calls: params, - sweepRecipient: address(0), - tokensToSweep: new address[](0) + calls: params }); bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); @@ -331,23 +327,6 @@ contract GatewayV2Test is Test { return commands; } - function makeCallContractsCommandWithSweep( - CallContractParams[] memory params, - address sweepRecipient, - address[] memory tokensToSweep - ) public pure returns (CommandV2[] memory) { - CallContractsParams memory p = CallContractsParams({ - calls: params, - sweepRecipient: sweepRecipient, - tokensToSweep: tokensToSweep - }); - bytes memory payload = abi.encode(p); - CommandV2[] memory commands = new CommandV2[](1); - commands[0] = - CommandV2({kind: CommandKind.CallContracts, gas: 500_000, payload: payload}); - return commands; - } - /** * Message Verification */ @@ -780,48 +759,7 @@ contract GatewayV2Test is Test { ); } - function testAgentCallContractsSweepRunsOnFailure() public { - bytes32 topic = keccak256("topic"); - - // First call reverts; sweep is configured to recover agent's ETH - CallContractParams[] memory params = new CallContractParams[](1); - params[0] = CallContractParams({ - target: address(helloWorld), - data: abi.encodeWithSignature("revertUnauthorized()"), - value: 0 - }); - - address sweepRecipient = address(new PayableRecipient()); - address[] memory tokensToSweep = new address[](1); - tokensToSweep[0] = address(0); // sweep ETH - vm.expectEmit(true, false, false, true); - emit IGatewayV2.CommandFailed(1, 0); - emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); - - vm.deal(assetHubAgent, 0.5 ether); - uint256 recipientBalanceBefore = sweepRecipient.balance; - - hoax(relayer, 1 ether); - IGatewayV2(address(gateway)) - .v2_submit( - InboundMessageV2({ - origin: Constants.ASSET_HUB_AGENT_ID, - nonce: 1, - topic: topic, - commands: makeCallContractsCommandWithSweep( - params, - sweepRecipient, - tokensToSweep - ) - }), - proof, - makeMockProof(), - relayerRewardAddress - ); - - assertEq(sweepRecipient.balance, recipientBalanceBefore + 0.5 ether, "sweep should have transferred ETH"); - } function testCreateAgent() public { bytes32 origin = bytes32(uint256(1)); From e5aadd152d831e0ac5c2fbbe2acbdf38d7f30879 Mon Sep 17 00:00:00 2001 From: ron Date: Mon, 15 Jun 2026 23:59:34 +0800 Subject: [PATCH 06/10] test: fix CallContractsParams encoding in GatewayV2 tests Wrap the CallContractParams array in the CallContractsParams struct before encoding to avoid decoding errors inside the gateway dispatcher during execution. --- contracts/test/GatewayV2.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index 7270769e1..e753e38ac 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -1082,7 +1082,8 @@ contract GatewayV2Test is Test { CallContractParams[] memory params = new CallContractParams[](1); params[0] = CallContractParams({target: address(0xdead), data: "", value: uint256(0)}); - bytes memory payload = abi.encode(params); + CallContractsParams memory p = CallContractsParams({calls: params}); + bytes memory payload = abi.encode(p); CommandV2 memory cmd = CommandV2({kind: CommandKind.CallContracts, gas: uint64(200_000), payload: payload}); From f905d402f87f49da394847a17982a651a9d56513 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 16 Jun 2026 13:46:35 +0800 Subject: [PATCH 07/10] refactor: bubble up revert reason on agent executor contract calls --- contracts/src/AgentExecutor.sol | 10 ++++++++-- contracts/test/GatewayV2.t.sol | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 0e33a543a..ee7901821 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -27,7 +27,10 @@ contract AgentExecutor { function callContract(address target, bytes memory data, uint256 value) external { bool success = Call.safeCall(target, data, value); if (!success) { - revert(); + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } } } @@ -37,7 +40,10 @@ contract AgentExecutor { for (uint256 i; i < len; ++i) { bool success = Call.safeCall(params[i].target, params[i].data, params[i].value); if (!success) { - revert(); + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } } } } diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index e753e38ac..ef734787d 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -1352,4 +1352,28 @@ contract GatewayV2Test is Test { // message should be recorded as dispatched assertTrue(gw.v2_isDispatched(msgv.nonce)); } + + function testAgentExecutorCallContractBubblesRevertReason() public { + bytes memory data = abi.encodeWithSignature("revertUnauthorized()"); + + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + executor.callContract(address(helloWorld), data, 0); + } + + function testAgentExecutorCallContractsBubblesRevertReason() public { + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("sayHello(string)", "Success"), + value: 0 + }); + params[1] = CallContractParams({ + target: address(helloWorld), + data: abi.encodeWithSignature("revertUnauthorized()"), + value: 0 + }); + + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + executor.callContracts(params); + } } From 3e7238def9cefb20315144c7d815a6baee3e5fc6 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 16 Jun 2026 13:50:31 +0800 Subject: [PATCH 08/10] test: add approve-transfer-send balance update tests for CallContracts --- contracts/test/GatewayV2.t.sol | 117 +++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index ef734787d..bd37af863 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -1376,4 +1376,121 @@ contract GatewayV2Test is Test { vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); executor.callContracts(params); } + + function testAgentCallContractsTransferApproveSendSuccess() public { + bytes32 topic = keccak256("topic"); + address recipient = makeAddr("recipient"); + + // Set up adapter + MockAdapter adapter = new MockAdapter(address(weth), recipient); + + // Fund agent with 1 WETH + vm.deal(assetHubAgent, 1 ether); + hoax(assetHubAgent); + weth.deposit{value: 1 ether}(); + + // Check initial balances + assertEq(weth.balanceOf(assetHubAgent), 1 ether); + assertEq(weth.balanceOf(recipient), 0); + + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(weth), + data: abi.encodeWithSignature("approve(address,uint256)", address(adapter), 1 ether), + value: 0 + }); + params[1] = CallContractParams({ + target: address(adapter), + data: abi.encodeWithSignature("swapAndSend(uint256)", 1 ether), + value: 0 + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.InboundMessageDispatched(1, topic, true, relayerRewardAddress); + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommand(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + + // Check final balances + assertEq(weth.balanceOf(assetHubAgent), 0); + assertEq(weth.balanceOf(recipient), 1 ether); + } + + function testAgentCallContractsTransferApproveSendFailureRevertsAll() public { + bytes32 topic = keccak256("topic"); + address recipient = makeAddr("recipient"); + + // Set up adapter + MockAdapter adapter = new MockAdapter(address(weth), recipient); + + // Fund agent with 1 WETH + vm.deal(assetHubAgent, 1 ether); + hoax(assetHubAgent); + weth.deposit{value: 1 ether}(); + + // Check initial balances + assertEq(weth.balanceOf(assetHubAgent), 1 ether); + assertEq(weth.balanceOf(recipient), 0); + + CallContractParams[] memory params = new CallContractParams[](2); + params[0] = CallContractParams({ + target: address(weth), + data: abi.encodeWithSignature("approve(address,uint256)", address(adapter), 1 ether), + value: 0 + }); + params[1] = CallContractParams({ + target: address(adapter), + data: abi.encodeWithSignature("swapAndSend(uint256)", 2 ether), // fails because agent only has 1 ether + value: 0 + }); + + vm.expectEmit(true, false, false, true); + emit IGatewayV2.CommandFailed(1, 0); // Gateway reports command failed + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractsCommand(params) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + + // Check final balances are unchanged + assertEq(weth.balanceOf(assetHubAgent), 1 ether); + assertEq(weth.balanceOf(recipient), 0); + // Approval should be reverted back to 0 + assertEq(weth.allowance(assetHubAgent, address(adapter)), 0); + } +} + +contract MockAdapter { + address public immutable token; + address public immutable recipient; + + constructor(address _token, address _recipient) { + token = _token; + recipient = _recipient; + } + + function swapAndSend(uint256 amount) external { + require(IERC20(token).transferFrom(msg.sender, recipient, amount), "transfer failed"); + } } From 031ded976c5d857afeedae75b75c05ddd2d173d5 Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 16 Jun 2026 14:31:02 +0800 Subject: [PATCH 09/10] refactor: extract bubbleRevert helper to dedupe revert bubbling Move the duplicated returndata-bubbling assembly out of AgentExecutor.callContract/callContracts into a shared Call.bubbleRevert() helper, annotated memory-safe. No behavioral change. Also tidy whitespace in GatewayV2 tests. --- contracts/src/AgentExecutor.sol | 10 ++-------- contracts/src/utils/Call.sol | 12 ++++++++++++ contracts/test/GatewayV2.t.sol | 4 +--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index ee7901821..55282d7f5 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -27,10 +27,7 @@ contract AgentExecutor { function callContract(address target, bytes memory data, uint256 value) external { bool success = Call.safeCall(target, data, value); if (!success) { - assembly { - returndatacopy(0, 0, returndatasize()) - revert(0, returndatasize()) - } + Call.bubbleRevert(); } } @@ -40,10 +37,7 @@ contract AgentExecutor { for (uint256 i; i < len; ++i) { bool success = Call.safeCall(params[i].target, params[i].data, params[i].value); if (!success) { - assembly { - returndatacopy(0, 0, returndatasize()) - revert(0, returndatasize()) - } + Call.bubbleRevert(); } } } diff --git a/contracts/src/utils/Call.sol b/contracts/src/utils/Call.sol index f007fa8ec..d85821019 100644 --- a/contracts/src/utils/Call.sol +++ b/contracts/src/utils/Call.sol @@ -33,6 +33,18 @@ library Call { * @param target Address to call * @param data Calldata to pass to the call */ + /** + * @notice Bubble up the revert reason from the most recent external call + * @dev Must be called immediately after the failed call so that returndata is still available + */ + function bubbleRevert() internal pure { + /// @solidity memory-safe-assembly + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + function safeCall(address target, bytes memory data, uint256 value) internal returns (bool) { bool success; assembly { diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index bd37af863..b65126fbf 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -759,8 +759,6 @@ contract GatewayV2Test is Test { ); } - - function testCreateAgent() public { bytes32 origin = bytes32(uint256(1)); vm.expectEmit(true, false, false, false); @@ -1355,7 +1353,7 @@ contract GatewayV2Test is Test { function testAgentExecutorCallContractBubblesRevertReason() public { bytes memory data = abi.encodeWithSignature("revertUnauthorized()"); - + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); executor.callContract(address(helloWorld), data, 0); } From 089468b0af2cb11ed0c26e5464d4f351cb72bd83 Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 19 Jun 2026 11:44:24 +0800 Subject: [PATCH 10/10] refactor: rename CallContracts command to MultiCall --- contracts/src/AgentExecutor.sol | 2 +- contracts/src/Gateway.sol | 4 +-- contracts/src/v2/Handlers.sol | 8 +++--- contracts/src/v2/Types.sol | 6 ++-- contracts/test/GatewayV2.t.sol | 50 ++++++++++++++++----------------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 55282d7f5..ec6ef620d 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -32,7 +32,7 @@ contract AgentExecutor { } // Call multiple contracts with Ether values; reverts on the first failure - function callContracts(CallContractParams[] calldata params) external { + function multiCall(CallContractParams[] calldata params) external { uint256 len = params.length; for (uint256 i; i < len; ++i) { bool success = Call.safeCall(params[i].target, params[i].data, params[i].value); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 938959243..b93c3c658 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -514,8 +514,8 @@ contract Gateway is IGatewayBase, IGatewayV1, IGatewayV2, IInitializable, IUpgra HandlersV2.mintForeignToken(command.payload); } else if (command.kind == CommandKind.CallContract) { HandlersV2.callContract(origin, AGENT_EXECUTOR, command.payload); - } else if (command.kind == CommandKind.CallContracts) { - HandlersV2.callContracts(origin, AGENT_EXECUTOR, command.payload); + } else if (command.kind == CommandKind.MultiCall) { + HandlersV2.multiCall(origin, AGENT_EXECUTOR, command.payload); } else { revert IGatewayV2.InvalidCommand(); } diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index 68d70c82c..5d16aa4ac 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -19,7 +19,7 @@ import { RegisterForeignTokenParams, MintForeignTokenParams, CallContractParams, - CallContractsParams + MultiCallParams } from "./Types.sol"; library HandlersV2 { @@ -72,10 +72,10 @@ library HandlersV2 { Functions.invokeOnAgent(agent, executor, call); } - function callContracts(bytes32 origin, address executor, bytes calldata data) external { - CallContractsParams memory params = abi.decode(data, (CallContractsParams)); + function multiCall(bytes32 origin, address executor, bytes calldata data) external { + MultiCallParams memory params = abi.decode(data, (MultiCallParams)); address agent = Functions.ensureAgent(origin); - bytes memory call = abi.encodeCall(AgentExecutor.callContracts, params.calls); + bytes memory call = abi.encodeCall(AgentExecutor.multiCall, params.calls); Functions.invokeOnAgent(agent, executor, call); } } diff --git a/contracts/src/v2/Types.sol b/contracts/src/v2/Types.sol index 49b11386f..1f9646553 100644 --- a/contracts/src/v2/Types.sol +++ b/contracts/src/v2/Types.sol @@ -37,7 +37,7 @@ library CommandKind { // Call an arbitrary solidity contract uint8 constant CallContract = 5; // Call multiple arbitrary solidity contracts - uint8 constant CallContracts = 6; + uint8 constant MultiCall = 6; } // Payload for outbound messages destined for Polkadot @@ -187,8 +187,8 @@ struct CallContractParams { uint256 value; } -// Payload for CallContracts command. Reverts on first call failure. -struct CallContractsParams { +// Payload for MultiCall command. Reverts on first call failure. +struct MultiCallParams { // Sub-calls to execute (reverts on first failure) CallContractParams[] calls; } diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index b65126fbf..4e39869da 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -25,7 +25,7 @@ import { RegisterForeignTokenParams, MintForeignTokenParams, CallContractParams, - CallContractsParams, + MultiCallParams, Payload, Asset, makeNativeAsset, @@ -298,32 +298,32 @@ contract GatewayV2Test is Test { return commands; } - function makeCallContractsCommand(CallContractParams[] memory params) + function makeMultiCallCommand(CallContractParams[] memory params) public pure returns (CommandV2[] memory) { - CallContractsParams memory p = CallContractsParams({ + MultiCallParams memory p = MultiCallParams({ calls: params }); bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); commands[0] = - CommandV2({kind: CommandKind.CallContracts, gas: 500_000, payload: payload}); + CommandV2({kind: CommandKind.MultiCall, gas: 500_000, payload: payload}); return commands; } - function makeCallContractsCommandWithInsufficientGas(CallContractParams[] memory params) + function makeMultiCallCommandWithInsufficientGas(CallContractParams[] memory params) public pure returns (CommandV2[] memory) { - CallContractsParams memory p = CallContractsParams({ + MultiCallParams memory p = MultiCallParams({ calls: params }); bytes memory payload = abi.encode(p); CommandV2[] memory commands = new CommandV2[](1); - commands[0] = CommandV2({kind: CommandKind.CallContracts, gas: 1, payload: payload}); + commands[0] = CommandV2({kind: CommandKind.MultiCall, gas: 1, payload: payload}); return commands; } @@ -625,7 +625,7 @@ contract GatewayV2Test is Test { ); } - function testAgentCallContractsSuccess() public { + function testAgentMultiCallSuccess() public { bytes32 topic = keccak256("topic"); CallContractParams[] memory params = new CallContractParams[](2); @@ -651,7 +651,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommand(params) + commands: makeMultiCallCommand(params) }), proof, makeMockProof(), @@ -659,7 +659,7 @@ contract GatewayV2Test is Test { ); } - function testAgentCallContractsRevertedOnFirstFailure() public { + function testAgentMultiCallRevertedOnFirstFailure() public { bytes32 topic = keccak256("topic"); CallContractParams[] memory params = new CallContractParams[](2); @@ -686,7 +686,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommand(params) + commands: makeMultiCallCommand(params) }), proof, makeMockProof(), @@ -694,7 +694,7 @@ contract GatewayV2Test is Test { ); } - function testAgentCallContractsRevertedOnSecondFailure() public { + function testAgentMultiCallRevertedOnSecondFailure() public { bytes32 topic = keccak256("topic"); CallContractParams[] memory params = new CallContractParams[](2); @@ -721,7 +721,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommand(params) + commands: makeMultiCallCommand(params) }), proof, makeMockProof(), @@ -729,7 +729,7 @@ contract GatewayV2Test is Test { ); } - function testAgentCallContractsRevertedForInsufficientGas() public { + function testAgentMultiCallRevertedForInsufficientGas() public { bytes32 topic = keccak256("topic"); CallContractParams[] memory params = new CallContractParams[](1); @@ -751,7 +751,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommandWithInsufficientGas(params) + commands: makeMultiCallCommandWithInsufficientGas(params) }), proof, makeMockProof(), @@ -1076,18 +1076,18 @@ contract GatewayV2Test is Test { assertTrue(!ok, "callContract with missing agent should return false"); } - function testCallContractsAgentDoesNotExistReturnsFalse() public { + function testMultiCallAgentDoesNotExistReturnsFalse() public { CallContractParams[] memory params = new CallContractParams[](1); params[0] = CallContractParams({target: address(0xdead), data: "", value: uint256(0)}); - CallContractsParams memory p = CallContractsParams({calls: params}); + MultiCallParams memory p = MultiCallParams({calls: params}); bytes memory payload = abi.encode(p); CommandV2 memory cmd = - CommandV2({kind: CommandKind.CallContracts, gas: uint64(200_000), payload: payload}); + CommandV2({kind: CommandKind.MultiCall, gas: uint64(200_000), payload: payload}); bool ok = gatewayLogic.callDispatch(cmd, bytes32(uint256(0x9999))); - assertTrue(!ok, "callContracts with missing agent should return false"); + assertTrue(!ok, "multiCall with missing agent should return false"); } function testInsufficientGasReverts() public { @@ -1358,7 +1358,7 @@ contract GatewayV2Test is Test { executor.callContract(address(helloWorld), data, 0); } - function testAgentExecutorCallContractsBubblesRevertReason() public { + function testAgentExecutorMultiCallBubblesRevertReason() public { CallContractParams[] memory params = new CallContractParams[](2); params[0] = CallContractParams({ target: address(helloWorld), @@ -1372,10 +1372,10 @@ contract GatewayV2Test is Test { }); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - executor.callContracts(params); + executor.multiCall(params); } - function testAgentCallContractsTransferApproveSendSuccess() public { + function testAgentMultiCallTransferApproveSendSuccess() public { bytes32 topic = keccak256("topic"); address recipient = makeAddr("recipient"); @@ -1413,7 +1413,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommand(params) + commands: makeMultiCallCommand(params) }), proof, makeMockProof(), @@ -1425,7 +1425,7 @@ contract GatewayV2Test is Test { assertEq(weth.balanceOf(recipient), 1 ether); } - function testAgentCallContractsTransferApproveSendFailureRevertsAll() public { + function testAgentMultiCallTransferApproveSendFailureRevertsAll() public { bytes32 topic = keccak256("topic"); address recipient = makeAddr("recipient"); @@ -1464,7 +1464,7 @@ contract GatewayV2Test is Test { origin: Constants.ASSET_HUB_AGENT_ID, nonce: 1, topic: topic, - commands: makeCallContractsCommand(params) + commands: makeMultiCallCommand(params) }), proof, makeMockProof(),