diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol new file mode 100644 index 0000000000..8c2585fd32 --- /dev/null +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; +import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; +import {Message} from "../../libs/Message.sol"; +import {IMailbox} from "../../interfaces/IMailbox.sol"; + +// ============ External Imports ============ +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {AddressToString} from "../../libs/AddressString.sol"; + +interface IAxelarGateway { + function callContract( + string calldata destinationChain, + string calldata destinationContractAddress, + bytes calldata payload + ) external; +} + +interface IAxelarGasService { + function payNativeGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address refundAddress + ) external payable; +} +interface IAxelarHookGasService { + function getGas() external view returns (uint256); +} + +contract AxelarHookGasService is Ownable { + uint256 public gas; + function setGas(uint256 _newGas) external onlyOwner { + gas = _newGas; + } + function getGas() external view returns (uint256) { + return gas; + } +} + +contract AxelarHook is IPostDispatchHook, Ownable { + using StandardHookMetadata for bytes; + using Message for bytes; + using AddressToString for address; + + IMailbox public immutable MAILBOX; + IAxelarGasService public immutable AXELAR_GAS_SERVICE; + IAxelarGateway public immutable AXELAR_GATEWAY; + IAxelarHookGasService public immutable AXELAR_HOOK_GAS; + string public DESTINATION_CHAIN; + string public DESTINATION_CONTRACT; + + constructor( + address _mailbox, + address axelarGateway, + address axelarGasReceiver, + address axelarHookGasService + ) { + MAILBOX = IMailbox(_mailbox); + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); + AXELAR_HOOK_GAS = IAxelarHookGasService(axelarHookGasService); + } + + /** + * @notice Initializes the hook with specific targets + */ + function initializeReceiver( + string memory destinationChain, + string memory destionationContract + ) external onlyOwner { + // require( + // bytes(DESTINATION_CHAIN).length == 0 && + // bytes(DESTINATION_CONTRACT).length == 0, + // "Already initialized" + // ); + DESTINATION_CHAIN = destinationChain; + DESTINATION_CONTRACT = destionationContract; + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function hookType() external pure returns (uint8) { + return uint8(IPostDispatchHook.Types.AXELAR); + } + + /** + * @notice Returns whether the hook supports metadata + * @return true the hook supports metadata + */ + function supportsMetadata(bytes calldata) external pure returns (bool) { + return true; + } + + function postDispatch( + bytes calldata metadata, + bytes calldata message + ) external payable { + // ensure hook only dispatches messages that are dispatched by the mailbox + bytes32 id = message.id(); + require(_isLatestDispatched(id), "message not dispatched by mailbox"); + + bytes memory axelarPayload = _encodeGmpPayload(id); + + // Pay for gas used by Axelar with ETH + AXELAR_GAS_SERVICE.payNativeGasForContractCall{value: msg.value}( + address(this), + DESTINATION_CHAIN, + DESTINATION_CONTRACT, + axelarPayload, + metadata.refundAddress(address(0)) + ); + + // bridging call + AXELAR_GATEWAY.callContract( + DESTINATION_CHAIN, + DESTINATION_CONTRACT, + axelarPayload + ); + } + + /** + * @notice Quote for the amount of value required to run this hook. + */ + function quoteDispatch( + bytes calldata, + bytes calldata + ) external view returns (uint256) { + return AXELAR_HOOK_GAS.getGas(); + } + + /** + * @notice Helper function to encode the Axelar GMP payload + * @param _id The latest id of the current dispatched hyperlane message + * @return bytes The Axelar GMP payload. + */ + function _encodeGmpPayload(bytes32 _id) internal returns (bytes memory) { + // dociding version used by Axelar + bytes4 version = bytes4(uint32(1)); + + //name of the arguments used in the cross-chain function call + string[] memory argumentNameArray = new string[](3); + argumentNameArray[0] = "origin_address"; + argumentNameArray[1] = "origin_chain"; + argumentNameArray[2] = "id"; + // type of argument used in the cross-chain function call + string[] memory abiTypeArray = new string[](3); + abiTypeArray[0] = "string"; + abiTypeArray[1] = "string"; + abiTypeArray[2] = "bytes32"; + + // message argument + bytes memory argValue = abi.encode( + address(this).toString(), + "ethereum-sepolia", + _id + ); + + // add the function name: (submit_meta) and argument value (_id) + bytes memory gmpPayload = abi.encode( + "submit_meta", + argumentNameArray, + abiTypeArray, + argValue + ); + + // encode the version and return the payload + return + abi.encodePacked( + version, // version number + gmpPayload + ); + } + + /** + * @notice Helper function to check wether an ID is the latest dispatched by Mailbox + * @param _id The id to check. + * @return true if latest, false otherwise. + */ + function _isLatestDispatched(bytes32 _id) internal view returns (bool) { + return MAILBOX.latestDispatchedId() == _id; + } +} diff --git a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol new file mode 100644 index 0000000000..8863350bd7 --- /dev/null +++ b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/** + * Format of metadata: + * + * [0:32] variant + * [32:] additional metadata + */ +library BridgeAggregationHookMetadata { + struct Metadata { + uint256 AxelarPayment; + } + + uint8 private constant AXELAR_PAYMENT_OFFSET = 0; + uint8 private constant MIN_METADATA_LENGTH = 32; + + /** + * @notice Returns the required payment for Axelar bridging. + * @param _metadata ABI encoded standard hook metadata. + * @return uint256 Payment amount. + */ + function axelarGasPayment( + bytes calldata _metadata + ) internal pure returns (uint256) { + if (_metadata.length < AXELAR_PAYMENT_OFFSET + 32) return 0; + return + uint256( + bytes32( + _metadata[AXELAR_PAYMENT_OFFSET:AXELAR_PAYMENT_OFFSET + 32] + ) + ); + } + + /** + * @notice Returs any additional metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return bytes Additional metadata. + */ + function getBridgeAggregationCustomMetadata( + bytes calldata _metadata + ) internal pure returns (bytes calldata) { + if (_metadata.length < MIN_METADATA_LENGTH) return _metadata[0:0]; + return _metadata[MIN_METADATA_LENGTH:]; + } + + /** + * @notice Formats the specified Axelar and Wormhole payments. + * @param _axelarPayment msg.value for the message. + * @param _customMetadata Additional metadata to include. + * @return ABI encoded standard hook metadata. + */ + function formatMetadata( + uint256 _axelarPayment, + bytes memory _customMetadata + ) internal pure returns (bytes memory) { + return abi.encodePacked(_axelarPayment, _customMetadata); + } +} diff --git a/solidity/contracts/hooks/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol new file mode 100644 index 0000000000..5493cb0aa2 --- /dev/null +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; +import {Message} from "../../libs/Message.sol"; +import {IMailbox} from "../../interfaces/IMailbox.sol"; + +// TODO: figure out whether it is possible to import this using Hardhat: +// https://github.com/wormhole-foundation/wormhole/blob/main/ethereum/contracts/interfaces/IWormhole.sol +interface IWormhole { + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable returns (uint64 sequence); +} + +contract WormholeHook is IPostDispatchHook { + using Message for bytes; + + IMailbox public immutable MAILBOX; + IWormhole public WORMHOLE; + + constructor(address _wormhole, address _mailbox) { + WORMHOLE = IWormhole(_wormhole); + MAILBOX = IMailbox(_mailbox); + } + + function hookType() external pure returns (uint8) { + return uint8(IPostDispatchHook.Types.WORMHOLE); + } + + function supportsMetadata(bytes calldata) external pure returns (bool) { + return true; + } + + function postDispatch( + bytes calldata, + bytes calldata message + ) external payable { + // ensure hook only dispatches messages that are dispatched by the mailbox + bytes32 id = message.id(); + require( + _isLatestDispatched(id), + "message not dispatched by Hyperlane mailbox" + ); + // use 0 nonce, _isLatestDispatched is sufficient check. + // 201 consistency level iis safest as it ensures finality is reached before bridging. + WORMHOLE.publishMessage{value: msg.value}(0, abi.encodePacked(id), 202); + } + + function quoteDispatch( + bytes calldata, + bytes calldata + ) external pure returns (uint256) { + return 0; + } + + /** + * @notice Helper function to check wether an ID is the latest dispatched by Mailbox + * @param _id The id to check. + * @return true if latest, false otherwise. + */ + function _isLatestDispatched(bytes32 _id) internal view returns (bool) { + return MAILBOX.latestDispatchedId() == _id; + } +} diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 69a44c7bc1..05162b3d10 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -23,7 +23,9 @@ interface IPostDispatchHook { FALLBACK_ROUTING, ID_AUTH_ISM, PAUSABLE, - PROTOCOL_FEE + PROTOCOL_FEE, + WORMHOLE, + AXELAR } /** diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol new file mode 100644 index 0000000000..12a6d51811 --- /dev/null +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {Message} from "../../libs/Message.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +interface IAxelarGateway { + function validateContractCall( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash + ) external returns (bool); +} + +contract AxelarIsm is IInterchainSecurityModule, Ownable { + using Message for bytes; + + IAxelarGateway public AXELAR_GATEWAY; + string public SOURCE_CHAIN; + string public SOURCE_ADDRESS; + + mapping(bytes32 => bool) public validated; + + /** + * @notice Initializes the hook with specific targets + */ + function initializeSource( + string memory sourceChain, + string memory sourceAddress, + address axelarGateway + ) external onlyOwner { + // require( + // bytes(SOURCE_CHAIN).length == 0 && + // bytes(SOURCE_ADDRESS).length == 0, + // "Already initialized" + // ); + SOURCE_CHAIN = sourceChain; + SOURCE_ADDRESS = sourceAddress; + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function moduleType() external pure returns (uint8) { + return uint8(IInterchainSecurityModule.Types.NULL); + } + + /** + * @notice Verifies that an encoded VM is validand marks the internal + * payload as processed. the payload should be the hyperlane message ID. + * @param commandId Axelar Specific unique command ID + * @param sourceChain Source chain where the call was iniitated from + * @param sourceAddress Source address that initiated the call + * @param payload the gmp payload. + */ + function execute( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + bytes32 payloadHash = keccak256(payload); + if ( + !AXELAR_GATEWAY.validateContractCall( + commandId, + sourceChain, + sourceAddress, + payloadHash + ) + ) revert("Not approved by Axelar Gateway"); + + // only accept calls from specific sournce chain and address. + require( + _compareStrings(sourceChain, SOURCE_CHAIN), + "Unexpected Axelar source chain" + ); + require( + _compareStrings(sourceAddress, SOURCE_ADDRESS), + "Unexpected Axelar source address" + ); + + //get hyperlane ID, decode into bytes32 from byte array + bytes32 hyperlaneId = abi.decode(payload, (bytes32)); + validated[hyperlaneId] = true; + } + + /** + * @notice verifies interchain messages processed by Axelar. + * @param _message Hyperlane encoded interchain message + * @return true if the message was verified. false otherwise + */ + function verify( + bytes calldata, + bytes calldata _message + ) external view returns (bool) { + return validated[_message.id()]; + } + + // ============ Helper Functions ============ + + /** + * @notice checks 2 strings for equality. + * @param str1 First string. + * @param str2 Second string. + * @return true if the strings are equal. False otherwise. + */ + function _compareStrings( + string calldata str1, + string memory str2 + ) internal pure returns (bool) { + return + keccak256(abi.encodePacked(str1)) == + keccak256(abi.encodePacked(str2)); + } +} diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol new file mode 100644 index 0000000000..120e9eeb49 --- /dev/null +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {Message} from "../../libs/Message.sol"; + +interface IWormhole { + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + uint32 guardianSetIndex; + Signature[] signatures; + bytes32 hash; + } + + function parseAndVerifyVM( + bytes calldata encodedVM + ) external view returns (VM memory vm, bool valid, string memory reason); +} + +contract WormholeIsm is IInterchainSecurityModule, Ownable { + using Message for bytes; + + IWormhole public WORMHOLE; + uint16 public SOURCE_CHAIN_ID; + bytes32 public SOURCE_ADDRESS; + + mapping(bytes32 => bool) public validated; + + /** + * @notice Initializes the hook with specific targets + */ + function initializeSource( + uint16 sourceChainId, + bytes32 sourceAddress, + address _wormhole + ) external onlyOwner { + // require( + // bytes(SOURCE_CHAIN_ID).length == 0 && + // bytes(SOURCE_ADDRESS) == bytes32(0), + // "Already initialized" + // ); + SOURCE_CHAIN_ID = sourceChainId; + SOURCE_ADDRESS = sourceAddress; + WORMHOLE = IWormhole(_wormhole); + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function moduleType() external pure returns (uint8) { + return uint8(IInterchainSecurityModule.Types.NULL); + } + + /** + * @notice Verifies that an encoded VM is validand marks the internal + * payload as processed. the payload should be the hyperlane message ID. + * @param encodedVM the wormhole encoded VMM + */ + function execute(bytes calldata encodedVM) external { + // parse and verify the Wormhole core message + ( + IWormhole.VM memory verifiedMessage, + bool valid, + string memory reason + ) = WORMHOLE.parseAndVerifyVM(encodedVM); + //revert if the message cannot be varified + require(valid, reason); + // only accept calls from specific source chain and addresses + require( + verifiedMessage.emitterChainId == SOURCE_CHAIN_ID, + "unexpectd source chain" + ); + require( + verifiedMessage.emitterAddress == SOURCE_ADDRESS, + "unexpectd source address" + ); + + //TODO get hyperlane ID. verify wormhole gmp input. + bytes32 hyperlaneid = bytes32(verifiedMessage.payload); + + validated[hyperlaneid] = true; + } + + /** + * @notice verifies interchain messages processed by Wormhole. + * @param _message Hyperlane encoded interchain message + * @return true if the message was verified. false otherwise + */ + function verify( + bytes calldata, + bytes calldata _message + ) external view returns (bool) { + return validated[_message.id()]; + } +} diff --git a/solidity/contracts/libs/AddressString.sol b/solidity/contracts/libs/AddressString.sol new file mode 100644 index 0000000000..3260477806 --- /dev/null +++ b/solidity/contracts/libs/AddressString.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library StringToAddress { + error InvalidAddressString(); + + function toAddress( + string memory addressString + ) internal pure returns (address) { + bytes memory stringBytes = bytes(addressString); + uint160 addressNumber = 0; + uint8 stringByte; + + if ( + stringBytes.length != 42 || + stringBytes[0] != "0" || + stringBytes[1] != "x" + ) revert InvalidAddressString(); + + for (uint256 i = 2; i < 42; ++i) { + stringByte = uint8(stringBytes[i]); + + if ((stringByte >= 97) && (stringByte <= 102)) stringByte -= 87; + else if ((stringByte >= 65) && (stringByte <= 70)) stringByte -= 55; + else if ((stringByte >= 48) && (stringByte <= 57)) stringByte -= 48; + else revert InvalidAddressString(); + + addressNumber |= uint160(uint256(stringByte) << ((41 - i) << 2)); + } + + return address(addressNumber); + } +} + +library AddressToString { + function toString(address address_) internal pure returns (string memory) { + bytes memory addressBytes = abi.encodePacked(address_); + bytes memory characters = "0123456789abcdef"; + bytes memory stringBytes = new bytes(42); + + stringBytes[0] = "0"; + stringBytes[1] = "x"; + + for (uint256 i; i < 20; ++i) { + stringBytes[2 + i * 2] = characters[uint8(addressBytes[i] >> 4)]; + stringBytes[3 + i * 2] = characters[uint8(addressBytes[i] & 0x0f)]; + } + + return string(stringBytes); + } +} diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol new file mode 100644 index 0000000000..475a453826 --- /dev/null +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {BridgeAggregationHookMetadata} from "../../contracts/hooks/libs/BridgeAggregationHookMetadata.sol"; +import {MessageUtils} from "../isms/IsmTestUtils.sol"; +import {AxelarHook} from "../../contracts/hooks/Axelar/AxelarHook.sol"; +import {AxelarHookGasService} from "../../contracts/hooks/Axelar/AxelarHook.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; + +contract AxelarHookTest is Test { + using StandardHookMetadata for bytes; + using BridgeAggregationHookMetadata for bytes; + using TypeCasts for address; + + AxelarHook hook; + AxelarHookGasService gas_service; + TestMailbox mailbox; + + address internal alice = address(0x1); // alice the user + address internal bob = address(0x2); // bob the beneficiary + address internal charlie = address(0x3); // charlie the crock + bytes internal testMessage; + + uint32 internal constant TEST_ORIGIN_DOMAIN = 1; + uint32 internal constant TEST_DESTINATION_DOMAIN = 2; + + string destinationChain = "Neutron"; + string destionationContract = "neutronContract"; + address axelarGateway = address(0); + address axelarGasReceiver = address(0); + error BadQuote(uint256 balance, uint256 required); + + function setUp() public { + mailbox = new TestMailbox(1); + gas_service = new AxelarHookGasService(); + hook = new AxelarHook( + address(mailbox), + axelarGateway, + axelarGasReceiver, + address(gas_service) + ); + hook.initializeReceiver(destinationChain, destionationContract); + testMessage = _encodeTestMessage(); + } + + function test_initialized() public { + string memory destChain = hook.DESTINATION_CHAIN(); + string memory destContract = hook.DESTINATION_CONTRACT(); + assertEq(destChain, destinationChain); + assertEq(destContract, destionationContract); + } + + function test_iinitializeReceiver_revertsWhenCalledAgain() public { + vm.expectRevert("Already initialized"); + + hook.initializeReceiver(destinationChain, destionationContract); + } + + // function test_quoteDispatch_revertsWithNoMetadata() public { + // vm.expectRevert("No Axelar Payment Received"); + + // bytes memory emptyCustomMetadata; + // bytes memory testMetadata = StandardHookMetadata.formatMetadata( + // 100, + // 100, + // msg.sender, + // emptyCustomMetadata + // ); + + // hook.quoteDispatch(testMetadata, testMessage); + // } + + // function test_quoteDispatch_revertsWithZeroQuote() public { + // vm.expectRevert("No Axelar Payment Received"); + // uint256 expectedQuote = 0; + // bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + // .formatMetadata(expectedQuote, abi.encodePacked()); + // bytes memory testMetadata = StandardHookMetadata.formatMetadata( + // 100, + // 100, + // msg.sender, + // justRightCustomMetadata + // ); + + // hook.quoteDispatch(testMetadata, testMessage); + // } + + // function test_quoteDispatch_ReturnsSmallQuote() public { + // uint256 expectedQuote = 1; + // bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + // .formatMetadata(expectedQuote, abi.encodePacked()); + // bytes memory testMetadata = StandardHookMetadata.formatMetadata( + // 100, + // 100, + // msg.sender, + // justRightCustomMetadata + // ); + + // uint256 quote = hook.quoteDispatch(testMetadata, testMessage); + // assertEq(quote, expectedQuote); + // } + + // function test_quoteDispatch_ReturnsLargeQuote() public { + // // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935. that's a big quote + // uint256 expectedQuote = type(uint256).max; + // bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + // .formatMetadata(expectedQuote, abi.encodePacked()); + // bytes memory testMetadata = StandardHookMetadata.formatMetadata( + // 100, + // 100, + // msg.sender, + // justRightCustomMetadata + // ); + + // uint256 quote = hook.quoteDispatch(testMetadata, testMessage); + // assertEq(quote, expectedQuote); + // } + function test_setGas_RevertsWhenNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(bob); + gas_service.setGas(100); + } + + function test_setGas_OwnerCanSetGas() public { + AxelarHookGasService temp_gas_service = new AxelarHookGasService(); + uint256 expectedQuote = type(uint256).max; + + temp_gas_service.setGas(expectedQuote); + uint256 quote = temp_gas_service.getGas(); + + assertEq(quote, expectedQuote); + } + + function test_quoteDispatch_ReturnsZeroWhenUnset() public { + uint256 expectedQuote = 0; + uint256 quote = hook.quoteDispatch("0x00", "0x00"); + + assertEq(quote, expectedQuote); + } + + function test_quoteDispatch_ReturnsGasServiceQuote() public { + // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935 + uint256 expectedQuote = type(uint256).max; + gas_service.setGas(expectedQuote); + + uint256 quote = hook.quoteDispatch("0x00", "0x00"); + assertEq(quote, expectedQuote); + } + // ============ Helper Functions ============ + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + uint8(0), + uint32(1), + TEST_ORIGIN_DOMAIN, + alice.addressToBytes32(), + TEST_DESTINATION_DOMAIN, + alice.addressToBytes32(), + abi.encodePacked("Hello World") + ); + } + + receive() external payable {} // to use when tests expand +}