diff --git a/evm/package.json b/evm/package.json index e390823b..0d6d353f 100644 --- a/evm/package.json +++ b/evm/package.json @@ -16,8 +16,11 @@ "dependencies": { "@polytope-labs/erc6160": "^0.3.1", "@polytope-labs/ismp-solidity": "^0.7.3", + "@polytope-labs/ismp-solidity-v1": "npm:@polytope-labs/ismp-solidity@^1.1.0", "@polytope-labs/solidity-merkle-trees": "^0.3.4", "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@uniswap/v3-core": "^1.0.1", + "@uniswap/v3-periphery": "^1.4.4", "openzeppelin-solidity": "4.8.1", "prettier": "^3.3.3", "prettier-plugin-solidity": "^1.3.1" diff --git a/evm/pnpm-lock.yaml b/evm/pnpm-lock.yaml new file mode 100644 index 00000000..5992332f --- /dev/null +++ b/evm/pnpm-lock.yaml @@ -0,0 +1,180 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@polytope-labs/erc6160': + specifier: ^0.3.1 + version: 0.3.1 + '@polytope-labs/ismp-solidity': + specifier: ^0.7.3 + version: 0.7.4 + '@polytope-labs/ismp-solidity-v1': + specifier: npm:@polytope-labs/ismp-solidity@^1.1.0 + version: '@polytope-labs/ismp-solidity@1.1.0' + '@polytope-labs/solidity-merkle-trees': + specifier: ^0.3.4 + version: 0.3.4 + '@uniswap/v2-periphery': + specifier: ^1.1.0-beta.0 + version: 1.1.0-beta.0 + '@uniswap/v3-core': + specifier: ^1.0.1 + version: 1.0.1 + '@uniswap/v3-periphery': + specifier: ^1.4.4 + version: 1.4.4 + openzeppelin-solidity: + specifier: 4.8.1 + version: 4.8.1 + prettier: + specifier: ^3.3.3 + version: 3.5.0 + prettier-plugin-solidity: + specifier: ^1.3.1 + version: 1.4.2(prettier@3.5.0) + +packages: + + '@openzeppelin/contracts@3.4.2-solc-0.7': + resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} + + '@polytope-labs/erc6160@0.3.1': + resolution: {integrity: sha512-rcadGvzF7ThHkOzgbsDyOiTwEnJN9okHSOK4wuCWiBgfic1raiWvpHJDRJE6egaJxcXM0wvdmRrtQlpammSRqQ==} + + '@polytope-labs/ismp-solidity@0.7.4': + resolution: {integrity: sha512-NLthu+D+ycLwMkSgdURpzfotKs9NtFuI/TKcNm+8XH8FF74B26o8BOJMtvE2TD2TPOxu9rcs54u2p5HNaHoU2w==} + + '@polytope-labs/ismp-solidity@1.1.0': + resolution: {integrity: sha512-p9eNRyOr4zqQnuRtekZgAL3prRRSZ7siCHqAywBAi+H86rlE/wR6Ukn/Q9CvXdIpZmVFTZrdljV1LW0vzACRTw==} + + '@polytope-labs/solidity-merkle-trees@0.3.4': + resolution: {integrity: sha512-ldLirdWNd7qNi8iRfpwlHCwy/FrVMd5wbsu9qVxx/IyOnbQnzzU816AfL3Zn01iN8DhB5UFLMkQ/HmF+VOLGFA==} + + '@solidity-parser/parser@0.19.0': + resolution: {integrity: sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA==} + + '@uniswap/lib@1.1.1': + resolution: {integrity: sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg==} + engines: {node: '>=10'} + + '@uniswap/lib@4.0.1-alpha': + resolution: {integrity: sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==} + engines: {node: '>=10'} + + '@uniswap/v2-core@1.0.0': + resolution: {integrity: sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==} + engines: {node: '>=10'} + + '@uniswap/v2-core@1.0.1': + resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} + engines: {node: '>=10'} + + '@uniswap/v2-periphery@1.1.0-beta.0': + resolution: {integrity: sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==} + engines: {node: '>=10'} + + '@uniswap/v3-core@1.0.1': + resolution: {integrity: sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==} + engines: {node: '>=10'} + + '@uniswap/v3-periphery@1.4.4': + resolution: {integrity: sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==} + engines: {node: '>=10'} + + base64-sol@1.0.1: + resolution: {integrity: sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==} + + openzeppelin-solidity@4.8.1: + resolution: {integrity: sha512-KM0pVpfrCBdifqN2ZeJZFvFuoGz3GmI4Ty/ceKNkcaf7VVWo/rLOfc5EiLh+Ukb5NadNmYo8WMeGhFA8hVWDpg==} + hasBin: true + + prettier-plugin-solidity@1.4.2: + resolution: {integrity: sha512-VVD/4XlDjSzyPWWCPW8JEleFa8JNKFYac5kNlMjVXemQyQZKfpekPMhFZSePuXB6L+RixlFvWe20iacGjFYrLw==} + engines: {node: '>=18'} + peerDependencies: + prettier: '>=2.3.0' + + prettier@3.5.0: + resolution: {integrity: sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==} + engines: {node: '>=14'} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + +snapshots: + + '@openzeppelin/contracts@3.4.2-solc-0.7': {} + + '@polytope-labs/erc6160@0.3.1': + dependencies: + openzeppelin-solidity: 4.8.1 + prettier: 3.5.0 + prettier-plugin-solidity: 1.4.2(prettier@3.5.0) + + '@polytope-labs/ismp-solidity@0.7.4': + dependencies: + '@polytope-labs/solidity-merkle-trees': 0.3.4 + openzeppelin-solidity: 4.8.1 + prettier: 3.5.0 + prettier-plugin-solidity: 1.4.2(prettier@3.5.0) + + '@polytope-labs/ismp-solidity@1.1.0': + dependencies: + '@polytope-labs/solidity-merkle-trees': 0.3.4 + openzeppelin-solidity: 4.8.1 + prettier: 3.5.0 + prettier-plugin-solidity: 1.4.2(prettier@3.5.0) + + '@polytope-labs/solidity-merkle-trees@0.3.4': + dependencies: + openzeppelin-solidity: 4.8.1 + prettier: 3.5.0 + prettier-plugin-solidity: 1.4.2(prettier@3.5.0) + + '@solidity-parser/parser@0.19.0': {} + + '@uniswap/lib@1.1.1': {} + + '@uniswap/lib@4.0.1-alpha': {} + + '@uniswap/v2-core@1.0.0': {} + + '@uniswap/v2-core@1.0.1': {} + + '@uniswap/v2-periphery@1.1.0-beta.0': + dependencies: + '@uniswap/lib': 1.1.1 + '@uniswap/v2-core': 1.0.0 + + '@uniswap/v3-core@1.0.1': {} + + '@uniswap/v3-periphery@1.4.4': + dependencies: + '@openzeppelin/contracts': 3.4.2-solc-0.7 + '@uniswap/lib': 4.0.1-alpha + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.1 + base64-sol: 1.0.1 + + base64-sol@1.0.1: {} + + openzeppelin-solidity@4.8.1: {} + + prettier-plugin-solidity@1.4.2(prettier@3.5.0): + dependencies: + '@solidity-parser/parser': 0.19.0 + prettier: 3.5.0 + semver: 7.7.1 + + prettier@3.5.0: {} + + semver@7.7.1: {} diff --git a/evm/remappings.txt b/evm/remappings.txt index 41257446..d445c05e 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -1,7 +1,10 @@ @polytope-labs/ismp-solidity/=node_modules/@polytope-labs/ismp-solidity/interfaces/ +@polytope-labs/ismp-solidity-v1/=node_modules/@polytope-labs/ismp-solidity-v1/interfaces/ @openzeppelin/=node_modules/openzeppelin-solidity @polytope-labs/solidity-merkle-trees/=node_modules/@polytope-labs/solidity-merkle-trees/ @polytope-labs/erc6160/=node_modules/@polytope-labs/erc6160/src/ @uniswap/v2-periphery/=node_modules/@uniswap/v2-periphery +@uniswap/v3-periphery/=node_modules/@uniswap/v3-periphery +@uniswap/v3-core/=node_modules/@uniswap/v3-core stringutils/=lib/solidity-stringutils/src/ @sp1-contracts/=lib/sp1-contracts/contracts/src/ \ No newline at end of file diff --git a/evm/src/interfaces/IWETH.sol b/evm/src/interfaces/IWETH.sol new file mode 100644 index 00000000..86e210ca --- /dev/null +++ b/evm/src/interfaces/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256) external; +} \ No newline at end of file diff --git a/evm/src/modules/IntentGateway.sol b/evm/src/modules/IntentGateway.sol new file mode 100644 index 00000000..f93fa220 --- /dev/null +++ b/evm/src/modules/IntentGateway.sol @@ -0,0 +1,656 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.17; + +import {DispatchPost, DispatchGet, IDispatcher, PostRequest} from "@polytope-labs/ismp-solidity-v1/IDispatcher.sol"; +import {BaseIsmpModule, IncomingPostRequest, IncomingGetResponse} from "@polytope-labs/ismp-solidity-v1/IIsmpModule.sol"; +import {StateMachine} from "@polytope-labs/ismp-solidity-v1/StateMachine.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; + +import {ICallDispatcher} from "../interfaces/ICallDispatcher.sol"; + +/** + * @notice Tokens that must be received for a valid order fulfillment + */ +struct PaymentInfo { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + bytes32 token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + bytes32 beneficiary; +} + +/** + * @notice Tokens that must be escrowed for an order + */ +struct TokenInfo { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + bytes32 token; + /// @dev The amount of the token to be sent + uint256 amount; +} + +/** + * @dev Represents an order in the IntentGateway module. + * @param Order The structure defining an order. + */ +struct Order { + /// @dev The address of the user who is initiating the transfer + bytes32 user; + /// @dev The state machine identifier of the origin chain + bytes sourceChain; + /// @dev The state machine identifier of the destination chain + bytes destChain; + /// @dev The block number by which the order must be filled on the destination chain + uint256 deadline; + /// @dev The nonce of the order + uint256 nonce; + /// @dev Represents the dispatch fees associated with the IntentGateway. + uint256 fees; + /// @dev The tokens that the filler will provide. + PaymentInfo[] outputs; + /// @dev The tokens that are escrowed for the filler. + TokenInfo[] inputs; + /// @dev A bytes array to store the calls if any. + bytes callData; +} + +/** + * @dev Struct to define the parameters for the IntentGateway module. + */ +struct Params { + /// @dev The address of the host contract + address host; + /// @dev Address of the dispatcher contract responsible for handling intents. + address dispatcher; +} + +/** + * @dev Struct representing the body of a request. + */ +struct RequestBody { + /// @dev Represents the commitment of an order. This is typically a hash that uniquely identifies the order. + bytes32 commitment; + /// @dev Stores the identifier for the beneficiary. + bytes32 beneficiary; + /// @dev An array of token identifiers. Each element in the array represents a unique token involved in the order. + TokenInfo[] tokens; +} + +/** + * @notice A struct representing the options for filling an intent. + * @dev This struct is used to specify various parameters and options + * when filling an intent in the IntentGateway contract. + */ +struct FillOptions { + /// @dev The fee paid to the relayer for processing transactions. + uint256 relayerFee; +} + +/** + * @dev Struct representing the options for canceling an intent. + */ +struct CancelOptions { + /// @dev The fee paid to the relayer for processing transactions. + uint256 relayerFee; + /// @dev Stores the height value. + uint256 height; +} + +/** + * @dev Struct representing a new instance of IntentGateway. + */ +struct NewDeployment { + /// @dev Identifier for the state machine. + bytes stateMachineId; + /// @dev A bytes32 variable to store the gateway identifier. + bytes32 gateway; +} + +/** + * @title IntentGateway + * @author Polytope Labs (hello@polytope.technology) + * + * @dev The IntentGateway allows for the creation and fulfillment of cross-chain orders. + */ +contract IntentGateway is BaseIsmpModule { + using SafeERC20 for IERC20; + + /** + * @dev Enum representing the different kinds of incoming requests that can be executed. + */ + enum RequestKind { + /// @dev Identifies a request for redeeming an escrow. + RedeemEscrow, + /// @dev Identifies a request for recording new contract deployments + NewDeployment, + /// @dev Identifies a request for updating parameters. + UpdateParams + } + + /** + * @dev Address constant for transaction fees, derived from the keccak256 hash of the string "txFees". + * This address is used to store or reference the transaction fees within the contract. + */ + address private constant TRANSACTION_FEES = address(uint160(uint256(keccak256("txFees")))); + + /** + * @notice Constant representing a filled slot in big endian format + * @dev Hex value 0x05 padded with leading zeros to fill 32 bytes + */ + bytes32 constant FILLED_SLOT_BIG_ENDIAN_BYTES = + hex"0000000000000000000000000000000000000000000000000000000000000005"; + + /** + * @dev Private variable to store the nonce value. + * This nonce is used to ensure the uniqueness of orders. + */ + uint256 private _nonce; + + /** + * @dev Private variable to store the parameters for the IntentGateway module. + * This variable is of type `Params` and is used internally within the contract. + */ + Params private _params; + + /** + * @dev Address of the admin, which can initialize the contract. + * The admin is reset to the zero address after initialization. + */ + address private _admin; + + /** + * @dev Mapping to store orders. + * The outer mapping key is a bytes32 value representing the order commitment. + * The inner mapping key is an address representing the escrowed token contract. + * The inner mapping value is a uint256 representing the order amount. + */ + mapping(bytes32 => mapping(address => uint256)) private _orders; + + /** + * @dev Mapping to store the addresses associated with filled intents. + * The key is a bytes32 hash representing the intent, and the value is the address + * that filled the intent. + */ + mapping(bytes32 => address) private _filled; + + /** + * @dev Mapping to store instances of contracts. + * The key the keccak(stateMachineId) and the value is the address of a known contract instance. + */ + mapping(bytes32 => bytes32) private _instances; + + /// @notice Thrown when an unauthorized action is attempted. + error Unauthorized(); + + /// @notice Thrown when an invalid input is provided. + error InvalidInput(); + + /// @notice Thrown when an action is attempted on an expired order. + error Expired(); + + /// @notice Thrown when there are insufficient native tokens to complete an action. + error InsufficientNativeToken(); + + /// @notice Thrown when an action is attempted on an order that has not yet expired. + error NotExpired(); + + /// @notice Thrown when an action is attempted on an order that has already been filled. + error Filled(); + + /// @notice Thrown when an action is attempted on the wrong chain. + error WrongChain(); + + /// @notice Thrown when an action is attempted on an unknown order. + error UnknownOrder(); + + /** + * @dev Emitted when an order is placed. + */ + event OrderPlaced( + /// @dev The address of the user who is initiating the transfer + bytes32 user, + /// @dev The state machine identifier of the origin chain + bytes sourceChain, + /// @dev The state machine identifier of the destination chain + bytes destChain, + /// @dev The block number by which the order must be filled on the destination chain + uint256 deadline, + /// @dev The nonce of the order + uint256 nonce, + /// @dev Represents the dispatch fees associated with the IntentGateway. + uint256 fees, + /// @dev The tokens that the filler will provide. + PaymentInfo[] outputs, + /// @dev The tokens that are escrowed for the filler. + TokenInfo[] inputs, + /// @dev A bytes array to store the calls if any. + bytes callData + ); + + /** + * @dev Emitted when an order is filled. + * @param commitment The unique identifier of the order. + * @param filler The address of the entity that filled the order. + */ + event OrderFilled(bytes32 indexed commitment, address filler); + + /** + * @dev Emitted when an escrow is released. + * @param commitment The unique identifier of the order. + */ + event EscrowReleased(bytes32 indexed commitment); + + /** + * @dev Emitted when an escrow is refunded. + * @param commitment The unique identifier of the order. + */ + event EscrowRefunded(bytes32 indexed commitment); + + /** + * @dev Emitted when the parameters are updated. + * @param previous The previous parameters. + * @param current The current parameters. + */ + event ParamsUpdated(Params previous, Params current); + + /** + * @dev Emitted when a new deployment of IntentGateway is added. + * @param stateMachineId The identifier for the state machine. + * @param gateway The gateway identifier. + */ + event NewDeploymentAdded(bytes stateMachineId, bytes32 gateway); + + constructor(address admin) { + _admin = admin; + } + + /** + * @notice Fallback function to receive ether + * @dev This function is called when ether is sent to the contract without data + * @custom:note The function is marked payable to allow receiving ether + */ + receive() external payable {} + + /** + * @dev Should return the `IsmpHost` address for the current chain. + * The `IsmpHost` is an immutable contract that will never change. + */ + function host() public view override returns (address) { + return _params.host; + } + + /** + * @dev Fetch the IntentGateway contract instance for a chain. + */ + function instance(bytes calldata stateMachineId) public view returns (bytes32) { + bytes32 gateway = _instances[keccak256(stateMachineId)]; + return gateway == bytes32(0) ? bytes32(uint256(uint160(address(this)))) : gateway; + } + + /** + * @dev Checks that the request originates from a known instance of the IntentGateway. + */ + modifier authenticate(PostRequest calldata request) { + bytes32 module = bytes32(request.from[:32]); + // IntentGateway only accepts incoming assets from itself or known instances + if (instance(request.source) != module) revert Unauthorized(); + _; + } + + /** + * @notice Sets the parameters for the IntentGateway. + * @param p The parameters to be set, encapsulated in a Params struct. + */ + function setParams(Params memory p) public { + if (msg.sender != _admin) revert Unauthorized(); + // infinite approval to save on gas + IERC20(IDispatcher(p.host).feeToken()).approve(p.host, type(uint256).max); + + _admin = address(0); + _params = p; + } + + /** + * @notice Retrieves the current parameter settings for the IntentGateway module + * @dev Returns a struct containing all configurable parameters + * @return Params A struct containing the module's current parameters + */ + function params() public view returns (Params memory) { + return _params; + } + + /** + * @notice Calculates the commitment slot hash required for storage queries. + * @dev The commitment slot hash is used as part of the proof verification process + * @param commitment The commitment value as a bytes32 hash + * @return bytes The calculated commitment slot hash + */ + function calculateCommitmentSlotHash(bytes32 commitment) public pure returns (bytes memory) { + return abi.encodePacked(keccak256(abi.encodePacked(FILLED_SLOT_BIG_ENDIAN_BYTES, commitment))); + } + + /** + * @notice Places an order with the given order details. + * @dev This function allows users to place an order by providing the order details. + * @param order The order details to be placed. + */ + function placeOrder(Order memory order) public payable { + address hostAddr = host(); + // fill out the order preludes + order.nonce = _nonce; + order.user = bytes32(uint256(uint160(msg.sender))); + order.sourceChain = IDispatcher(hostAddr).host(); + + bytes32 commitment = keccak256(abi.encode(order)); + + // escrow tokens + uint256 msgValue = msg.value; + uint256 inputsLen = order.inputs.length; + for (uint256 i = 0; i < inputsLen; i++) { + if (order.inputs[i].amount == 0) revert InvalidInput(); + address token = address(uint160(uint256(order.inputs[i].token))); + if (token == address(0)) { + // native token + if (msgValue < order.inputs[i].amount) revert InsufficientNativeToken(); + msgValue -= order.inputs[i].amount; + } else { + IERC20(token).safeTransferFrom(msg.sender, address(this), order.inputs[i].amount); + } + + // commit order + _orders[commitment][token] += order.inputs[i].amount; + } + + if (order.fees > 0) { + // escrow fees + address feeToken = IDispatcher(hostAddr).feeToken(); + if (msgValue > 0) { + address uniswapV2 = IDispatcher(hostAddr).uniswapV2Router(); + address WETH = IUniswapV2Router02(uniswapV2).WETH(); + address[] memory path = new address[](2); + path[0] = WETH; + path[1] = IDispatcher(hostAddr).feeToken(); + IUniswapV2Router02(uniswapV2).swapExactETHForTokens{value: msgValue}( + order.fees, + path, + address(this), + block.timestamp + ); + } else { + IERC20(feeToken).safeTransferFrom(msg.sender, address(this), order.fees); + } + _orders[commitment][TRANSACTION_FEES] = order.fees; + } + + _nonce += 1; + emit OrderPlaced({ + user: order.user, + sourceChain: order.sourceChain, + destChain: order.destChain, + deadline: order.deadline, + nonce: order.nonce, + fees: order.fees, + outputs: order.outputs, + inputs: order.inputs, + callData: order.callData + }); + } + + /** + * @notice Fills an order with the specified options. + * @param order The order to be filled. + * @param options The options to be used when filling the order. + * @dev This function is payable and can accept Ether. + */ + function fillOrder(Order calldata order, FillOptions memory options) public payable { + address hostAddr = host(); + // Ensure the order is being filled on the correct chain + if (keccak256(order.destChain) != keccak256(IDispatcher(hostAddr).host())) revert WrongChain(); + + // Ensure the order has not expired + if (order.deadline < block.number) revert Expired(); + + // Ensure the order has not been filled + bytes32 commitment = keccak256(abi.encode(order)); + if (_filled[commitment] != address(0)) revert Filled(); + + // fill the order + uint256 msgValue = msg.value; + uint256 outputsLen = order.outputs.length; + for (uint256 i = 0; i < outputsLen; i++) { + address token = address(uint160(uint256(order.outputs[i].token))); + address beneficiary = address(uint160(uint256(order.outputs[i].beneficiary))); + + if (token == address(0)) { + // native token + if (msgValue < order.outputs[i].amount) revert InsufficientNativeToken(); + (bool sent, ) = beneficiary.call{value: order.outputs[i].amount}(""); + if (!sent) revert InsufficientNativeToken(); + msgValue -= order.outputs[i].amount; + } else { + IERC20(token).safeTransferFrom(msg.sender, beneficiary, order.outputs[i].amount); + } + } + + // dispatch calls if any + if (order.callData.length > 0) { + ICallDispatcher(_params.dispatcher).dispatch(order.callData); + } + + // construct settlement message + bytes memory data = abi.encode( + RequestBody({ + commitment: commitment, + tokens: order.inputs, + beneficiary: bytes32(uint256(uint160(msg.sender))) + }) + ); + DispatchPost memory request = DispatchPost({ + dest: order.sourceChain, + to: abi.encodePacked(instance(order.sourceChain)), + body: bytes.concat(bytes1(uint8(RequestKind.RedeemEscrow)), data), + timeout: 0, + fee: options.relayerFee, + payer: msg.sender + }); + + // dispatch settlement message + if (msgValue > 0) { + // there's some native tokens left to pay for request dispatch + IDispatcher(hostAddr).dispatch{value: msgValue}(request); + } else { + // try to pay for dispatch with fee token + address feeToken = IDispatcher(hostAddr).feeToken(); + uint256 fee = quote(request); + IERC20(feeToken).safeTransferFrom(msg.sender, address(this), fee); + IDispatcher(hostAddr).dispatch(request); + } + + _filled[commitment] = msg.sender; + emit OrderFilled({commitment: commitment, filler: msg.sender}); + } + + /** + * @notice Executes an incoming post request. + * @dev This function is called when an incoming post request is accepted. + * It is only accessible by the host. + * @param incoming The incoming post request data. + */ + function onAccept(IncomingPostRequest calldata incoming) external override onlyHost { + RequestKind kind = RequestKind(uint8(incoming.request.body[0])); + if (kind == RequestKind.RedeemEscrow) return redeem(incoming); + + // only hyperbridge is permitted to perfom these actions + if (keccak256(incoming.request.source) != keccak256(IDispatcher(host()).hyperbridge())) revert Unauthorized(); + if (kind == RequestKind.NewDeployment) { + NewDeployment memory body = abi.decode(incoming.request.body[1:], (NewDeployment)); + _instances[keccak256(body.stateMachineId)] = body.gateway; + + emit NewDeploymentAdded({stateMachineId: body.stateMachineId, gateway: body.gateway}); + } else if (kind == RequestKind.UpdateParams) { + Params memory body = abi.decode(incoming.request.body[1:], (Params)); + emit ParamsUpdated({previous: _params, current: body}); + + _params = body; + } + } + + /** + * @notice Redeems the escrowed tokens for an incoming post request. + * @dev This function is marked as internal and requires authentication. + * @param incoming The incoming post request data. + */ + function redeem(IncomingPostRequest calldata incoming) internal authenticate(incoming.request) { + RequestBody memory body = abi.decode(incoming.request.body[1:], (RequestBody)); + address beneficiary = address(uint160(uint256(body.beneficiary))); + + // redeem escrowed tokens + uint256 len = body.tokens.length; + for (uint256 i = 0; i < len; i++) { + address token = address(uint160(uint256(body.tokens[i].token))); + uint256 amount = body.tokens[i].amount; + if (_orders[body.commitment][token] == 0) revert UnknownOrder(); + + if (token == address(0)) { + (bool sent, ) = beneficiary.call{value: amount}(""); + if (!sent) revert InsufficientNativeToken(); + } else { + IERC20(token).safeTransfer(beneficiary, amount); + } + + _orders[body.commitment][token] -= amount; + } + + // redeem tx fees + uint256 fees = _orders[body.commitment][TRANSACTION_FEES]; + if (fees > 0) { + IERC20(IDispatcher(host()).feeToken()).safeTransfer(beneficiary, fees); + + delete _orders[body.commitment][TRANSACTION_FEES]; + } + + _filled[body.commitment] = incoming.relayer; + + emit EscrowReleased({commitment: body.commitment}); + } + + /** + * @notice Cancels an existing order. + * @param order The order to be canceled. + * @param options Additional options for the cancellation process. + * @dev This function can only be called by the order owner and requires a payment. + * It will initiate a storage query on the source chain to verify the order has not + * yet been filled. If the order has not been filled, the tokens will be released. + */ + function cancelOrder(Order calldata order, CancelOptions memory options) public payable { + bytes32 commitment = keccak256(abi.encode(order)); + + // only owner can cancel order + if (order.user != bytes32(uint256(uint160(msg.sender)))) revert Unauthorized(); + + // order has not yet expired + if (options.height <= order.deadline) revert NotExpired(); + + // order has already been filled + if (_filled[commitment] != address(0)) revert Filled(); + + // fetch the tokens + uint256 inputsLen = order.inputs.length; + for (uint256 i = 0; i < inputsLen; i++) { + // check for order existence + if (_orders[commitment][address(uint160(uint256(order.inputs[i].token)))] == 0) revert UnknownOrder(); + } + + bytes memory context = abi.encode( + RequestBody({commitment: commitment, tokens: order.inputs, beneficiary: order.user}) + ); + + bytes[] memory keys = new bytes[](1); + keys[0] = bytes.concat( + // contract address + abi.encodePacked(address(uint160(uint256(instance(order.destChain))))), + // storage slot hash + calculateCommitmentSlotHash(commitment) + ); + DispatchGet memory request = DispatchGet({ + dest: order.destChain, + keys: keys, + timeout: 0, + height: uint64(options.height), + fee: options.relayerFee, + context: context + }); + + // dispatch storage query request + address hostAddr = host(); + if (msg.value > 0) { + // there's some native tokens left to pay for request dispatch + IDispatcher(hostAddr).dispatch{value: msg.value}(request); + } else { + // try to pay for dispatch with fee token + address feeToken = IDispatcher(hostAddr).feeToken(); + uint256 fee = quote(request); + IERC20(feeToken).safeTransferFrom(msg.sender, address(this), fee); + IDispatcher(hostAddr).dispatch(request); + } + } + + /** + * @notice Handles the response for an incoming GET request. + * @dev This function is called by the host to process the response of a GET request. + * @param incoming The response data structure for the GET request. + * Only the host can call this function. + */ + function onGetResponse(IncomingGetResponse memory incoming) external override onlyHost { + if (incoming.response.values[0].value.length != 0) revert Filled(); + + RequestBody memory body = abi.decode(incoming.response.request.context, (RequestBody)); + address beneficiary = address(uint160(uint256(body.beneficiary))); + + // recover escrowed tokens + uint256 len = body.tokens.length; + for (uint256 i = 0; i < len; i++) { + address token = address(uint160(uint256(body.tokens[i].token))); + uint256 amount = body.tokens[i].amount; + if (_orders[body.commitment][token] == 0) revert UnknownOrder(); + + if (token == address(0)) { + (bool sent, ) = beneficiary.call{value: amount}(""); + if (!sent) revert InsufficientNativeToken(); + } else { + IERC20(token).safeTransfer(beneficiary, amount); + } + + _orders[body.commitment][token] -= amount; + } + + // recover tx fees + uint256 fees = _orders[body.commitment][TRANSACTION_FEES]; + if (fees > 0) { + IERC20(IDispatcher(host()).feeToken()).safeTransfer(beneficiary, fees); + + delete _orders[body.commitment][TRANSACTION_FEES]; + } + + emit EscrowRefunded({commitment: body.commitment}); + } +} diff --git a/evm/src/modules/UniV3UniswapV2Wrapper.sol b/evm/src/modules/UniV3UniswapV2Wrapper.sol new file mode 100644 index 00000000..fcbf20c0 --- /dev/null +++ b/evm/src/modules/UniV3UniswapV2Wrapper.sol @@ -0,0 +1,169 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.17; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {IQuoter} from "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; + +import {IWETH} from "../interfaces/IWETH.sol"; + +/** + * @title UniV3UniswapV2Wrapper + * @author Polytope Labs (hello@polytope.technology) + * + * @dev A module that wraps the Uniswap V3 Swap Router to provide a compatible interface with the Uniswap V2 Router. + */ +contract UniV3UniswapV2Wrapper { + using SafeERC20 for IERC20; + + struct Params { + /// @dev Address of the Wrapped Ether (WETH) token. + address WETH; + /// @dev Address of the Uniswap V3 Swap Router. + address swapRouter; + /// @dev Address of the Uniswap V3 quoter contract + address quoter; + } + + /** + * @dev Private variable to store the parameters for the UniV3UniswapV2Wrapper module. + */ + Params private _params; + + /** + * @dev The maximum allowable fees for the UniV3UniswapV2Wrapper module. + * This constant represents a fee of 0.3%, which is equivalent to 3,000 basis points. + */ + uint24 constant MAX_FEES = 3_000; // 0.3% + + /** + * @dev Address of the admin. This address has special privileges and control over certain + * aspects of the contract. + */ + address private _admin; + + /** + * @dev Error indicating that a deposit operation has failed. + */ + error DepositFailed(); + + /** + * @dev Error indicating that a refund operation has failed. + */ + error RefundFailed(); + + /** + * @dev Error indicating that the caller is not authorized. + */ + error Unauthorized(); + + /** + * @dev Error indicating that the first token in the path is not WETH. + */ + error InvalidWethAddress(); + + constructor(address admin) { + _admin = admin; + } + + /** + * @notice Initializes the Uniswap V3 to V2 wrapper module + * @dev Can only be called once + * @param params Initialization parameters. + */ + function init(Params memory params) public { + if (msg.sender != _admin) revert Unauthorized(); + // approve the swap router to spend WETH + IWETH(params.WETH).approve(params.swapRouter, type(uint256).max); + + _params = params; + // admin can only do this once + _admin = address(0); + } + + /** + * @dev Returns the address for the wrapped native token + */ + function WETH() public view returns (address) { + return _params.WETH; + } + /** + * @notice Swaps exact amount of ETH for specified amount of tokens through V3. + * @dev Will attempt to swap using the uniswap V3 interface + * @param amountOut The amount of tokens to receive + * @param path Array of token addresses representing the swap path + * @param recipient Address that will receive the output tokens + * @param deadline Unix timestamp deadline by which the transaction must confirm + * @return amounts Array of amounts for intermediate and output token transfers + */ + function swapETHForExactTokens( + uint256 amountOut, + address[] calldata path, + address recipient, + uint256 deadline + ) external payable returns (uint256[] memory) { + address weth = _params.WETH; + if (path[0] != weth) revert InvalidWethAddress(); + + // wrap native token, univ3 doesn't support native ETH + (bool sent, ) = weth.call{value: msg.value}(""); + if (!sent) revert DepositFailed(); + + ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ + tokenIn: weth, + tokenOut: path[1], + fee: MAX_FEES, + recipient: recipient, + deadline: deadline, + amountOut: amountOut, + amountInMaximum: msg.value, + sqrtPriceLimitX96: 0 + }); + uint256 spent = ISwapRouter(_params.swapRouter).exactOutputSingle(params); + if (spent < msg.value) { + uint256 refund = msg.value - spent; + // unwrap the unspent WETH + IWETH(weth).withdraw(refund); + (bool success, ) = recipient.call{value: refund}(""); + if (!success) revert RefundFailed(); + } + + uint256[] memory amounts = new uint256[](2); + amounts[0] = spent; + amounts[1] = amountOut; + + return amounts; + } + + /** + * @notice Given an output amount of an asset and a path, returns the input amounts required. + * @param amountOut The amount of the asset you want to receive. + * @param path An array of token addresses representing the path of the swap. + * @return amounts An array of input amounts required to obtain the output amount. + */ + function getAmountsIn(uint amountOut, address[] calldata path) external returns (uint[] memory) { + uint256 quote = IQuoter(_params.quoter).quoteExactOutputSingle(path[0], path[1], MAX_FEES, amountOut, 0); + uint256[] memory out = new uint256[](1); + out[0] = quote; + return out; + } + + /// @notice Accepts ETH transfers to this contract + /// @dev Fallback function to receive ETH payments, required for unwrapping WETH + receive() external payable {} +} diff --git a/evm/test/IntentGatewayTest.sol b/evm/test/IntentGatewayTest.sol new file mode 100644 index 00000000..3f804433 --- /dev/null +++ b/evm/test/IntentGatewayTest.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; + +import {MainnetForkBaseTest} from "./MainnetForkBaseTest.sol"; +import {IntentGateway, RequestBody, Order, Params, PaymentInfo, TokenInfo, FillOptions, CancelOptions, NewDeployment} from "../src/modules/IntentGateway.sol"; + +import {IncomingPostRequest, IncomingGetResponse, BaseIsmpModule} from "@polytope-labs/ismp-solidity-v1/IIsmpModule.sol"; +import {PostRequest, GetResponse, GetRequest} from "@polytope-labs/ismp-solidity-v1/Message.sol"; +import {StorageValue} from "@polytope-labs/solidity-merkle-trees/src/Types.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract IntentGatewayTest is MainnetForkBaseTest { + using SafeERC20 for IERC20; + IntentGateway public intentGateway; + + address public filler = address(0x27865); + + receive() external payable {} + + function setUp() public override { + super.setUp(); + intentGateway = new IntentGateway(address(this)); + Params memory params = Params({host: address(host), dispatcher: address(dispatcher)}); + intentGateway.setParams(params); + vm.stopPrank(); + + // Set initial balances and approvals + vm.deal(address(this), 100 ether); + vm.deal(filler, 100 ether); + } + + // Helper to create an Order struct + function createTestOrder(bytes memory callData) internal view returns (Order memory) { + PaymentInfo[] memory outputs = new PaymentInfo[](1); + outputs[0] = PaymentInfo({ + token: bytes32(uint256(uint160(address(0)))), // Native token + amount: 1 ether, + beneficiary: bytes32(uint256(uint160(address(this)))) + }); + + TokenInfo[] memory inputs = new TokenInfo[](1); + inputs[0] = TokenInfo({ + token: bytes32(uint256(uint160(address(0)))), // Native token + amount: 1 ether + }); + + return + Order({ + user: bytes32(uint256(uint160(address(this)))), + sourceChain: host.host(), + nonce: 0, + destChain: bytes("EVM-1"), + deadline: block.number + 100, + fees: 0, + outputs: outputs, + inputs: inputs, + callData: callData + }); + } + + /** + * @notice Tests the order placement functionality of the IntentGateway + * @dev This test function verifies the correct behavior of placing orders through the gateway + */ + function testPlaceOrder() public { + Order memory order = createTestOrder(bytes("")); + + // Place the order + intentGateway.placeOrder{value: 1 ether}(order); + + vm.expectRevert(IntentGateway.InsufficientNativeToken.selector); + intentGateway.placeOrder{value: 0.9 ether}(order); + + // Check the balances + assertEq(address(this).balance, 99 ether); + assertEq(address(intentGateway).balance, 1 ether); + } + + // write a test for the `fillOrder` function + function testFillOrder() public { + Order memory order = createTestOrder(bytes("")); + + // Place the order + intentGateway.placeOrder{value: 1 ether}(order); + + assertEq(filler.balance, 100 ether); + assertEq(address(intentGateway).balance, 1 ether); + + Order memory order1 = abi.decode(abi.encode(order), (Order)); + order1.destChain = bytes("EVM-2"); + vm.expectRevert(IntentGateway.WrongChain.selector); + intentGateway.fillOrder{value: 2 ether}(order1, FillOptions({relayerFee: 0})); + + uint256 initial = block.number; + vm.roll(order.deadline + 1); + assertEq(block.number, order.deadline + 1); + vm.expectRevert(IntentGateway.Expired.selector); + intentGateway.fillOrder{value: 2 ether}(order, FillOptions({relayerFee: 0})); + vm.roll(initial); + + vm.expectRevert(IntentGateway.InsufficientNativeToken.selector); + intentGateway.fillOrder{value: 0.9 ether}(order, FillOptions({relayerFee: 0})); + + // Fill the order + vm.prank(filler); + intentGateway.fillOrder{value: 2 ether}(order, FillOptions({relayerFee: 0})); + + vm.expectRevert(IntentGateway.Filled.selector); + intentGateway.cancelOrder{value: 1 ether}(order, CancelOptions({relayerFee: 0, height: order.deadline + 1})); + + // Construct storage value for filled request + bytes memory context = abi.encode( + RequestBody({ + commitment: keccak256(abi.encode(order)), + tokens: order.inputs, + beneficiary: bytes32(uint256(uint160(address(this)))) + }) + ); + bytes memory hostId = host.host(); + StorageValue[] memory values = new StorageValue[](1); + values[0].value = bytes("0xdeadbeef"); + vm.startPrank(address(host)); + vm.expectRevert(IntentGateway.Filled.selector); + intentGateway.onGetResponse( + IncomingGetResponse({ + relayer: address(0), + response: GetResponse({ + values: values, + request: GetRequest({ + source: hostId, + dest: hostId, + nonce: 0, + from: address(intentGateway), + timeoutTimestamp: 0, + context: context, + keys: new bytes[](0), + height: uint64(block.number + 1) + }) + }) + }) + ); + + // Check the balances + assertEq(address(this).balance, 100 ether); + assertEq(filler.balance, 98 ether); + } + + function testRedeemEscrow() public { + Order memory order = createTestOrder(bytes("")); + + // Place the order + intentGateway.placeOrder{value: 1 ether}(order); + + assertEq(filler.balance, 100 ether); + assertEq(address(intentGateway).balance, 1 ether); + + // Fill the order + vm.prank(filler); + intentGateway.fillOrder{value: 2 ether}(order, FillOptions({relayerFee: 0})); + + // Check the balances + assertEq(address(this).balance, 100 ether); + assertEq(filler.balance, 98 ether); + + // Redeem the escrow + bytes memory data = abi.encode( + RequestBody({ + commitment: keccak256(abi.encode(order)), + tokens: order.inputs, + beneficiary: bytes32(uint256(uint160(filler))) + }) + ); + bytes memory hostId = host.host(); + PostRequest memory request = PostRequest({ + source: hostId, + dest: hostId, + nonce: 0, + from: abi.encodePacked(bytes32(uint256(uint160(address(0x1256))))), + to: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + body: bytes.concat(bytes1(uint8(IntentGateway.RequestKind.RedeemEscrow)), data), + timeoutTimestamp: 0 + }); + + vm.expectRevert(BaseIsmpModule.UnauthorizedCall.selector); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: request})); + + request.from = abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))); + vm.prank(address(host)); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: request})); + + // requests are invalidated as soon as they are executed + // but just checking that we delete the order from storage + vm.prank(address(host)); + vm.expectRevert(IntentGateway.UnknownOrder.selector); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: request})); + + // Check the balances + assertEq(address(intentGateway).balance, 0 ether); + assertEq(filler.balance, 99 ether); + } + + function testCancelOrder() public { + // Place the order + Order memory order = createTestOrder(bytes("")); + order.deadline = block.number - 100; + intentGateway.placeOrder{value: 1 ether}(order); + + assertEq(address(this).balance, 99 ether); + assertEq(address(intentGateway).balance, 1 ether); + + vm.prank(filler); + vm.expectRevert(IntentGateway.Unauthorized.selector); + intentGateway.cancelOrder{value: 1 ether}(order, CancelOptions({relayerFee: 0, height: block.number})); + + vm.expectRevert(IntentGateway.NotExpired.selector); + intentGateway.cancelOrder{value: 1 ether}(order, CancelOptions({relayerFee: 0, height: order.deadline - 1})); + + // Cancel the order + intentGateway.cancelOrder{value: 1 ether}(order, CancelOptions({relayerFee: 0, height: block.number})); + + // Respond with storage proof + bytes memory context = abi.encode( + RequestBody({ + commitment: keccak256(abi.encode(order)), + tokens: order.inputs, + beneficiary: bytes32(uint256(uint160(address(this)))) + }) + ); + bytes memory hostId = host.host(); + StorageValue[] memory values = new StorageValue[](1); + GetResponse memory response = GetResponse({ + values: values, + request: GetRequest({ + source: hostId, + dest: hostId, + nonce: 0, + from: address(intentGateway), + timeoutTimestamp: 0, + context: context, + keys: new bytes[](0), + height: uint64(block.number + 1) + }) + }); + vm.prank(address(host)); + intentGateway.onGetResponse(IncomingGetResponse({relayer: address(0), response: response})); + + // requests are invalidated as soon as they are executed + // but just checking that we delete the order from storage + vm.prank(address(host)); + vm.expectRevert(IntentGateway.UnknownOrder.selector); + intentGateway.onGetResponse(IncomingGetResponse({relayer: address(0), response: response})); + + // Check the balances + assertEq(address(this).balance, 99 ether); + assertEq(address(intentGateway).balance, 0 ether); + } + + function testCrossChainGovernance() public { + // Add new contract deployments + NewDeployment memory deployment = NewDeployment({ + stateMachineId: bytes("EVM-5"), + gateway: bytes32(uint256(uint160(address(0xdeadbeef)))) + }); + bytes memory data = abi.encode(deployment); + bytes memory hostId = host.host(); + PostRequest memory newDeploymentRequest = PostRequest({ + source: hostId, + dest: hostId, + nonce: 0, + from: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + to: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + body: bytes.concat(bytes1(uint8(IntentGateway.RequestKind.NewDeployment)), data), + timeoutTimestamp: 0 + }); + + // source should be hyperbridge + vm.prank(address(host)); + vm.expectRevert(IntentGateway.Unauthorized.selector); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: newDeploymentRequest})); + + // ensure that intent gateway rejects the settlement request + Order memory order = createTestOrder(bytes("")); + intentGateway.placeOrder{value: 1 ether}(order); + PostRequest memory redeemEscrowRequest = PostRequest({ + source: deployment.stateMachineId, + dest: hostId, + nonce: 0, + from: abi.encodePacked(deployment.gateway), + to: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + body: bytes.concat( + bytes1(uint8(IntentGateway.RequestKind.RedeemEscrow)), + abi.encode( + RequestBody({ + commitment: keccak256(abi.encode(order)), + tokens: order.inputs, + beneficiary: bytes32(uint256(uint160(filler))) + }) + ) + ), + timeoutTimestamp: 0 + }); + vm.prank(address(host)); + vm.expectRevert(IntentGateway.Unauthorized.selector); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: redeemEscrowRequest})); + + // Execute cross-chain governance request + newDeploymentRequest.source = host.hyperbridge(); + vm.prank(address(host)); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: newDeploymentRequest})); + + // Now it recognizes the deployment + vm.deal(address(intentGateway), 1 ether); + vm.prank(address(host)); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: redeemEscrowRequest})); + + // Check the settlement was successful + assertEq(address(intentGateway).balance, 0 ether); + assertEq(filler.balance, 101 ether); + + // Update parameters + Params memory params = intentGateway.params(); + params.dispatcher = address(0xdeadbeef); + PostRequest memory updateParamsRequest = PostRequest({ + source: host.hyperbridge(), + dest: host.host(), + nonce: 0, + from: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + to: abi.encodePacked(bytes32(uint256(uint160(address(intentGateway))))), + body: bytes.concat(bytes1(uint8(IntentGateway.RequestKind.UpdateParams)), abi.encode(params)), + timeoutTimestamp: 0 + }); + vm.prank(address(host)); + intentGateway.onAccept(IncomingPostRequest({relayer: address(0), request: updateParamsRequest})); + assertEq(intentGateway.params().dispatcher, params.dispatcher); + } +} diff --git a/evm/test/MainnetForkBaseTest.sol b/evm/test/MainnetForkBaseTest.sol index 370e1f2c..48e7a0b6 100644 --- a/evm/test/MainnetForkBaseTest.sol +++ b/evm/test/MainnetForkBaseTest.sol @@ -52,6 +52,7 @@ contract MainnetForkBaseTest is Test { IERC20 internal feeToken; IUniswapV2Router02 internal _uniswapV2Router; TokenRegistrar internal _registrar; + CallDispatcher internal dispatcher; uint256 internal mainnetFork; @@ -71,7 +72,7 @@ contract MainnetForkBaseTest is Test { consensusClient = new TestConsensusClient(); handler = new HandlerV1(); - CallDispatcher dispatcher = new CallDispatcher(); + dispatcher = new CallDispatcher(); uint256 paraId = 2000; HostManagerParams memory gParams = HostManagerParams({admin: address(this), host: address(0)}); diff --git a/evm/test/UniV3UniswapV2WrapperTest.sol b/evm/test/UniV3UniswapV2WrapperTest.sol new file mode 100644 index 00000000..99191cfe --- /dev/null +++ b/evm/test/UniV3UniswapV2WrapperTest.sol @@ -0,0 +1,86 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {MainnetForkBaseTest} from "./MainnetForkBaseTest.sol"; +import {UniV3UniswapV2Wrapper} from "../src/modules/UniV3UniswapV2Wrapper.sol"; + +contract UniV3UniswapV2WrapperTest is MainnetForkBaseTest { + address private constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address private constant UNISWAP_V3_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WHALE = address(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045); + + UniV3UniswapV2Wrapper private wrapper; + + function setUp() public override { + vm.selectFork(vm.createFork(vm.envString("MAINNET_FORK_URL"))); + + wrapper = new UniV3UniswapV2Wrapper(address(this)); + wrapper.init( + UniV3UniswapV2Wrapper.Params({WETH: WETH, swapRouter: UNISWAP_V3_ROUTER, quoter: UNISWAP_V3_QUOTER}) + ); + } + + function testSwapETHForExactTokens() public { + // Create path for the swap (ETH -> DAI) + address[] memory path = new address[](2); + path[0] = WETH; + path[1] = DAI; + + // Amount of DAI we want to receive + uint256 amountOut = 1_000 * 1e18; // 1000 DAI + uint256 amountsIn = wrapper.getAmountsIn(amountOut, path)[0]; // 1000 DAI + + // Get initial DAI balance + uint256 initialDaiBalance = IERC20(DAI).balanceOf(WHALE); + + // Get initial ETH balance + uint256 initialEthBalance = WHALE.balance; + + // Set deadline to 1 hour from now + uint256 deadline = block.timestamp; + + // Execute swap with more ETH than needed to ensure it succeeds + uint256 slippage = amountsIn * 50 / 10_000; // 0.5% slippage + vm.prank(WHALE); + uint256[] memory amounts = wrapper.swapETHForExactTokens{value: amountsIn + slippage}( + amountOut, + path, + WHALE, + deadline + ); + + // Verify the swap results + assertEq( + IERC20(DAI).balanceOf(WHALE), + initialDaiBalance + amountOut, + "DAI balance should increase by exact amount" + ); + assertTrue(amounts[0] > 0, "ETH spent should be greater than 0"); + assertEq(amounts[1], amountOut, "Amount out should match requested amount"); + assertTrue(WHALE.balance < initialEthBalance, "ETH balance should decrease"); + assertTrue(WHALE.balance == initialEthBalance - amounts[0], "Should receive refund for unused ETH"); + } + + // Required to receive ETH refunds + receive() external payable {} +} diff --git a/evm/yarn.lock b/evm/yarn.lock index 83b0ee27..ccd4088a 100644 --- a/evm/yarn.lock +++ b/evm/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@openzeppelin/contracts@3.4.2-solc-0.7": + version "3.4.2-solc-0.7" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz#38f4dbab672631034076ccdf2f3201fab1726635" + integrity sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA== + "@polytope-labs/erc6160@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@polytope-labs/erc6160/-/erc6160-0.3.1.tgz#c631c4b909a401ea1a7bce583969e8e6297565bc" @@ -11,10 +16,20 @@ prettier "^3.3.3" prettier-plugin-solidity "^1.3.1" +"@polytope-labs/ismp-solidity-v1@npm:@polytope-labs/ismp-solidity@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@polytope-labs/ismp-solidity/-/ismp-solidity-1.1.0.tgz#2319d642368077f28a20049b473e133f56925192" + integrity sha512-p9eNRyOr4zqQnuRtekZgAL3prRRSZ7siCHqAywBAi+H86rlE/wR6Ukn/Q9CvXdIpZmVFTZrdljV1LW0vzACRTw== + dependencies: + "@polytope-labs/solidity-merkle-trees" "^0.3.3" + openzeppelin-solidity "^4.8.1" + prettier "^3.3.3" + prettier-plugin-solidity "^1.3.1" + "@polytope-labs/ismp-solidity@^0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@polytope-labs/ismp-solidity/-/ismp-solidity-0.7.3.tgz#615ffed2c3e4126394ea68d874ab2899ddc7a742" - integrity sha512-hJvIOVhwmviFzch4D34yVIf4d5mkXBTsT+X4K+MbSX6de0mpd1UDMmUZLgkVguKkn1XWO+Y99vEjD4KvJEjH9Q== + version "0.7.4" + resolved "https://registry.yarnpkg.com/@polytope-labs/ismp-solidity/-/ismp-solidity-0.7.4.tgz#00a9e02f2eb0cdbb444fb3f244dc8199c5bfb685" + integrity sha512-NLthu+D+ycLwMkSgdURpzfotKs9NtFuI/TKcNm+8XH8FF74B26o8BOJMtvE2TD2TPOxu9rcs54u2p5HNaHoU2w== dependencies: "@polytope-labs/solidity-merkle-trees" "^0.3.3" openzeppelin-solidity "^4.8.1" @@ -30,67 +45,79 @@ prettier "^3.3.3" prettier-plugin-solidity "^1.4.1" -"@solidity-parser/parser@^0.17.0": - version "0.17.0" - resolved "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.17.0.tgz" - integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== - -"@solidity-parser/parser@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" - integrity sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA== +"@solidity-parser/parser@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.19.0.tgz#37a8983b2725af9b14ff8c4a475fa0e98d773c3f" + integrity sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA== "@uniswap/lib@1.1.1": version "1.1.1" - resolved "https://registry.npmjs.org/@uniswap/lib/-/lib-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-1.1.1.tgz#0afd29601846c16e5d082866cbb24a9e0758e6bc" integrity sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg== +"@uniswap/lib@^4.0.1-alpha": + version "4.0.1-alpha" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" + integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== + "@uniswap/v2-core@1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.0.tgz#e0fab91a7d53e8cafb5326ae4ca18351116b0844" integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA== +"@uniswap/v2-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + "@uniswap/v2-periphery@^1.1.0-beta.0": version "1.1.0-beta.0" - resolved "https://registry.npmjs.org/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz" + resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz#20a4ccfca22f1a45402303aedb5717b6918ebe6d" integrity sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g== dependencies: "@uniswap/lib" "1.1.1" "@uniswap/v2-core" "1.0.0" +"@uniswap/v3-core@^1.0.0", "@uniswap/v3-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" + integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== + +"@uniswap/v3-periphery@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz#d2756c23b69718173c5874f37fd4ad57d2f021b7" + integrity sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw== + dependencies: + "@openzeppelin/contracts" "3.4.2-solc-0.7" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v3-core" "^1.0.0" + base64-sol "1.0.1" + +base64-sol@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-sol/-/base64-sol-1.0.1.tgz#91317aa341f0bc763811783c5729f1c2574600f6" + integrity sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg== + openzeppelin-solidity@4.8.1, openzeppelin-solidity@^4.8.1: version "4.8.1" - resolved "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-4.8.1.tgz" + resolved "https://registry.yarnpkg.com/openzeppelin-solidity/-/openzeppelin-solidity-4.8.1.tgz#2ab492a5a53a5520401e94df36e43807de7a3b50" integrity sha512-KM0pVpfrCBdifqN2ZeJZFvFuoGz3GmI4Ty/ceKNkcaf7VVWo/rLOfc5EiLh+Ukb5NadNmYo8WMeGhFA8hVWDpg== -prettier-plugin-solidity@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz" - integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== +prettier-plugin-solidity@^1.3.1, prettier-plugin-solidity@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.2.tgz#d4f6173674e73a29731a8c79c45ab6f5246310df" + integrity sha512-VVD/4XlDjSzyPWWCPW8JEleFa8JNKFYac5kNlMjVXemQyQZKfpekPMhFZSePuXB6L+RixlFvWe20iacGjFYrLw== dependencies: - "@solidity-parser/parser" "^0.17.0" - semver "^7.5.4" - solidity-comments-extractor "^0.0.8" - -prettier-plugin-solidity@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.1.tgz#8060baf18853a9e34d2e09e47e87b4f19e15afe9" - integrity sha512-Mq8EtfacVZ/0+uDKTtHZGW3Aa7vEbX/BNx63hmVg6YTiTXSiuKP0amj0G6pGwjmLaOfymWh3QgXEZkjQbU8QRg== - dependencies: - "@solidity-parser/parser" "^0.18.0" - semver "^7.5.4" + "@solidity-parser/parser" "^0.19.0" + semver "^7.6.3" prettier@^3.3.3: - version "3.3.3" - resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== - -semver@^7.5.4: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -solidity-comments-extractor@^0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz" - integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== + version "3.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.1.tgz#22fac9d0b18c0b92055ac8fb619ac1c7bef02fb7" + integrity sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw== + +semver@^7.6.3: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== diff --git a/modules/pallets/token-governor/src/impls.rs b/modules/pallets/token-governor/src/impls.rs index f76d5b9c..6118ff6c 100644 --- a/modules/pallets/token-governor/src/impls.rs +++ b/modules/pallets/token-governor/src/impls.rs @@ -30,10 +30,11 @@ use sp_runtime::traits::AccountIdConversion; use crate::{ AssetMetadatas, AssetOwners, AssetRegistration, ChainWithSupply, Config, ContractInstance, ERC20AssetRegistration, ERC6160AssetRegistration, ERC6160AssetUpdate, Error, Event, - GatewayParams, Pallet, PendingAsset, RegistrarParamsUpdate, SolAssetMetadata, - SolChangeAssetAdmin, SolContractInstance, SolDeregsiterAsset, SolRegistrarParams, - SolTokenGatewayParams, SupportedChains, TokenGatewayParams, TokenGatewayParamsUpdate, - TokenGatewayRequest, TokenRegistrarParams, UnsignedERC6160AssetRegistration, PALLET_ID, + GatewayParams, GatewayParamsUpdate, IntentGatewayParams, NewIntentGatewayDeployment, Pallet, + PendingAsset, RegistrarParamsUpdate, SolAssetMetadata, SolChangeAssetAdmin, + SolContractInstance, SolDeregsiterAsset, SolGatewayParams, SolNewIntentGatewayDeployment, + SolRegistrarParams, SupportedChains, TokenGatewayParams, TokenGatewayRequest, + TokenRegistrarParams, UnsignedERC6160AssetRegistration, PALLET_ID, }; use token_gateway_primitives::AssetMetadata; @@ -251,9 +252,9 @@ where Ok(()) } - /// Dispatches a request to update the TokenRegistrar contract parameters - pub fn update_gateway_params_impl( - updates: BTreeMap, + /// Dispatches a request to update the TokenGateway contract parameters + pub fn update_token_gateway_params_impl( + updates: BTreeMap>, ) -> Result<(), Error> { for (state_machine, update) in updates { let stored_params = TokenGatewayParams::::get(&state_machine); @@ -265,7 +266,7 @@ where // if the params already exists then we dispatch a request to update it if let Some(old) = stored_params { let dispatcher = T::Dispatcher::default(); - let body: SolTokenGatewayParams = new_params.clone().into(); + let body: SolGatewayParams = new_params.clone().into(); dispatcher .dispatch_request( DispatchRequest::Post(DispatchPost { @@ -280,7 +281,7 @@ where .map_err(|_| Error::::DispatchFailed)?; } - Self::deposit_event(Event::::GatewayParamsUpdated { + Self::deposit_event(Event::::TokenGatewayParamsUpdated { old: old_params, new: new_params, state_machine, @@ -291,8 +292,8 @@ where } /// Introduce a new instance of the token gateway which has a different address - pub fn add_new_gateway_instance( - updates: BTreeMap, + pub fn new_token_gateway_instance_impl( + updates: BTreeMap>, ) -> Result<(), Error> { // first set them all for (state_machine, update) in updates.iter() { @@ -420,4 +421,97 @@ where AssetOwners::::insert(asset_id, who); Ok(()) } + + /// Register a new intent gateway instance. + pub fn new_intent_gateway_instance_impl( + updates: BTreeMap, + ) -> Result<(), Error> { + // first set them all + for (state_machine, update) in updates.iter() { + let new_params = GatewayParams::default().update::( + &state_machine, + GatewayParamsUpdate { + address: Some(update.module_id.clone()), + call_dispatcher: None, + }, + ); + IntentGatewayParams::::insert(state_machine.clone(), new_params); + } + + // now dispatch cross-chain governance actions + let dispatcher = T::Dispatcher::default(); + for (state_machine, new_deployment) in updates { + let body: SolNewIntentGatewayDeployment = new_deployment.into(); + + for (chain, GatewayParams { address, .. }) in IntentGatewayParams::::iter() { + if chain == state_machine || chain.is_substrate() { + continue; + } + dispatcher + .dispatch_request( + DispatchRequest::Post(DispatchPost { + dest: chain.clone(), + from: PALLET_ID.to_vec(), + to: address.as_bytes().to_vec(), + timeout: 0, + body: vec![vec![1], SolNewIntentGatewayDeployment::abi_encode(&body)] + .concat(), + }), + FeeMetadata { payer: [0u8; 32].into(), fee: Default::default() }, + ) + .map_err(|_| Error::::DispatchFailed)?; + } + } + + Ok(()) + } + + /// Updates the intent gateway parameters for the given state machine. + pub fn update_intent_gateway_params_impl( + params: BTreeMap>, + ) -> Result<(), Error> { + for (state_machine, update) in params { + let stored_params = IntentGatewayParams::::get(&state_machine); + let old_params = stored_params.clone().unwrap_or_default(); + + let new_params = old_params.update::(&state_machine, update); + + IntentGatewayParams::::insert(state_machine.clone(), new_params.clone()); + + if !state_machine.is_evm() { + continue; + } + + // if the params already exists then we dispatch a request to update it + if let Some(old) = stored_params { + let dispatcher = T::Dispatcher::default(); + let body: SolGatewayParams = GatewayParams { + host: new_params.host, + call_dispatcher: new_params.call_dispatcher, + ..Default::default() + } + .into(); + dispatcher + .dispatch_request( + DispatchRequest::Post(DispatchPost { + dest: state_machine.clone(), + from: PALLET_ID.to_vec(), + to: old.address.as_bytes().to_vec(), + timeout: 0, + body: vec![vec![2], SolGatewayParams::abi_encode(&body)].concat(), + }), + FeeMetadata { payer: [0u8; 32].into(), fee: Default::default() }, + ) + .map_err(|_| Error::::DispatchFailed)?; + } + + Self::deposit_event(Event::::IntentGatewayParamsUpdated { + old: old_params, + new: new_params, + state_machine, + }); + } + + Ok(()) + } } diff --git a/modules/pallets/token-governor/src/lib.rs b/modules/pallets/token-governor/src/lib.rs index 3e589e7e..460965b6 100644 --- a/modules/pallets/token-governor/src/lib.rs +++ b/modules/pallets/token-governor/src/lib.rs @@ -107,10 +107,15 @@ pub mod pallet { pub type TokenRegistrarParams = StorageMap<_, Twox64Concat, StateMachine, RegistrarParams, OptionQuery>; - /// TokenGateway protocol parameters. + /// TokenGateway protocol parameters per chain #[pallet::storage] pub type TokenGatewayParams = - StorageMap<_, Twox64Concat, StateMachine, GatewayParams, OptionQuery>; + StorageMap<_, Twox64Concat, StateMachine, GatewayParams, OptionQuery>; + + /// IntentGateway protocol parameters per chain + #[pallet::storage] + pub type IntentGatewayParams = + StorageMap<_, Twox64Concat, StateMachine, GatewayParams, OptionQuery>; /// Native asset ids for standalone chains connected to token gateway. #[pallet::storage] @@ -146,12 +151,21 @@ pub mod pallet { /// The state machine it was updated for state_machine: StateMachine, }, + /// The IntentGateway params have been updated for a state machine + IntentGatewayParamsUpdated { + /// The old params + old: GatewayParams, + /// The new params + new: GatewayParams, + /// The state machine it was updated for + state_machine: StateMachine, + }, /// The TokenGateway params have been updated for a state machine - GatewayParamsUpdated { + TokenGatewayParamsUpdated { /// The old params - old: GatewayParams, + old: GatewayParams, /// The new params - new: GatewayParams, + new: GatewayParams, /// The state machine it was updated for state_machine: StateMachine, }, @@ -253,16 +267,17 @@ pub mod pallet { Ok(()) } - /// Dispatches a request to update the TokenRegistrar contract parameters + /// Records the update of the TokenGateway contract parameters and + /// dispatches a request to update the TokenGateway contract parameters #[pallet::call_index(3)] #[pallet::weight(weight())] - pub fn update_gateway_params( + pub fn update_token_gateway_params( origin: OriginFor, - update: BTreeMap, + update: BTreeMap>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - Self::update_gateway_params_impl(update)?; + Self::update_token_gateway_params_impl(update)?; Ok(()) } @@ -322,13 +337,13 @@ pub mod pallet { /// Adds a new token gateway contract instance to all existing instances #[pallet::call_index(7)] #[pallet::weight(weight())] - pub fn new_contract_instance( + pub fn new_token_gateway_instance( origin: OriginFor, - updates: BTreeMap, + updates: BTreeMap>, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - Self::add_new_gateway_instance(updates)?; + Self::new_token_gateway_instance_impl(updates)?; Ok(()) } @@ -372,6 +387,35 @@ pub mod pallet { Ok(()) } + + /// Records the update of the IntentGateway contract parameters and + /// dispatches a request to update the IntentGateway contract parameters + #[pallet::call_index(10)] + #[pallet::weight(weight())] + pub fn update_intent_gateway_params( + origin: OriginFor, + params: BTreeMap>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::update_intent_gateway_params_impl(params)?; + + Ok(()) + } + + /// Adds a new intent gateway contract instance to all existing instances + #[pallet::call_index(11)] + #[pallet::weight(weight())] + pub fn new_intent_gateway_instance( + origin: OriginFor, + params: BTreeMap, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Self::new_intent_gateway_instance_impl(params)?; + + Ok(()) + } } /// This allows users to create assets from any chain using the TokenRegistrar. diff --git a/modules/pallets/token-governor/src/types.rs b/modules/pallets/token-governor/src/types.rs index b4bc2044..24bdea5a 100644 --- a/modules/pallets/token-governor/src/types.rs +++ b/modules/pallets/token-governor/src/types.rs @@ -174,22 +174,22 @@ pub struct RegistrarParamsUpdate { /// Protocol Parameters for the TokenGateway contract #[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, Default)] -pub struct GatewayParams { +pub struct GatewayParams { /// The Ismp host address pub host: H160, /// Contract for dispatching calls in `AssetWithCall` pub call_dispatcher: H160, /// Token gateway address - pub address: H160, + pub address: T, } /// Struct for updating the protocol parameters for a TokenGateway #[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, Default)] -pub struct TokenGatewayParamsUpdate { +pub struct GatewayParamsUpdate { /// Contract for dispatching calls in `AssetWithCall` pub call_dispatcher: Option, /// Token gateway address - pub address: Option, + pub address: Option, } /// Describes the token gateway module on a given chain @@ -197,10 +197,19 @@ pub struct TokenGatewayParamsUpdate { pub struct ContractInstance { /// The associated chain pub chain: StateMachine, - // The token gateway params on this chain + // The token gateway module id on this chain pub module_id: H160, } +/// Describes the token gateway module on a given chain +#[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)] +pub struct NewIntentGatewayDeployment { + /// The associated chain + pub chain: StateMachine, + // The intent gateway module id on this chain + pub module_id: H256, +} + impl Params { pub fn update(&self, update: ParamsUpdate) -> Params { let mut params = self.clone(); @@ -238,13 +247,13 @@ impl RegistrarParams { } } -impl GatewayParams { +impl GatewayParams { /// Convenience method for updating protocol params pub fn update( &self, state_machine: &StateMachine, - update: TokenGatewayParamsUpdate, - ) -> GatewayParams { + update: GatewayParamsUpdate, + ) -> GatewayParams { let mut params = self.clone(); if let Some(host) = EvmHosts::::get(state_machine) { @@ -266,13 +275,21 @@ impl GatewayParams { alloy_sol_macro::sol! { #![sol(all_derives)] - struct SolTokenGatewayParams { + // Params for both the IntentGateway and TokenGateway + struct SolGatewayParams { // address of the IsmpHost contract on this chain address host; // dispatcher for delegating external calls address dispatcher; } + struct SolNewIntentGatewayDeployment { + // Identifier for the state machine. + bytes stateMachineId; + // A bytes32 variable to store the gateway identifier. + bytes32 gateway; + } + struct SolAssetMetadata { // ERC20 token contract address for the asset address erc20; @@ -322,15 +339,21 @@ alloy_sol_macro::sol! { } } -impl From for SolTokenGatewayParams { - fn from(value: GatewayParams) -> Self { - SolTokenGatewayParams { - host: value.host.0.into(), - dispatcher: value.call_dispatcher.0.into(), +impl From for SolNewIntentGatewayDeployment { + fn from(value: NewIntentGatewayDeployment) -> Self { + SolNewIntentGatewayDeployment { + stateMachineId: value.chain.to_string().as_bytes().to_vec().into(), + gateway: value.module_id.0.into(), } } } +impl From> for SolGatewayParams { + fn from(value: GatewayParams) -> Self { + SolGatewayParams { host: value.host.0.into(), dispatcher: value.call_dispatcher.0.into() } + } +} + impl From for SolRegistrarParams { fn from(value: RegistrarParams) -> Self { SolRegistrarParams { @@ -371,13 +394,13 @@ pub trait TokenGatewayRequest { fn encode_request(&self) -> Vec; } -impl TokenGatewayRequest for SolTokenGatewayParams { +impl TokenGatewayRequest for SolGatewayParams { /// Encodes the SetAsste alongside the enum variant for the TokenGateway request fn encode_request(&self) -> Vec { use alloy_sol_types::SolType; let variant = vec![1u8]; // enum variant on token gateway - let encoded = SolTokenGatewayParams::abi_encode(self); + let encoded = SolGatewayParams::abi_encode(self); [variant, encoded].concat() }