From dfd8e0e8a3096c1238fda9b90b3b045d8339e21b Mon Sep 17 00:00:00 2001 From: ron Date: Tue, 12 May 2026 20:02:18 +0800 Subject: [PATCH 1/3] Restrict Asset Hub agent from executing contract calls in V2 --- contracts/src/Functions.sol | 11 ++++++++-- contracts/src/interfaces/IGatewayBase.sol | 1 + contracts/src/v2/Handlers.sol | 2 +- contracts/test/GatewayV2.t.sol | 26 +++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/contracts/src/Functions.sol b/contracts/src/Functions.sol index f372befca..dc178491a 100644 --- a/contracts/src/Functions.sol +++ b/contracts/src/Functions.sol @@ -15,6 +15,7 @@ import {TokenInfo} from "./types/Common.sol"; import {ChannelID, Channel} from "./v1/Types.sol"; import {IGatewayBase} from "./interfaces/IGatewayBase.sol"; import {IGatewayV1} from "./v1/IGateway.sol"; +import {Constants} from "./Constants.sol"; // Common functionality that is shared between V1 and V2 library Functions { @@ -22,10 +23,8 @@ library Functions { using SafeNativeTransfer for address payable; using SafeTokenTransferFrom for IERC20; - error AgentDoesNotExist(); error InvalidToken(); error InvalidAmount(); - error ChannelDoesNotExist(); /// Looks up an agent contract address, failing if no such mapping exists function ensureAgent(bytes32 agentID) internal view returns (address agent) { @@ -35,6 +34,14 @@ library Functions { } } + /// Looks up an agent contract address, failing if no such mapping exists or if the agent is the primary asset hub agent + function ensureAuthorizedAgent(bytes32 agentID) internal view returns (address agent) { + if(agentID == Constants.ASSET_HUB_AGENT_ID) { + revert IGatewayBase.AgentNotAuthorized(); + } + agent = ensureAgent(agentID); + } + /// @dev Ensure that the specified parachain has a channel allocated function ensureChannel(ChannelID channelID) internal view returns (Channel storage ch) { ch = CoreStorage.layout().channels[channelID]; diff --git a/contracts/src/interfaces/IGatewayBase.sol b/contracts/src/interfaces/IGatewayBase.sol index f14cb5b93..3a5178d20 100644 --- a/contracts/src/interfaces/IGatewayBase.sol +++ b/contracts/src/interfaces/IGatewayBase.sol @@ -13,6 +13,7 @@ interface IGatewayBase { error Unsupported(); error InvalidDestinationFee(); error AgentDoesNotExist(); + error AgentNotAuthorized(); error TokenAlreadyRegistered(); error TokenMintFailed(); error TokenTransferFailed(); diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index 74b70a56d..35705b062 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -65,7 +65,7 @@ library HandlersV2 { function callContract(bytes32 origin, address executor, bytes calldata data) external { CallContractParams memory params = abi.decode(data, (CallContractParams)); - address agent = Functions.ensureAgent(origin); + address agent = Functions.ensureAuthorizedAgent(origin); bytes memory call = abi.encodeCall(AgentExecutor.callContract, (params.target, params.data, params.value)); Functions.invokeOnAgent(agent, executor, call); diff --git a/contracts/test/GatewayV2.t.sol b/contracts/test/GatewayV2.t.sol index e037c1a19..533c35410 100644 --- a/contracts/test/GatewayV2.t.sol +++ b/contracts/test/GatewayV2.t.sol @@ -579,6 +579,32 @@ contract GatewayV2Test is Test { vm.expectEmit(true, false, false, true); emit IGatewayV2.InboundMessageDispatched(1, topic, true, relayerRewardAddress); + address bridgeHubAgent = IGatewayV2(address(gateway)).agentOf(Constants.BRIDGE_HUB_AGENT_ID); + vm.deal(bridgeHubAgent, 1 ether); + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + InboundMessageV2({ + origin: Constants.BRIDGE_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeCallContractCommand(0.1 ether) + }), + proof, + makeMockProof(), + relayerRewardAddress + ); + } + + function testAgentCallContractFailsForAssetHub() public { + bytes32 topic = keccak256("topic"); + + // The command fails, then the message is dispatched with success: false + vm.expectEmit(true, false, false, false); + emit IGatewayV2.CommandFailed(1, 0); + vm.expectEmit(true, false, false, true); + emit IGatewayV2.InboundMessageDispatched(1, topic, false, relayerRewardAddress); + vm.deal(assetHubAgent, 1 ether); hoax(relayer, 1 ether); IGatewayV2(address(gateway)) From f41f2e2e530b0aebd4d18bc0697edfc0e2083221 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 13 May 2026 11:48:44 +0800 Subject: [PATCH 2/3] Rename ensureAuthorizedAgent to ensureNonPrivilegedAgent --- contracts/src/Functions.sol | 2 +- contracts/src/v2/Handlers.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/Functions.sol b/contracts/src/Functions.sol index dc178491a..c27c64585 100644 --- a/contracts/src/Functions.sol +++ b/contracts/src/Functions.sol @@ -35,7 +35,7 @@ library Functions { } /// Looks up an agent contract address, failing if no such mapping exists or if the agent is the primary asset hub agent - function ensureAuthorizedAgent(bytes32 agentID) internal view returns (address agent) { + function ensureNonPrivilegedAgent(bytes32 agentID) internal view returns (address agent) { if(agentID == Constants.ASSET_HUB_AGENT_ID) { revert IGatewayBase.AgentNotAuthorized(); } diff --git a/contracts/src/v2/Handlers.sol b/contracts/src/v2/Handlers.sol index 35705b062..f05e4022d 100644 --- a/contracts/src/v2/Handlers.sol +++ b/contracts/src/v2/Handlers.sol @@ -65,7 +65,7 @@ library HandlersV2 { function callContract(bytes32 origin, address executor, bytes calldata data) external { CallContractParams memory params = abi.decode(data, (CallContractParams)); - address agent = Functions.ensureAuthorizedAgent(origin); + address agent = Functions.ensureNonPrivilegedAgent(origin); bytes memory call = abi.encodeCall(AgentExecutor.callContract, (params.target, params.data, params.value)); Functions.invokeOnAgent(agent, executor, call); From 2c8bf0d66f9ed786d382831a12277683b802e782 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 13 May 2026 11:49:40 +0800 Subject: [PATCH 3/3] Rename AgentNotAuthorized to UnauthorizedPrivilegedAgent --- contracts/src/Functions.sol | 2 +- contracts/src/interfaces/IGatewayBase.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/Functions.sol b/contracts/src/Functions.sol index c27c64585..7a1d03031 100644 --- a/contracts/src/Functions.sol +++ b/contracts/src/Functions.sol @@ -37,7 +37,7 @@ library Functions { /// Looks up an agent contract address, failing if no such mapping exists or if the agent is the primary asset hub agent function ensureNonPrivilegedAgent(bytes32 agentID) internal view returns (address agent) { if(agentID == Constants.ASSET_HUB_AGENT_ID) { - revert IGatewayBase.AgentNotAuthorized(); + revert IGatewayBase.UnauthorizedPrivilegedAgent(); } agent = ensureAgent(agentID); } diff --git a/contracts/src/interfaces/IGatewayBase.sol b/contracts/src/interfaces/IGatewayBase.sol index 3a5178d20..5a55112f3 100644 --- a/contracts/src/interfaces/IGatewayBase.sol +++ b/contracts/src/interfaces/IGatewayBase.sol @@ -13,7 +13,7 @@ interface IGatewayBase { error Unsupported(); error InvalidDestinationFee(); error AgentDoesNotExist(); - error AgentNotAuthorized(); + error UnauthorizedPrivilegedAgent(); error TokenAlreadyRegistered(); error TokenMintFailed(); error TokenTransferFailed();