diff --git a/contracts/src/AgentExecutor.sol b/contracts/src/AgentExecutor.sol index 8ce7a9bae5..6727ee8af0 100644 --- a/contracts/src/AgentExecutor.sol +++ b/contracts/src/AgentExecutor.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023 Snowfork pragma solidity 0.8.23; -import {ParaID} from "./Types.sol"; +import {AgentExecuteCommand, ParaID} from "./Types.sol"; import {SubstrateTypes} from "./SubstrateTypes.sol"; import {IERC20} from "./interfaces/IERC20.sol"; @@ -16,6 +16,17 @@ contract AgentExecutor { using SafeTokenTransfer for IERC20; using SafeNativeTransfer for address payable; + /// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms, + /// the `data` parameter is constructed by the BridgeHub parachain. + /// + function execute(bytes memory data) external { + (AgentExecuteCommand command, bytes memory params) = abi.decode(data, (AgentExecuteCommand, bytes)); + if (command == AgentExecuteCommand.TransferToken) { + (address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128)); + _transferToken(token, recipient, amount); + } + } + /// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`, /// as the gateway needs to control an agent's ether balance directly. /// @@ -24,10 +35,15 @@ contract AgentExecutor { } /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. - function transferToken(address token, address recipient, uint128 amount) external { + function _transferToken(address token, address recipient, uint128 amount) internal { IERC20(token).safeTransfer(recipient, amount); } + /// @dev Transfer ERC20 to `recipient`. Only callable via `execute`. + function transferToken(address token, address recipient, uint128 amount) external { + _transferToken(token, recipient, amount); + } + /// @dev Mint ERC20 token to `recipient`. function mintToken(address token, address recipient, uint256 amount) external { ERC20(token).mint(recipient, amount); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index b27a655cb9..0cbee80984 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -18,7 +18,8 @@ import { MultiAddress, Ticket, Costs, - TokenInfo + TokenInfo, + AgentExecuteCommand } from "./Types.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IInitializable} from "./interfaces/IInitializable.sol"; @@ -30,6 +31,7 @@ import {Math} from "./utils/Math.sol"; import {ScaleCodec} from "./utils/ScaleCodec.sol"; import { + AgentExecuteParams, UpgradeParams, CreateAgentParams, CreateChannelParams, @@ -85,6 +87,7 @@ contract Gateway is IGateway, IInitializable { error ChannelAlreadyCreated(); error ChannelDoesNotExist(); error InvalidChannelUpdate(); + error AgentExecutionFailed(bytes returndata); error InvalidAgentExecutionPayload(); error InvalidCodeHash(); error InvalidConstructorParams(); @@ -169,8 +172,8 @@ contract Gateway is IGateway, IInitializable { bool success = true; // Dispatch message to a handler - if (message.command == Command.TransferToken) { - try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} + if (message.command == Command.AgentExecute) { + try Gateway(this).agentExecute{gas: maxDispatchGas}(message.params) {} catch { success = false; } @@ -214,6 +217,11 @@ contract Gateway is IGateway, IInitializable { catch { success = false; } + } else if (message.command == Command.TransferToken) { + try Gateway(this).transferToken{gas: maxDispatchGas}(message.params) {} + catch { + success = false; + } } else if (message.command == Command.RegisterForeignToken) { try Gateway(this).registerForeignToken{gas: maxDispatchGas}(message.params) {} catch { @@ -277,6 +285,24 @@ contract Gateway is IGateway, IInitializable { * Handlers */ + // Execute code within an agent + function agentExecute(bytes calldata data) external onlySelf { + AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); + + address agent = _ensureAgent(params.agentID); + + if (params.payload.length == 0) { + revert InvalidAgentExecutionPayload(); + } + + bytes memory call = abi.encodeCall(AgentExecutor.execute, params.payload); + + (bool success, bytes memory returndata) = Agent(payable(agent)).invoke(AGENT_EXECUTOR, call); + if (!success) { + revert AgentExecutionFailed(returndata); + } + } + /// @dev Create an agent for a consensus system on Polkadot function createAgent(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index 99aedd19d8..8287437d4c 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -5,6 +5,12 @@ pragma solidity 0.8.23; import {ChannelID, OperatingMode} from "./Types.sol"; import {UD60x18} from "prb/math/src/UD60x18.sol"; +// Payload for AgentExecute +struct AgentExecuteParams { + bytes32 agentID; + bytes payload; +} + // Payload for CreateAgent struct CreateAgentParams { /// @dev The agent ID of the consensus system diff --git a/contracts/src/Types.sol b/contracts/src/Types.sol index afe21ab17f..ee35dd026a 100644 --- a/contracts/src/Types.sol +++ b/contracts/src/Types.sol @@ -76,7 +76,7 @@ enum OperatingMode { /// @dev Messages from Polkadot take the form of these commands. enum Command { - TransferToken, + AgentExecute, Upgrade, CreateAgent, CreateChannel, @@ -85,10 +85,16 @@ enum Command { TransferNativeFromAgent, SetTokenTransferFees, SetPricingParameters, + TransferToken, RegisterForeignToken, MintForeignToken } +enum AgentExecuteCommand { + TransferToken, + MintToken +} + /// @dev Application-level costs for a message struct Costs { /// @dev Costs in foreign currency diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index e2670a8098..17aa42563d 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -27,6 +27,7 @@ import {ERC20Lib} from "../src/ERC20Lib.sol"; import { UpgradeParams, CreateAgentParams, + AgentExecuteParams, CreateChannelParams, UpdateChannelParams, SetOperatingModeParams, @@ -39,6 +40,7 @@ import { } from "../src/Params.sol"; import { + AgentExecuteCommand, InboundMessage, OperatingMode, ParaID, @@ -1033,4 +1035,16 @@ contract GatewayTest is Test { IGateway(address(gateway)).sendToken{value: 0.1 ether}(address(dotToken), destPara, recipientAddress32, 1, 1); } + + function testLegacyAgentExecutionForCompatibility() public { + token.transfer(address(assetHubAgent), 200); + + AgentExecuteParams memory params = AgentExecuteParams({ + agentID: assetHubAgentID, + payload: abi.encode(AgentExecuteCommand.TransferToken, abi.encode(address(token), address(account2), 10)) + }); + + bytes memory encodedParams = abi.encode(params); + GatewayMock(address(gateway)).agentExecutePublic(encodedParams); + } } diff --git a/contracts/test/mocks/GatewayMock.sol b/contracts/test/mocks/GatewayMock.sol index 3a86626464..29e04cfb48 100644 --- a/contracts/test/mocks/GatewayMock.sol +++ b/contracts/test/mocks/GatewayMock.sol @@ -19,6 +19,10 @@ contract GatewayMock is Gateway { uint8 foreignTokenDecimals ) Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubHubAgentID, foreignTokenDecimals) {} + function agentExecutePublic(bytes calldata params) external { + this.agentExecute(params); + } + function createAgentPublic(bytes calldata params) external { this.createAgent(params); }