From 7efcc8673982c7b4205b333c51bd3bee0c38514d Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sun, 10 Dec 2023 01:08:35 -0600 Subject: [PATCH 01/26] add Wormhole Hook from https://github.com/foxpy/hyperlane-monorepo/tree/feat/wormhole-hook --- .../contracts/hooks/wormhole/WormholeHook.sol | 45 +++++++++++++++++++ .../interfaces/hooks/IPostDispatchHook.sol | 3 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 solidity/contracts/hooks/wormhole/WormholeHook.sol diff --git a/solidity/contracts/hooks/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol new file mode 100644 index 0000000000..d1c0322409 --- /dev/null +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.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 { + IWormhole public wormhole; + + constructor(address _wormhole) { + wormhole = IWormhole(_wormhole); + } + + function hookType() external pure returns (uint8) { + return uint8(IPostDispatchHook.Types.WORMHOLE); + } + + function supportsMetadata(bytes calldata) external pure returns (bool) { + return false; + } + + function postDispatch( + bytes calldata, + bytes calldata message + ) external payable { + wormhole.publishMessage{value: msg.value}(0, message, 200); + } + + function quoteDispatch( + bytes calldata, + bytes calldata + ) external pure returns (uint256) { + return 0; + } +} diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 69a44c7bc1..304fe2dc22 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -23,7 +23,8 @@ interface IPostDispatchHook { FALLBACK_ROUTING, ID_AUTH_ISM, PAUSABLE, - PROTOCOL_FEE + PROTOCOL_FEE, + WORMHOLE } /** From 61508a1a915bbeabe582cfb3f7b202883e654e33 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Wed, 13 Dec 2023 02:07:37 -0600 Subject: [PATCH 02/26] Add Axelar post-dispatch hook --- .../contracts/hooks/axelar/AxelarHook.sol | 128 ++++++++++++++++++ .../interfaces/hooks/IPostDispatchHook.sol | 3 +- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 solidity/contracts/hooks/axelar/AxelarHook.sol diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol new file mode 100644 index 0000000000..31a75424c6 --- /dev/null +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -0,0 +1,128 @@ +// 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"; + +//TODO: remove temp intermal import. for testing speed purposes only +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; +} + +contract AxelarHook is IPostDispatchHook { + error MetadataSize(uint256 expected, uint256 actual); + + using StandardHookMetadata for bytes; + using Message for bytes; + + IAxelarGasService public immutable AXELAR_GAS_SERVICE; + IAxelarGateway public immutable AXELAR_GATEWAY; + string public DESTINATION_CHAIN; + string public DESTINATION_CONTRACT; + bytes GMP_CALL_CODE; + + constructor( + string memory destinationChain, + string memory destionationContract, + address axelarGateway, + address axelarGasReceiver, + bytes memory gmp_call_code + ) { + DESTINATION_CHAIN = destinationChain; + DESTINATION_CONTRACT = destionationContract; + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); + GMP_CALL_CODE = gmp_call_code; + } + + /** + * @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 { + bytes memory axelarPayload = _formatPayload(message); + // 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 Post action after a message is dispatched via the Mailbox + * @param metadata The metadata required for the hook. Metadata should contain custom metadata + * for the gas amount owed to the Axelar Gas Service. + */ + function quoteDispatch( + bytes calldata metadata, + bytes calldata + ) external pure returns (uint256) { + bytes calldata customMetadata = metadata.getCustomMetadata(); + // Ensure that the custom metadata is of the correct size + + require(customMetadata.length <= 32, "Custom metadata is too large"); + require( + customMetadata.length > 0, + "Empty custom metadata. Axelar needs payment." + ); + + uint256 quote; + assembly { + // Copy the custom metadata to memory + // The '0x20' adds an offset for the length field in memory + calldatacopy(0x20, customMetadata.offset, customMetadata.length) + // Load the data from memory into 'number' + quote := mload(0x20) + } + + require(quote > 0, "Custom Metadata can't be zero value"); + + return quote; + } + + function _formatPayload( + bytes calldata message + ) internal returns (bytes memory) { + return abi.encodePacked(GMP_CALL_CODE, message.id()); + } +} diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 304fe2dc22..05162b3d10 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -24,7 +24,8 @@ interface IPostDispatchHook { ID_AUTH_ISM, PAUSABLE, PROTOCOL_FEE, - WORMHOLE + WORMHOLE, + AXELAR } /** From 2b8a07fb559d730537b9df1b865f0892f78cb47a Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Wed, 13 Dec 2023 02:08:05 -0600 Subject: [PATCH 03/26] test Axelar Hook quoteDispatch --- solidity/test/hooks/AxelarHook.t.sol | 139 +++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 solidity/test/hooks/AxelarHook.t.sol diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol new file mode 100644 index 0000000000..386240e0ee --- /dev/null +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -0,0 +1,139 @@ +// 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 {MessageUtils} from "../isms/IsmTestUtils.sol"; +import {AxelarHook} from "../../contracts/hooks/Axelar/AxelarHook.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; + +contract AxelarHookTest is Test { + using StandardHookMetadata for bytes; + using TypeCasts for address; + AxelarHook hook; + + 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); + bytes gmp_call_code = abi.encodePacked(uint8(1)); + error BadQuote(uint256 balance, uint256 required); + + function setUp() public { + hook = new AxelarHook( + destinationChain, + destionationContract, + axelarGateway, + axelarGasReceiver, + gmp_call_code + ); + testMessage = _encodeTestMessage(); + } + + function test_quoteDispatch_revertsWithNoMetadata() public { + vm.expectRevert("Empty custom metadata. Axelar needs payment."); + + bytes memory emptyCustomMetadata; + bytes memory testMetadata = StandardHookMetadata.formatMetadata( + 100, + 100, + msg.sender, + emptyCustomMetadata + ); + + hook.quoteDispatch(testMetadata, testMessage); + } + + function test_quoteDispatch_revertsWithTooLargeMetadata() public { + vm.expectRevert("Custom metadata is too large"); + // tooLargeCustomMetadata becomes 33 bytes long after packing + bytes memory tooLargeCustomMetadata = abi.encodePacked( + bytes32(uint256(100)), + bytes1(uint8(1)) + ); + bytes memory testMetadata = StandardHookMetadata.formatMetadata( + 100, + 100, + msg.sender, + tooLargeCustomMetadata + ); + + hook.quoteDispatch(testMetadata, testMessage); + } + + function test_quoteDispatch_revertsWithZeroQuote() public { + vm.expectRevert("Custom Metadata can't be zero value"); + uint256 expectedQuote = 0; + bytes memory justRightCustomMetadata = abi.encodePacked( + bytes32(expectedQuote) + ); + 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 = abi.encodePacked( + bytes32(expectedQuote) + ); + 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 = abi.encodePacked( + bytes32(expectedQuote) + ); + bytes memory testMetadata = StandardHookMetadata.formatMetadata( + 100, + 100, + msg.sender, + justRightCustomMetadata + ); + + uint256 quote = hook.quoteDispatch(testMetadata, testMessage); + 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 +} From 31d2dd657d079310af4469efeaaeaa576049e587 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Wed, 13 Dec 2023 02:10:25 -0600 Subject: [PATCH 04/26] remove unused custom error --- solidity/contracts/hooks/axelar/AxelarHook.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 31a75424c6..b18a43b788 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -26,8 +26,6 @@ interface IAxelarGasService { } contract AxelarHook is IPostDispatchHook { - error MetadataSize(uint256 expected, uint256 actual); - using StandardHookMetadata for bytes; using Message for bytes; From d00bba9ac514050a1f4038b162644ec005195a5c Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Wed, 13 Dec 2023 02:12:01 -0600 Subject: [PATCH 05/26] make _formatPayload view --- solidity/contracts/hooks/axelar/AxelarHook.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index b18a43b788..5383563899 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -120,7 +120,7 @@ contract AxelarHook is IPostDispatchHook { function _formatPayload( bytes calldata message - ) internal returns (bytes memory) { + ) internal view returns (bytes memory) { return abi.encodePacked(GMP_CALL_CODE, message.id()); } } From d20d755f08ef2c77174546363d00897fd071b918 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Wed, 13 Dec 2023 02:15:27 -0600 Subject: [PATCH 06/26] remove apostrophe from error message --- solidity/contracts/hooks/axelar/AxelarHook.sol | 2 +- solidity/test/hooks/AxelarHook.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 5383563899..90ec3298b3 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -113,7 +113,7 @@ contract AxelarHook is IPostDispatchHook { quote := mload(0x20) } - require(quote > 0, "Custom Metadata can't be zero value"); + require(quote > 0, "Custom Metadata cannot be zero value"); return quote; } diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index 386240e0ee..a4299f68e0 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -72,7 +72,7 @@ contract AxelarHookTest is Test { } function test_quoteDispatch_revertsWithZeroQuote() public { - vm.expectRevert("Custom Metadata can't be zero value"); + vm.expectRevert("Custom Metadata cannot be zero value"); uint256 expectedQuote = 0; bytes memory justRightCustomMetadata = abi.encodePacked( bytes32(expectedQuote) From 36be7bbed4de0c4418dc327b4c5b6a2447a1f3d8 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 14 Dec 2023 03:58:14 -0600 Subject: [PATCH 07/26] add custom metadata formatting library to help with bridge aggregation --- .../libs/BridgeAggregationHookMetadata.sol | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol diff --git a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol new file mode 100644 index 0000000000..46f380e78b --- /dev/null +++ b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/** + * Format of metadata: + * + * [0:2] variant + * [2:34] Axelar Payment + * [34:66] Wormhole Payment + * [66:] additional metadata + */ +library BridgeAggregationHookMetadata { + struct Metadata { + uint256 AxelarPayment; + uint256 WormholePayment; + } + + uint8 private constant AXELAR_PAYMENT_OFFSET = 0; + uint8 private constant WORMHOLE_PAYMENT_OFFSET = 32; + uint8 private constant MIN_METADATA_LENGTH = 64; + + /** + * @notice Returns the variant of the metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return variant of the metadata as uint8. + */ + 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 Returns the variant of the metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return variant of the metadata as uint8. + */ + function wormholeGasPayment( + bytes calldata _metadata + ) internal pure returns (uint256) { + if (_metadata.length < WORMHOLE_PAYMENT_OFFSET + 32) return 0; + return + uint256( + bytes32( + _metadata[WORMHOLE_PAYMENT_OFFSET:WORMHOLE_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 _wormholePayment Gas limit for the message. + * @param _customMetadata Additional metadata to include. + * @return ABI encoded standard hook metadata. + */ + function formatMetadata( + uint256 _axelarPayment, + uint256 _wormholePayment, + bytes memory _customMetadata + ) internal pure returns (bytes memory) { + return + abi.encodePacked(_axelarPayment, _wormholePayment, _customMetadata); + } +} From eeb04b0144f4b381552d7dd9a221e2fce8aee9a0 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 14 Dec 2023 03:58:54 -0600 Subject: [PATCH 08/26] update Axelar hook and tests with new library + small fixes --- .../contracts/hooks/axelar/AxelarHook.sol | 34 ++++++----------- solidity/test/hooks/AxelarHook.t.sol | 38 +++++-------------- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 90ec3298b3..4c4ede818d 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -4,6 +4,8 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; +import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetadata.sol"; + import {Message} from "../../libs/Message.sol"; //TODO: remove temp intermal import. for testing speed purposes only @@ -27,26 +29,27 @@ interface IAxelarGasService { contract AxelarHook is IPostDispatchHook { using StandardHookMetadata for bytes; + using BridgeAggregationHookMetadata for bytes; using Message for bytes; IAxelarGasService public immutable AXELAR_GAS_SERVICE; IAxelarGateway public immutable AXELAR_GATEWAY; string public DESTINATION_CHAIN; string public DESTINATION_CONTRACT; - bytes GMP_CALL_CODE; + bytes GMP_CALL_DATA; constructor( string memory destinationChain, string memory destionationContract, address axelarGateway, address axelarGasReceiver, - bytes memory gmp_call_code + bytes memory gmp_call_data ) { DESTINATION_CHAIN = destinationChain; DESTINATION_CONTRACT = destionationContract; AXELAR_GATEWAY = IAxelarGateway(axelarGateway); AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); - GMP_CALL_CODE = gmp_call_code; + GMP_CALL_DATA = gmp_call_data; } /** @@ -89,31 +92,16 @@ contract AxelarHook is IPostDispatchHook { /** * @notice Post action after a message is dispatched via the Mailbox * @param metadata The metadata required for the hook. Metadata should contain custom metadata - * for the gas amount owed to the Axelar Gas Service. + * adhering to the BridgeAggregationHookMetadata structure. */ function quoteDispatch( bytes calldata metadata, bytes calldata ) external pure returns (uint256) { - bytes calldata customMetadata = metadata.getCustomMetadata(); - // Ensure that the custom metadata is of the correct size - - require(customMetadata.length <= 32, "Custom metadata is too large"); - require( - customMetadata.length > 0, - "Empty custom metadata. Axelar needs payment." - ); - - uint256 quote; - assembly { - // Copy the custom metadata to memory - // The '0x20' adds an offset for the length field in memory - calldatacopy(0x20, customMetadata.offset, customMetadata.length) - // Load the data from memory into 'number' - quote := mload(0x20) - } + bytes calldata bridgeMetadata = metadata.getCustomMetadata(); - require(quote > 0, "Custom Metadata cannot be zero value"); + uint256 quote = bridgeMetadata.axelarGasPayment(); + require(quote > 0, "No Axelar Payment Received"); return quote; } @@ -121,6 +109,6 @@ contract AxelarHook is IPostDispatchHook { function _formatPayload( bytes calldata message ) internal view returns (bytes memory) { - return abi.encodePacked(GMP_CALL_CODE, message.id()); + return abi.encodePacked(GMP_CALL_DATA, message.id()); } } diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index a4299f68e0..9850cee68b 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -5,12 +5,14 @@ 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 {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; contract AxelarHookTest is Test { using StandardHookMetadata for bytes; + using BridgeAggregationHookMetadata for bytes; using TypeCasts for address; AxelarHook hook; @@ -41,7 +43,7 @@ contract AxelarHookTest is Test { } function test_quoteDispatch_revertsWithNoMetadata() public { - vm.expectRevert("Empty custom metadata. Axelar needs payment."); + vm.expectRevert("No Axelar Payment Received"); bytes memory emptyCustomMetadata; bytes memory testMetadata = StandardHookMetadata.formatMetadata( @@ -54,29 +56,11 @@ contract AxelarHookTest is Test { hook.quoteDispatch(testMetadata, testMessage); } - function test_quoteDispatch_revertsWithTooLargeMetadata() public { - vm.expectRevert("Custom metadata is too large"); - // tooLargeCustomMetadata becomes 33 bytes long after packing - bytes memory tooLargeCustomMetadata = abi.encodePacked( - bytes32(uint256(100)), - bytes1(uint8(1)) - ); - bytes memory testMetadata = StandardHookMetadata.formatMetadata( - 100, - 100, - msg.sender, - tooLargeCustomMetadata - ); - - hook.quoteDispatch(testMetadata, testMessage); - } - function test_quoteDispatch_revertsWithZeroQuote() public { - vm.expectRevert("Custom Metadata cannot be zero value"); + vm.expectRevert("No Axelar Payment Received"); uint256 expectedQuote = 0; - bytes memory justRightCustomMetadata = abi.encodePacked( - bytes32(expectedQuote) - ); + bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + .formatMetadata(expectedQuote, 0, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, @@ -89,9 +73,8 @@ contract AxelarHookTest is Test { function test_quoteDispatch_ReturnsSmallQuote() public { uint256 expectedQuote = 1; - bytes memory justRightCustomMetadata = abi.encodePacked( - bytes32(expectedQuote) - ); + bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + .formatMetadata(expectedQuote, 0, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, @@ -106,9 +89,8 @@ contract AxelarHookTest is Test { function test_quoteDispatch_ReturnsLargeQuote() public { // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935. that's a big quote uint256 expectedQuote = type(uint256).max; - bytes memory justRightCustomMetadata = abi.encodePacked( - bytes32(expectedQuote) - ); + bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata + .formatMetadata(expectedQuote, 0, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, From 8f2fd8eb023544bf241db343fcf2f8b96d91874b Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 14 Dec 2023 04:01:50 -0600 Subject: [PATCH 09/26] doc fix --- .../hooks/libs/BridgeAggregationHookMetadata.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol index 46f380e78b..26008be52a 100644 --- a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol +++ b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol @@ -20,9 +20,9 @@ library BridgeAggregationHookMetadata { uint8 private constant MIN_METADATA_LENGTH = 64; /** - * @notice Returns the variant of the metadata. + * @notice Returns the required payment for Axelar bridging. * @param _metadata ABI encoded standard hook metadata. - * @return variant of the metadata as uint8. + * @return uint256 Payment amount. */ function axelarGasPayment( bytes calldata _metadata @@ -37,9 +37,9 @@ library BridgeAggregationHookMetadata { } /** - * @notice Returns the variant of the metadata. + * @notice Returns the required payment for Wormhole bridging. * @param _metadata ABI encoded standard hook metadata. - * @return variant of the metadata as uint8. + * @return uint256 Payment amount. */ function wormholeGasPayment( bytes calldata _metadata From 2e887669cb81f935cffb1bade09f5c16839c8623 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 14 Dec 2023 04:17:52 -0600 Subject: [PATCH 10/26] add _isLatestDispatched check to Axelar hook --- solidity/contracts/hooks/axelar/AxelarHook.sol | 10 ++++++++-- solidity/test/hooks/AxelarHook.t.sol | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 4c4ede818d..4b9704e193 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -7,6 +7,7 @@ import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetadata.sol"; import {Message} from "../../libs/Message.sol"; +import {MailboxClient} from "../../client/MailboxClient.sol"; //TODO: remove temp intermal import. for testing speed purposes only interface IAxelarGateway { @@ -27,7 +28,7 @@ interface IAxelarGasService { ) external payable; } -contract AxelarHook is IPostDispatchHook { +contract AxelarHook is IPostDispatchHook, MailboxClient { using StandardHookMetadata for bytes; using BridgeAggregationHookMetadata for bytes; using Message for bytes; @@ -39,12 +40,13 @@ contract AxelarHook is IPostDispatchHook { bytes GMP_CALL_DATA; constructor( + address _mailbox, string memory destinationChain, string memory destionationContract, address axelarGateway, address axelarGasReceiver, bytes memory gmp_call_data - ) { + ) MailboxClient(_mailbox) { DESTINATION_CHAIN = destinationChain; DESTINATION_CONTRACT = destionationContract; AXELAR_GATEWAY = IAxelarGateway(axelarGateway); @@ -71,6 +73,10 @@ contract AxelarHook is IPostDispatchHook { bytes calldata metadata, bytes calldata message ) external payable { + // ensure messages which were not dispatched are not inserted into the tree + bytes32 id = message.id(); + require(_isLatestDispatched(id), "message not dispatching"); + bytes memory axelarPayload = _formatPayload(message); // Pay for gas used by Axelar with ETH AXELAR_GAS_SERVICE.payNativeGasForContractCall{value: msg.value}( diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index 9850cee68b..707e19131a 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -9,12 +9,15 @@ import {BridgeAggregationHookMetadata} from "../../contracts/hooks/libs/BridgeAg import {MessageUtils} from "../isms/IsmTestUtils.sol"; import {AxelarHook} 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; + TestMailbox mailbox; address internal alice = address(0x1); // alice the user address internal bob = address(0x2); // bob the beneficiary @@ -32,7 +35,10 @@ contract AxelarHookTest is Test { error BadQuote(uint256 balance, uint256 required); function setUp() public { + mailbox = new TestMailbox(1); + hook = new AxelarHook( + address(mailbox), destinationChain, destionationContract, axelarGateway, From 8f405246cbd2e262858681dfc0d6b32a6b6435e5 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 14 Dec 2023 04:20:01 -0600 Subject: [PATCH 11/26] doc fix --- solidity/contracts/hooks/axelar/AxelarHook.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 4b9704e193..2257273dc4 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -73,9 +73,9 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { bytes calldata metadata, bytes calldata message ) external payable { - // ensure messages which were not dispatched are not inserted into the tree + // ensure hook only dispatches messages that are dispatched by the mailbox bytes32 id = message.id(); - require(_isLatestDispatched(id), "message not dispatching"); + require(_isLatestDispatched(id), "message not dispatched by mailbox"); bytes memory axelarPayload = _formatPayload(message); // Pay for gas used by Axelar with ETH From c78975556a6305c71c2f043f13106d7f3257eee2 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Fri, 15 Dec 2023 09:55:18 -0600 Subject: [PATCH 12/26] modify BridgeAggregationHookMetadata to only include Axelar payment data --- .../libs/BridgeAggregationHookMetadata.sol | 33 +++---------------- solidity/test/hooks/AxelarHook.t.sol | 10 +++--- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol index 26008be52a..8863350bd7 100644 --- a/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol +++ b/solidity/contracts/hooks/libs/BridgeAggregationHookMetadata.sol @@ -4,20 +4,16 @@ pragma solidity >=0.8.0; /** * Format of metadata: * - * [0:2] variant - * [2:34] Axelar Payment - * [34:66] Wormhole Payment - * [66:] additional metadata + * [0:32] variant + * [32:] additional metadata */ library BridgeAggregationHookMetadata { struct Metadata { uint256 AxelarPayment; - uint256 WormholePayment; } uint8 private constant AXELAR_PAYMENT_OFFSET = 0; - uint8 private constant WORMHOLE_PAYMENT_OFFSET = 32; - uint8 private constant MIN_METADATA_LENGTH = 64; + uint8 private constant MIN_METADATA_LENGTH = 32; /** * @notice Returns the required payment for Axelar bridging. @@ -36,24 +32,6 @@ library BridgeAggregationHookMetadata { ); } - /** - * @notice Returns the required payment for Wormhole bridging. - * @param _metadata ABI encoded standard hook metadata. - * @return uint256 Payment amount. - */ - function wormholeGasPayment( - bytes calldata _metadata - ) internal pure returns (uint256) { - if (_metadata.length < WORMHOLE_PAYMENT_OFFSET + 32) return 0; - return - uint256( - bytes32( - _metadata[WORMHOLE_PAYMENT_OFFSET:WORMHOLE_PAYMENT_OFFSET + - 32] - ) - ); - } - /** * @notice Returs any additional metadata. * @param _metadata ABI encoded standard hook metadata. @@ -69,16 +47,13 @@ library BridgeAggregationHookMetadata { /** * @notice Formats the specified Axelar and Wormhole payments. * @param _axelarPayment msg.value for the message. - * @param _wormholePayment Gas limit for the message. * @param _customMetadata Additional metadata to include. * @return ABI encoded standard hook metadata. */ function formatMetadata( uint256 _axelarPayment, - uint256 _wormholePayment, bytes memory _customMetadata ) internal pure returns (bytes memory) { - return - abi.encodePacked(_axelarPayment, _wormholePayment, _customMetadata); + return abi.encodePacked(_axelarPayment, _customMetadata); } } diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index 707e19131a..b02c7ecbe5 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -31,7 +31,6 @@ contract AxelarHookTest is Test { string destionationContract = "neutronContract"; address axelarGateway = address(0); address axelarGasReceiver = address(0); - bytes gmp_call_code = abi.encodePacked(uint8(1)); error BadQuote(uint256 balance, uint256 required); function setUp() public { @@ -42,8 +41,7 @@ contract AxelarHookTest is Test { destinationChain, destionationContract, axelarGateway, - axelarGasReceiver, - gmp_call_code + axelarGasReceiver ); testMessage = _encodeTestMessage(); } @@ -66,7 +64,7 @@ contract AxelarHookTest is Test { vm.expectRevert("No Axelar Payment Received"); uint256 expectedQuote = 0; bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata - .formatMetadata(expectedQuote, 0, abi.encodePacked()); + .formatMetadata(expectedQuote, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, @@ -80,7 +78,7 @@ contract AxelarHookTest is Test { function test_quoteDispatch_ReturnsSmallQuote() public { uint256 expectedQuote = 1; bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata - .formatMetadata(expectedQuote, 0, abi.encodePacked()); + .formatMetadata(expectedQuote, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, @@ -96,7 +94,7 @@ contract AxelarHookTest is Test { // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935. that's a big quote uint256 expectedQuote = type(uint256).max; bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata - .formatMetadata(expectedQuote, 0, abi.encodePacked()); + .formatMetadata(expectedQuote, abi.encodePacked()); bytes memory testMetadata = StandardHookMetadata.formatMetadata( 100, 100, From ceeebfa633cc5f88e2f453fa5c7396d649fa8e83 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Fri, 15 Dec 2023 10:17:22 -0600 Subject: [PATCH 13/26] generate gmp payload internally --- .../contracts/hooks/axelar/AxelarHook.sol | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 2257273dc4..c9898ad597 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -9,7 +9,6 @@ import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetada import {Message} from "../../libs/Message.sol"; import {MailboxClient} from "../../client/MailboxClient.sol"; -//TODO: remove temp intermal import. for testing speed purposes only interface IAxelarGateway { function callContract( string calldata destinationChain, @@ -37,21 +36,18 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { IAxelarGateway public immutable AXELAR_GATEWAY; string public DESTINATION_CHAIN; string public DESTINATION_CONTRACT; - bytes GMP_CALL_DATA; constructor( address _mailbox, string memory destinationChain, string memory destionationContract, address axelarGateway, - address axelarGasReceiver, - bytes memory gmp_call_data + address axelarGasReceiver ) MailboxClient(_mailbox) { DESTINATION_CHAIN = destinationChain; DESTINATION_CONTRACT = destionationContract; AXELAR_GATEWAY = IAxelarGateway(axelarGateway); AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); - GMP_CALL_DATA = gmp_call_data; } /** @@ -77,7 +73,8 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { bytes32 id = message.id(); require(_isLatestDispatched(id), "message not dispatched by mailbox"); - bytes memory axelarPayload = _formatPayload(message); + bytes memory axelarPayload = _encodeGmpPayload(id); + // Pay for gas used by Axelar with ETH AXELAR_GAS_SERVICE.payNativeGasForContractCall{value: msg.value}( address(this), @@ -112,9 +109,34 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { return quote; } - function _formatPayload( - bytes calldata message - ) internal view returns (bytes memory) { - return abi.encodePacked(GMP_CALL_DATA, message.id()); + /** + * @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 pure returns (bytes memory) { + // dociding version used by Axelar + bytes4 version = bytes4(0x00000001); + + //name of the arguments used in the cross-chain function call + string[] memory argumentNameArray = new string[](1); + argumentNameArray[0] = "id"; + + // type of argument used in the cross-chain function call + string[] memory abiTypeArray = new string[](1); + abiTypeArray[0] = "string"; + + // add the function name: (submit_meta) and argument value (_id) + bytes memory gmpPayload = abi.encode( + "submit_meta", + argumentNameArray, + abiTypeArray, + _id + ); + + // encode the version and return the payload + return abi.encodePacked(version, gmpPayload); } } From 5805e9265f722ae9565f1aef09eafb7c6026413b Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 16 Dec 2023 16:50:04 -0600 Subject: [PATCH 14/26] hook fixes --- .../contracts/hooks/wormhole/WormholeHook.sol | 17 +++++++++++++---- .../interfaces/IInterchainSecurityModule.sol | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/hooks/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol index d1c0322409..2a98323044 100644 --- a/solidity/contracts/hooks/wormhole/WormholeHook.sol +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; +import {Message} from "../../libs/Message.sol"; +import {MailboxClient} from "../../client/MailboxClient.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 @@ -14,10 +16,12 @@ interface IWormhole { ) external payable returns (uint64 sequence); } -contract WormholeHook is IPostDispatchHook { +contract WormholeHook is IPostDispatchHook, MailboxClient { + using Message for bytes; + IWormhole public wormhole; - constructor(address _wormhole) { + constructor(address _wormhole, address _mailbox) MailboxClient(_mailbox) { wormhole = IWormhole(_wormhole); } @@ -26,14 +30,19 @@ contract WormholeHook is IPostDispatchHook { } function supportsMetadata(bytes calldata) external pure returns (bool) { - return false; + return true; } function postDispatch( bytes calldata, bytes calldata message ) external payable { - wormhole.publishMessage{value: msg.value}(0, message, 200); + // ensure hook only dispatches messages that are dispatched by the mailbox + bytes32 id = message.id(); + require(_isLatestDispatched(id), "message not dispatched by 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), 201); } function quoteDispatch( diff --git a/solidity/contracts/interfaces/IInterchainSecurityModule.sol b/solidity/contracts/interfaces/IInterchainSecurityModule.sol index d4e6c30c61..b361e16902 100644 --- a/solidity/contracts/interfaces/IInterchainSecurityModule.sol +++ b/solidity/contracts/interfaces/IInterchainSecurityModule.sol @@ -10,7 +10,9 @@ interface IInterchainSecurityModule { MERKLE_ROOT_MULTISIG, MESSAGE_ID_MULTISIG, NULL, // used with relayer carrying no metadata - CCIP_READ + CCIP_READ, + AXELAR, + WORMHOLE } /** From cd9056af73fc3bc05b90304113aa61f2f83ab4e6 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 16 Dec 2023 18:28:57 -0600 Subject: [PATCH 15/26] add v1 axelar ISM --- solidity/contracts/isms/Axelar/AxelarIsm.sol | 110 +++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 solidity/contracts/isms/Axelar/AxelarIsm.sol diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol new file mode 100644 index 0000000000..6b323f7b84 --- /dev/null +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -0,0 +1,110 @@ +// 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"; + +interface IAxelarGateway { + function validateContractCall( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash + ) external returns (bool); +} + +contract AxelarIsm is IInterchainSecurityModule { + using Message for bytes; + + IAxelarGateway public immutable AXELAR_GATEWAY; + string public SOURCE_CHAIN; + string public SOURCE_ADDRESS; + + mapping(bytes32 => bool) public validated; + + constructor( + address axelarGateway, + string memory sourceChain, + string memory sourceAddress + ) { + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + SOURCE_CHAIN = sourceChain; + SOURCE_ADDRESS = sourceAddress; + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function moduleType() external pure returns (uint8) { + return uint8(IInterchainSecurityModule.Types.AXELAR); + } + + /** + * @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" + ); + + //TODO get hyperlane ID. verify Axelar gmp input. this is a placeholder zero-value. + bytes32 hyperlaneId; // bytes32(payload[:32]); + + 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)); + } +} From f4e301d1e4d79f6488ca84b211cf42b0fbfcab68 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 16 Dec 2023 18:29:26 -0600 Subject: [PATCH 16/26] add v1 wormhole ISM --- .../contracts/isms/Wormhole/WormholeIsm.sol | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 solidity/contracts/isms/Wormhole/WormholeIsm.sol diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol new file mode 100644 index 0000000000..076a30296a --- /dev/null +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -0,0 +1,100 @@ +// 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"; + +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 { + using Message for bytes; + + IWormhole public WORMHOLE; + uint16 public SOURCE_CHAIN_ID; + bytes32 public SOURCE_ADDRESS; + + mapping(bytes32 => bool) public validated; + + constructor( + address _wormhole, + uint16 sourceChainId, + bytes32 sourceAddress + ) { + WORMHOLE = IWormhole(_wormhole); + SOURCE_CHAIN_ID = sourceChainId; + SOURCE_ADDRESS = sourceAddress; + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function moduleType() external pure returns (uint8) { + return uint8(IInterchainSecurityModule.Types.WORMHOLE); + } + + /** + * @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()]; + } +} From 1932675a396ce76eb78bd698e301654c0ecdcc26 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 16 Dec 2023 18:30:24 -0600 Subject: [PATCH 17/26] add axelar and wormhole to ISM interface --- solidity/contracts/interfaces/IInterchainSecurityModule.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solidity/contracts/interfaces/IInterchainSecurityModule.sol b/solidity/contracts/interfaces/IInterchainSecurityModule.sol index d4e6c30c61..b361e16902 100644 --- a/solidity/contracts/interfaces/IInterchainSecurityModule.sol +++ b/solidity/contracts/interfaces/IInterchainSecurityModule.sol @@ -10,7 +10,9 @@ interface IInterchainSecurityModule { MERKLE_ROOT_MULTISIG, MESSAGE_ID_MULTISIG, NULL, // used with relayer carrying no metadata - CCIP_READ + CCIP_READ, + AXELAR, + WORMHOLE } /** From 7e163e95358e628bda40c209090032451afb2b8c Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sun, 17 Dec 2023 23:57:13 -0600 Subject: [PATCH 18/26] secure hook --- solidity/contracts/hooks/wormhole/WormholeHook.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solidity/contracts/hooks/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol index 2a98323044..7d5538a5ea 100644 --- a/solidity/contracts/hooks/wormhole/WormholeHook.sol +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -39,7 +39,10 @@ contract WormholeHook is IPostDispatchHook, MailboxClient { ) 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"); + 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), 201); From 962d50b3a6dd7897df34790cf1812d8efbbde1b0 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Tue, 26 Dec 2023 01:38:42 -0500 Subject: [PATCH 19/26] move source and destination values to initializer --- solidity/contracts/hooks/axelar/AxelarHook.sol | 17 ++++++++++++----- solidity/contracts/isms/Axelar/AxelarIsm.sol | 16 +++++++++++----- .../contracts/isms/Wormhole/WormholeIsm.sol | 16 +++++++++++----- solidity/test/hooks/AxelarHook.t.sol | 16 ++++++++++++++-- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index c9898ad597..2f6794d53e 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -5,7 +5,7 @@ pragma solidity >=0.8.0; import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetadata.sol"; - +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Message} from "../../libs/Message.sol"; import {MailboxClient} from "../../client/MailboxClient.sol"; @@ -39,17 +39,24 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { constructor( address _mailbox, - string memory destinationChain, - string memory destionationContract, address axelarGateway, address axelarGasReceiver ) MailboxClient(_mailbox) { - DESTINATION_CHAIN = destinationChain; - DESTINATION_CONTRACT = destionationContract; AXELAR_GATEWAY = IAxelarGateway(axelarGateway); AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); } + /** + * @notice Initializes the hook with specific targets + */ + function initializeReceiver( + string memory destinationChain, + string memory destionationContract + ) external onlyOwner initializer { + DESTINATION_CHAIN = destinationChain; + DESTINATION_CONTRACT = destionationContract; + } + /** * @notice Returns an enum that represents the type of hook */ diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index 6b323f7b84..e60d91bff4 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; import {Message} from "../../libs/Message.sol"; @@ -13,7 +14,7 @@ interface IAxelarGateway { ) external returns (bool); } -contract AxelarIsm is IInterchainSecurityModule { +contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { using Message for bytes; IAxelarGateway public immutable AXELAR_GATEWAY; @@ -22,12 +23,17 @@ contract AxelarIsm is IInterchainSecurityModule { mapping(bytes32 => bool) public validated; - constructor( - address axelarGateway, + constructor(address axelarGateway) { + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + } + + /** + * @notice Initializes the hook with specific targets + */ + function initializeSource( string memory sourceChain, string memory sourceAddress - ) { - AXELAR_GATEWAY = IAxelarGateway(axelarGateway); + ) external onlyOwner initializer { SOURCE_CHAIN = sourceChain; SOURCE_ADDRESS = sourceAddress; } diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol index 076a30296a..c4a3fbed5d 100644 --- a/solidity/contracts/isms/Wormhole/WormholeIsm.sol +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; import {Message} from "../../libs/Message.sol"; @@ -30,7 +31,7 @@ interface IWormhole { ) external view returns (VM memory vm, bool valid, string memory reason); } -contract WormholeIsm is IInterchainSecurityModule { +contract WormholeIsm is IInterchainSecurityModule, OwnableUpgradeable { using Message for bytes; IWormhole public WORMHOLE; @@ -39,12 +40,17 @@ contract WormholeIsm is IInterchainSecurityModule { mapping(bytes32 => bool) public validated; - constructor( - address _wormhole, + constructor(address _wormhole) { + WORMHOLE = IWormhole(_wormhole); + } + + /** + * @notice Initializes the hook with specific targets + */ + function initializeSource( uint16 sourceChainId, bytes32 sourceAddress - ) { - WORMHOLE = IWormhole(_wormhole); + ) external onlyOwner initializer { SOURCE_CHAIN_ID = sourceChainId; SOURCE_ADDRESS = sourceAddress; } diff --git a/solidity/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index b02c7ecbe5..1b1740cf60 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -38,14 +38,26 @@ contract AxelarHookTest is Test { hook = new AxelarHook( address(mailbox), - destinationChain, - destionationContract, axelarGateway, axelarGasReceiver ); + 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("Initializable: contract is already initialized"); + + hook.initializeReceiver(destinationChain, destionationContract); + } + function test_quoteDispatch_revertsWithNoMetadata() public { vm.expectRevert("No Axelar Payment Received"); From 64533246aedbe7e9e6e94499cfba508ee10005ee Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 28 Dec 2023 00:49:36 -0600 Subject: [PATCH 20/26] Remove constructor from hooks --- solidity/contracts/isms/Axelar/AxelarIsm.sol | 10 ++++------ solidity/contracts/isms/Wormhole/WormholeIsm.sol | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index e60d91bff4..9af2fde6f7 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -17,25 +17,23 @@ interface IAxelarGateway { contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { using Message for bytes; - IAxelarGateway public immutable AXELAR_GATEWAY; + IAxelarGateway public AXELAR_GATEWAY; string public SOURCE_CHAIN; string public SOURCE_ADDRESS; mapping(bytes32 => bool) public validated; - constructor(address axelarGateway) { - AXELAR_GATEWAY = IAxelarGateway(axelarGateway); - } - /** * @notice Initializes the hook with specific targets */ function initializeSource( string memory sourceChain, - string memory sourceAddress + string memory sourceAddress, + address axelarGateway ) external onlyOwner initializer { SOURCE_CHAIN = sourceChain; SOURCE_ADDRESS = sourceAddress; + AXELAR_GATEWAY = IAxelarGateway(axelarGateway); } /** diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol index c4a3fbed5d..a216efecec 100644 --- a/solidity/contracts/isms/Wormhole/WormholeIsm.sol +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -40,19 +40,17 @@ contract WormholeIsm is IInterchainSecurityModule, OwnableUpgradeable { mapping(bytes32 => bool) public validated; - constructor(address _wormhole) { - WORMHOLE = IWormhole(_wormhole); - } - /** * @notice Initializes the hook with specific targets */ function initializeSource( uint16 sourceChainId, - bytes32 sourceAddress + bytes32 sourceAddress, + address _wormhole ) external onlyOwner initializer { SOURCE_CHAIN_ID = sourceChainId; SOURCE_ADDRESS = sourceAddress; + WORMHOLE = IWormhole(_wormhole); } /** From 53d1401fac7640c29ad77c96db8a937a999696e3 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 28 Dec 2023 01:37:31 -0600 Subject: [PATCH 21/26] Remove MailboxClient dependencies --- .../contracts/hooks/axelar/AxelarHook.sol | 24 +++++++++++++++---- .../contracts/hooks/wormhole/WormholeHook.sol | 23 +++++++++++++----- solidity/test/hooks/AxelarHook.t.sol | 2 +- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index 2f6794d53e..e5204cae64 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -7,7 +7,7 @@ import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetadata.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Message} from "../../libs/Message.sol"; -import {MailboxClient} from "../../client/MailboxClient.sol"; +import {IMailbox} from "../../interfaces/IMailbox.sol"; interface IAxelarGateway { function callContract( @@ -27,11 +27,12 @@ interface IAxelarGasService { ) external payable; } -contract AxelarHook is IPostDispatchHook, MailboxClient { +contract AxelarHook is IPostDispatchHook, Ownable { using StandardHookMetadata for bytes; using BridgeAggregationHookMetadata for bytes; using Message for bytes; + IMailbox public immutable MAILBOX; IAxelarGasService public immutable AXELAR_GAS_SERVICE; IAxelarGateway public immutable AXELAR_GATEWAY; string public DESTINATION_CHAIN; @@ -41,7 +42,8 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { address _mailbox, address axelarGateway, address axelarGasReceiver - ) MailboxClient(_mailbox) { + ) { + MAILBOX = IMailbox(_mailbox); AXELAR_GATEWAY = IAxelarGateway(axelarGateway); AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); } @@ -52,7 +54,12 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { function initializeReceiver( string memory destinationChain, string memory destionationContract - ) external onlyOwner initializer { + ) external onlyOwner { + require( + bytes(DESTINATION_CHAIN).length == 0 && + bytes(DESTINATION_CONTRACT).length == 0, + "Already initialized" + ); DESTINATION_CHAIN = destinationChain; DESTINATION_CONTRACT = destionationContract; } @@ -146,4 +153,13 @@ contract AxelarHook is IPostDispatchHook, MailboxClient { // encode the version and return the payload return abi.encodePacked(version, 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/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol index 7d5538a5ea..54fa6740cd 100644 --- a/solidity/contracts/hooks/wormhole/WormholeHook.sol +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {Message} from "../../libs/Message.sol"; -import {MailboxClient} from "../../client/MailboxClient.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 @@ -16,13 +16,15 @@ interface IWormhole { ) external payable returns (uint64 sequence); } -contract WormholeHook is IPostDispatchHook, MailboxClient { +contract WormholeHook is IPostDispatchHook { using Message for bytes; - IWormhole public wormhole; + IMailbox public immutable MAILBOX; + IWormhole public WORMHOLE; - constructor(address _wormhole, address _mailbox) MailboxClient(_mailbox) { - wormhole = IWormhole(_wormhole); + constructor(address _wormhole, address _mailbox) { + WORMHOLE = IWormhole(_wormhole); + MAILBOX = IMailbox(_mailbox); } function hookType() external pure returns (uint8) { @@ -45,7 +47,7 @@ contract WormholeHook is IPostDispatchHook, MailboxClient { ); // 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), 201); + WORMHOLE.publishMessage{value: msg.value}(0, abi.encodePacked(id), 201); } function quoteDispatch( @@ -54,4 +56,13 @@ contract WormholeHook is IPostDispatchHook, MailboxClient { ) 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/test/hooks/AxelarHook.t.sol b/solidity/test/hooks/AxelarHook.t.sol index 1b1740cf60..6a810550c7 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -53,7 +53,7 @@ contract AxelarHookTest is Test { } function test_iinitializeReceiver_revertsWhenCalledAgain() public { - vm.expectRevert("Initializable: contract is already initialized"); + vm.expectRevert("Already initialized"); hook.initializeReceiver(destinationChain, destionationContract); } From 9831a5e901e6c7b58db86c3c68ac5478cdd48477 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 28 Dec 2023 01:38:55 -0600 Subject: [PATCH 22/26] consistency level 202 for full safety --- solidity/contracts/hooks/wormhole/WormholeHook.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/hooks/wormhole/WormholeHook.sol b/solidity/contracts/hooks/wormhole/WormholeHook.sol index 54fa6740cd..5493cb0aa2 100644 --- a/solidity/contracts/hooks/wormhole/WormholeHook.sol +++ b/solidity/contracts/hooks/wormhole/WormholeHook.sol @@ -47,7 +47,7 @@ contract WormholeHook is IPostDispatchHook { ); // 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), 201); + WORMHOLE.publishMessage{value: msg.value}(0, abi.encodePacked(id), 202); } function quoteDispatch( From ca018b3f8648e65366b0b10ff605202d89cc868f Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 27 Jan 2024 19:55:05 -0600 Subject: [PATCH 23/26] get Axelar payload --- solidity/contracts/isms/Axelar/AxelarIsm.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index 9af2fde6f7..b77b89c5df 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -77,8 +77,8 @@ contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { "Unexpected Axelar source address" ); - //TODO get hyperlane ID. verify Axelar gmp input. this is a placeholder zero-value. - bytes32 hyperlaneId; // bytes32(payload[:32]); + //get hyperlane ID, decode into bytes32 from byte array + bytes32 hyperlaneId = abi.decode(payload, (bytes32)); validated[hyperlaneId] = true; } From 2f2c819323740ceabdc3cd15e9ba06b335eea4d8 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Sat, 27 Jan 2024 20:06:27 -0600 Subject: [PATCH 24/26] remove space --- solidity/contracts/isms/Axelar/AxelarIsm.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index b77b89c5df..c04159cf9a 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -79,7 +79,6 @@ contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { //get hyperlane ID, decode into bytes32 from byte array bytes32 hyperlaneId = abi.decode(payload, (bytes32)); - validated[hyperlaneId] = true; } From cd56cf65b50d78a89a492e06818cbdf856ec3114 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Thu, 15 Feb 2024 21:00:08 -0600 Subject: [PATCH 25/26] testing modifications --- .../contracts/hooks/axelar/AxelarHook.sol | 83 +++++++---- solidity/contracts/isms/Axelar/AxelarIsm.sol | 11 +- .../contracts/isms/Wormhole/WormholeIsm.sol | 11 +- solidity/contracts/libs/AddressString.sol | 52 +++++++ solidity/test/hooks/AxelarHook.t.sol | 131 +++++++++++------- 5 files changed, 203 insertions(+), 85 deletions(-) create mode 100644 solidity/contracts/libs/AddressString.sol diff --git a/solidity/contracts/hooks/axelar/AxelarHook.sol b/solidity/contracts/hooks/axelar/AxelarHook.sol index e5204cae64..8c2585fd32 100644 --- a/solidity/contracts/hooks/axelar/AxelarHook.sol +++ b/solidity/contracts/hooks/axelar/AxelarHook.sol @@ -4,11 +4,13 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; -import {BridgeAggregationHookMetadata} from "../libs/BridgeAggregationHookMetadata.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.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, @@ -26,26 +28,42 @@ interface IAxelarGasService { 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 BridgeAggregationHookMetadata 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 axelarGasReceiver, + address axelarHookGasService ) { MAILBOX = IMailbox(_mailbox); AXELAR_GATEWAY = IAxelarGateway(axelarGateway); AXELAR_GAS_SERVICE = IAxelarGasService(axelarGasReceiver); + AXELAR_HOOK_GAS = IAxelarHookGasService(axelarHookGasService); } /** @@ -55,11 +73,11 @@ contract AxelarHook is IPostDispatchHook, Ownable { string memory destinationChain, string memory destionationContract ) external onlyOwner { - require( - bytes(DESTINATION_CHAIN).length == 0 && - bytes(DESTINATION_CONTRACT).length == 0, - "Already initialized" - ); + // require( + // bytes(DESTINATION_CHAIN).length == 0 && + // bytes(DESTINATION_CONTRACT).length == 0, + // "Already initialized" + // ); DESTINATION_CHAIN = destinationChain; DESTINATION_CONTRACT = destionationContract; } @@ -107,20 +125,13 @@ contract AxelarHook is IPostDispatchHook, Ownable { } /** - * @notice Post action after a message is dispatched via the Mailbox - * @param metadata The metadata required for the hook. Metadata should contain custom metadata - * adhering to the BridgeAggregationHookMetadata structure. + * @notice Quote for the amount of value required to run this hook. */ function quoteDispatch( - bytes calldata metadata, + bytes calldata, bytes calldata - ) external pure returns (uint256) { - bytes calldata bridgeMetadata = metadata.getCustomMetadata(); - - uint256 quote = bridgeMetadata.axelarGasPayment(); - require(quote > 0, "No Axelar Payment Received"); - - return quote; + ) external view returns (uint256) { + return AXELAR_HOOK_GAS.getGas(); } /** @@ -128,30 +139,42 @@ contract AxelarHook is IPostDispatchHook, Ownable { * @param _id The latest id of the current dispatched hyperlane message * @return bytes The Axelar GMP payload. */ - function _encodeGmpPayload( - bytes32 _id - ) internal pure returns (bytes memory) { + function _encodeGmpPayload(bytes32 _id) internal returns (bytes memory) { // dociding version used by Axelar - bytes4 version = bytes4(0x00000001); + bytes4 version = bytes4(uint32(1)); //name of the arguments used in the cross-chain function call - string[] memory argumentNameArray = new string[](1); - argumentNameArray[0] = "id"; - + 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[](1); + 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, - _id + argValue ); // encode the version and return the payload - return abi.encodePacked(version, gmpPayload); + return + abi.encodePacked( + version, // version number + gmpPayload + ); } /** diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index c04159cf9a..9a7e738791 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; import {Message} from "../../libs/Message.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; interface IAxelarGateway { function validateContractCall( @@ -14,7 +14,7 @@ interface IAxelarGateway { ) external returns (bool); } -contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { +contract AxelarIsm is IInterchainSecurityModule, Ownable { using Message for bytes; IAxelarGateway public AXELAR_GATEWAY; @@ -30,7 +30,12 @@ contract AxelarIsm is IInterchainSecurityModule, OwnableUpgradeable { string memory sourceChain, string memory sourceAddress, address axelarGateway - ) external onlyOwner initializer { + ) 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); diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol index a216efecec..7d089cf102 100644 --- a/solidity/contracts/isms/Wormhole/WormholeIsm.sol +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; import {Message} from "../../libs/Message.sol"; @@ -31,7 +31,7 @@ interface IWormhole { ) external view returns (VM memory vm, bool valid, string memory reason); } -contract WormholeIsm is IInterchainSecurityModule, OwnableUpgradeable { +contract WormholeIsm is IInterchainSecurityModule, Ownable { using Message for bytes; IWormhole public WORMHOLE; @@ -47,7 +47,12 @@ contract WormholeIsm is IInterchainSecurityModule, OwnableUpgradeable { uint16 sourceChainId, bytes32 sourceAddress, address _wormhole - ) external onlyOwner initializer { + ) 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); 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 index 6a810550c7..475a453826 100644 --- a/solidity/test/hooks/AxelarHook.t.sol +++ b/solidity/test/hooks/AxelarHook.t.sol @@ -8,6 +8,7 @@ import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetad 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"; @@ -17,6 +18,7 @@ contract AxelarHookTest is Test { using TypeCasts for address; AxelarHook hook; + AxelarHookGasService gas_service; TestMailbox mailbox; address internal alice = address(0x1); // alice the user @@ -35,11 +37,12 @@ contract AxelarHookTest is Test { function setUp() public { mailbox = new TestMailbox(1); - + gas_service = new AxelarHookGasService(); hook = new AxelarHook( address(mailbox), axelarGateway, - axelarGasReceiver + axelarGasReceiver, + address(gas_service) ); hook.initializeReceiver(destinationChain, destionationContract); testMessage = _encodeTestMessage(); @@ -58,66 +61,96 @@ contract AxelarHookTest is Test { 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_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_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 - ); + function test_setGas_OwnerCanSetGas() public { + AxelarHookGasService temp_gas_service = new AxelarHookGasService(); + uint256 expectedQuote = type(uint256).max; - hook.quoteDispatch(testMetadata, testMessage); + temp_gas_service.setGas(expectedQuote); + uint256 quote = temp_gas_service.getGas(); + + assertEq(quote, expectedQuote); } - 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 - ); + function test_quoteDispatch_ReturnsZeroWhenUnset() public { + uint256 expectedQuote = 0; + uint256 quote = hook.quoteDispatch("0x00", "0x00"); - uint256 quote = hook.quoteDispatch(testMetadata, testMessage); assertEq(quote, expectedQuote); } - function test_quoteDispatch_ReturnsLargeQuote() public { - // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935. that's a big quote + function test_quoteDispatch_ReturnsGasServiceQuote() public { + // type(uint256).max = 115792089237316195423570985008687907853269984665640564039457584007913129639935 uint256 expectedQuote = type(uint256).max; - bytes memory justRightCustomMetadata = BridgeAggregationHookMetadata - .formatMetadata(expectedQuote, abi.encodePacked()); - bytes memory testMetadata = StandardHookMetadata.formatMetadata( - 100, - 100, - msg.sender, - justRightCustomMetadata - ); + gas_service.setGas(expectedQuote); - uint256 quote = hook.quoteDispatch(testMetadata, testMessage); + uint256 quote = hook.quoteDispatch("0x00", "0x00"); assertEq(quote, expectedQuote); } - // ============ Helper Functions ============ function _encodeTestMessage() internal view returns (bytes memory) { From 565801ffc90169121bb377a113400925564dcbe4 Mon Sep 17 00:00:00 2001 From: NicholasDotSol Date: Tue, 20 Feb 2024 08:58:33 -0600 Subject: [PATCH 26/26] change ISM type to null for relayer compatibility --- solidity/contracts/interfaces/IInterchainSecurityModule.sol | 4 +--- solidity/contracts/isms/Axelar/AxelarIsm.sol | 2 +- solidity/contracts/isms/Wormhole/WormholeIsm.sol | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/interfaces/IInterchainSecurityModule.sol b/solidity/contracts/interfaces/IInterchainSecurityModule.sol index b361e16902..d4e6c30c61 100644 --- a/solidity/contracts/interfaces/IInterchainSecurityModule.sol +++ b/solidity/contracts/interfaces/IInterchainSecurityModule.sol @@ -10,9 +10,7 @@ interface IInterchainSecurityModule { MERKLE_ROOT_MULTISIG, MESSAGE_ID_MULTISIG, NULL, // used with relayer carrying no metadata - CCIP_READ, - AXELAR, - WORMHOLE + CCIP_READ } /** diff --git a/solidity/contracts/isms/Axelar/AxelarIsm.sol b/solidity/contracts/isms/Axelar/AxelarIsm.sol index 9a7e738791..12a6d51811 100644 --- a/solidity/contracts/isms/Axelar/AxelarIsm.sol +++ b/solidity/contracts/isms/Axelar/AxelarIsm.sol @@ -45,7 +45,7 @@ contract AxelarIsm is IInterchainSecurityModule, Ownable { * @notice Returns an enum that represents the type of hook */ function moduleType() external pure returns (uint8) { - return uint8(IInterchainSecurityModule.Types.AXELAR); + return uint8(IInterchainSecurityModule.Types.NULL); } /** diff --git a/solidity/contracts/isms/Wormhole/WormholeIsm.sol b/solidity/contracts/isms/Wormhole/WormholeIsm.sol index 7d089cf102..120e9eeb49 100644 --- a/solidity/contracts/isms/Wormhole/WormholeIsm.sol +++ b/solidity/contracts/isms/Wormhole/WormholeIsm.sol @@ -62,7 +62,7 @@ contract WormholeIsm is IInterchainSecurityModule, Ownable { * @notice Returns an enum that represents the type of hook */ function moduleType() external pure returns (uint8) { - return uint8(IInterchainSecurityModule.Types.WORMHOLE); + return uint8(IInterchainSecurityModule.Types.NULL); } /**