diff --git a/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol index 41dd3c129..d61ae991d 100644 --- a/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol +++ b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol @@ -35,14 +35,18 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook { payable override { - IPostDispatchHook configuredHook = customHooks[message.senderAddress()][ - message.destination() - ][message.recipient()]; - if (address(configuredHook) == address(0)) { - configuredHook = mailbox.defaultHook(); - } + _getConfiguredHook(message).postDispatch{value: msg.value}( + metadata, + message + ); + } - configuredHook.postDispatch{value: msg.value}(metadata, message); + function quoteDispatch(bytes calldata metadata, bytes calldata message) + public + view + returns (uint256) + { + return _getConfiguredHook(message).quoteDispatch(metadata, message); } function setHook( @@ -52,4 +56,20 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook { ) external { customHooks[msg.sender][destinationDomain][recipient] = hook; } + + // ============ Internal Functions ============ + + function _getConfiguredHook(bytes calldata message) + internal + view + returns (IPostDispatchHook) + { + IPostDispatchHook configuredHook = customHooks[message.senderAddress()][ + message.destination() + ][message.recipient()]; + if (address(configuredHook) == address(0)) { + configuredHook = mailbox.defaultHook(); + } + return configuredHook; + } } diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol index c74cebb73..54f558dc5 100644 --- a/solidity/contracts/hooks/DomainRoutingHook.sol +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -38,9 +38,29 @@ contract DomainRoutingHook is IPostDispatchHook, Ownable { virtual override { - hooks[message.destination()].postDispatch{value: msg.value}( + _getConfiguredHook(message).postDispatch{value: msg.value}( metadata, message ); } + + function quoteDispatch(bytes calldata metadata, bytes calldata message) + public + view + virtual + override + returns (uint256) + { + return _getConfiguredHook(message).quoteDispatch(metadata, message); + } + + // ============ Internal Functions ============ + + function _getConfiguredHook(bytes calldata message) + internal + view + returns (IPostDispatchHook) + { + return hooks[message.destination()]; + } } diff --git a/solidity/contracts/hooks/ERC5164Hook.sol b/solidity/contracts/hooks/ERC5164Hook.sol index b401140d2..f85348766 100644 --- a/solidity/contracts/hooks/ERC5164Hook.sol +++ b/solidity/contracts/hooks/ERC5164Hook.sol @@ -43,6 +43,15 @@ contract ERC5164Hook is AbstractMessageIdAuthHook { dispatcher = IMessageDispatcher(_dispatcher); } + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + revert("not implemented"); + } + function _sendMessageId( bytes calldata, /* metadata */ bytes memory payload diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index c4c61f385..bd301d1a4 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -43,4 +43,11 @@ contract MerkleTreeHook is IPostDispatchHook, MailboxClient { require(isLatestDispatched(id), "message not dispatching"); _tree.insert(id); } + + function quoteDispatch( + bytes calldata, /*metadata*/ + bytes calldata /*message*/ + ) external pure override returns (uint256) { + return 0; + } } diff --git a/solidity/contracts/hooks/OPStackHook.sol b/solidity/contracts/hooks/OPStackHook.sol index 7b4c036bc..4c8b82fa5 100644 --- a/solidity/contracts/hooks/OPStackHook.sol +++ b/solidity/contracts/hooks/OPStackHook.sol @@ -57,6 +57,18 @@ contract OPStackHook is AbstractMessageIdAuthHook { l1Messenger = ICrossDomainMessenger(_messenger); } + // ============ External functions ============ + + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + return 0; // gas subsidized by the L2 + } + // ============ Internal functions ============ /// @inheritdoc AbstractMessageIdAuthHook diff --git a/solidity/contracts/hooks/PausableHook.sol b/solidity/contracts/hooks/PausableHook.sol index 99835fa48..c28ad46d5 100644 --- a/solidity/contracts/hooks/PausableHook.sol +++ b/solidity/contracts/hooks/PausableHook.sol @@ -13,6 +13,16 @@ contract PausableHook is IPostDispatchHook, Ownable, Pausable { whenNotPaused {} + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + return 0; + } + function pause() external onlyOwner { _pause(); } diff --git a/solidity/contracts/hooks/StaticProtocolFee.sol b/solidity/contracts/hooks/StaticProtocolFee.sol index deb3a75af..75b5b0d2f 100644 --- a/solidity/contracts/hooks/StaticProtocolFee.sol +++ b/solidity/contracts/hooks/StaticProtocolFee.sol @@ -74,6 +74,16 @@ contract StaticProtocolFee is IPostDispatchHook, Ownable { if (refund > 0) payable(message.senderAddress()).sendValue(refund); } + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + view + override + returns (uint256) + { + return protocolFee; + } + /** * @notice Sets the protocol fee. * @param _protocolFee The new protocol fee. diff --git a/solidity/contracts/igps/InterchainGasPaymaster.sol b/solidity/contracts/igps/InterchainGasPaymaster.sol index 01979dd19..4a4544e61 100644 --- a/solidity/contracts/igps/InterchainGasPaymaster.sol +++ b/solidity/contracts/igps/InterchainGasPaymaster.sol @@ -1,6 +1,18 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + // ============ Internal Imports ============ import {Message} from "../libs/Message.sol"; import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol"; @@ -86,20 +98,26 @@ contract InterchainGasPaymaster is payable override { - uint256 gasLimit; - address refundAddress; - if (metadata.length == 0) { - gasLimit = DEFAULT_GAS_USAGE; - refundAddress = message.senderAddress(); - } else { - gasLimit = metadata.gasLimit(); - refundAddress = metadata.refundAddress(); - if (refundAddress == address(0)) - refundAddress = message.senderAddress(); - } + uint256 gasLimit = metadata.gasLimit(DEFAULT_GAS_USAGE); + address refundAddress = metadata.refundAddress(message.senderAddress()); payForGas(message.id(), message.destination(), gasLimit, refundAddress); } + /** + * @notice Quote gas payment for a hook call. + * @param metadata The metadata as gasConfig. + * @param message The message to pay for. + */ + function quoteDispatch(bytes calldata metadata, bytes calldata message) + external + view + override + returns (uint256) + { + uint256 gasLimit = metadata.gasLimit(DEFAULT_GAS_USAGE); + return quoteGasPayment(message.destination(), gasLimit); + } + /** * @notice Transfers the entire native token balance to the beneficiary. * @dev The beneficiary must be able to receive native tokens. diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 737268a48..7b86e9892 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -2,7 +2,23 @@ pragma solidity >=0.8.0; interface IPostDispatchHook { + /** + * @notice Post action afte a message is dispatched via the Mailbox + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + */ function postDispatch(bytes calldata metadata, bytes calldata message) external payable; + + /** + * @notice Estimate the amount of gas consumed by the postDispatch call + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + * @return Gas quote for the postDispatch call + */ + function quoteDispatch(bytes calldata metadata, bytes calldata message) + external + view + returns (uint256); } diff --git a/solidity/contracts/libs/hooks/IGPMetadata.sol b/solidity/contracts/libs/hooks/IGPMetadata.sol index 0bac899f9..24ad99e94 100644 --- a/solidity/contracts/libs/hooks/IGPMetadata.sol +++ b/solidity/contracts/libs/hooks/IGPMetadata.sol @@ -28,11 +28,12 @@ library IGPMetadata { * @param _metadata ABI encoded IGP hook metadata. * @return Gas limit for the message as uint256. */ - function gasLimit(bytes calldata _metadata) + function gasLimit(bytes calldata _metadata, uint256 _default) internal pure returns (uint256) { + if (_metadata.length < GAS_LIMIT_OFFSET + 32) return _default; return uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32])); } @@ -42,17 +43,23 @@ library IGPMetadata { * @param _metadata ABI encoded IGP hook metadata. * @return Refund address for the message as address. */ - function refundAddress(bytes calldata _metadata) + function refundAddress(bytes calldata _metadata, address _default) internal pure returns (address) { - return - address( + address _refundAddress; + if (_metadata.length < REFUND_ADDRESS_OFFSET + 20) { + _refundAddress = _default; + } else { + _refundAddress = address( bytes20( _metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20] ) ); + if (_refundAddress == address(0)) _refundAddress = _default; + } + return _refundAddress; } /** diff --git a/solidity/contracts/test/TestPostDispatchHook.sol b/solidity/contracts/test/TestPostDispatchHook.sol index 82febe3cc..019cd8abf 100644 --- a/solidity/contracts/test/TestPostDispatchHook.sol +++ b/solidity/contracts/test/TestPostDispatchHook.sol @@ -4,13 +4,19 @@ pragma solidity >=0.8.0; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; contract TestPostDispatchHook is IPostDispatchHook { - event PostDispatchHookCalled(); + uint256 public mockGasQuote = 25000; function postDispatch( bytes calldata, /*metadata*/ bytes calldata /*message*/ ) external payable override { - // test - emit event - emit PostDispatchHookCalled(); + // test - empty + } + + function quoteDispatch( + bytes calldata, /*metadata*/ + bytes calldata /*message*/ + ) external view override returns (uint256) { + return mockGasQuote; } } diff --git a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol index 1e87e6518..c1a1301ac 100644 --- a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol +++ b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol @@ -34,22 +34,51 @@ contract FallbackDomainRoutingHookTest is Test { mailbox.setDefaultHook(address(mailboxDefaultHook)); } - function test_postDispatchHook_configured() public payable { + /* ============ hook.quoteDispatch ============ */ + + function test_quoteDispatchHook_configured() public { fallbackHook.setHook( TEST_DESTINATION_DOMAIN, address(testRecipient).addressToBytes32(), configuredTestHook ); - vm.expectEmit(false, false, false, false, address(configuredTestHook)); - emit PostDispatchHookCalled(); + vm.expectCall( + address(configuredTestHook), + abi.encodeCall(configuredTestHook.quoteDispatch, ("", testMessage)) + ); + assertEq(fallbackHook.quoteDispatch("", testMessage), 25000); + } + function test_quoteDispatch_default() public payable { + vm.expectCall( + address(mailboxDefaultHook), + abi.encodeCall(mailboxDefaultHook.quoteDispatch, ("", testMessage)) + ); + fallbackHook.quoteDispatch("", testMessage); + } + + /* ============ hook.postDispatch ============ */ + + function test_postDispatchHook_configured() public payable { + fallbackHook.setHook( + TEST_DESTINATION_DOMAIN, + address(testRecipient).addressToBytes32(), + configuredTestHook + ); + + vm.expectCall( + address(configuredTestHook), + abi.encodeCall(configuredTestHook.postDispatch, ("", testMessage)) + ); fallbackHook.postDispatch{value: msg.value}("", testMessage); } function test_postDispatch_default() public payable { - vm.expectEmit(false, false, false, false, address(mailboxDefaultHook)); - emit PostDispatchHookCalled(); + vm.expectCall( + address(mailboxDefaultHook), + abi.encodeCall(mailboxDefaultHook.postDispatch, ("", testMessage)) + ); fallbackHook.postDispatch{value: msg.value}("", testMessage); } diff --git a/solidity/test/hooks/StaticProtocolFee.t.sol b/solidity/test/hooks/StaticProtocolFee.t.sol index 6428bf172..25d934715 100644 --- a/solidity/test/hooks/StaticProtocolFee.t.sol +++ b/solidity/test/hooks/StaticProtocolFee.t.sol @@ -67,6 +67,10 @@ contract StaticProtocolFeeTest is Test { assertEq(fees.beneficiary(), bob); } + function testQuoteDispatch() public { + assertEq(fees.quoteDispatch("", testMessage), 1e15); + } + function testFuzz_postDispatch_inusfficientFees( uint256 feeRequired, uint256 feeSent diff --git a/solidity/test/igps/InterchainGasPaymaster.t.sol b/solidity/test/igps/InterchainGasPaymaster.t.sol index 276a400d8..885398ac1 100644 --- a/solidity/test/igps/InterchainGasPaymaster.t.sol +++ b/solidity/test/igps/InterchainGasPaymaster.t.sol @@ -28,6 +28,7 @@ contract InterchainGasPaymasterTest is Test { bytes32 constant testMessageId = 0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f; address constant testRefundAddress = address(0xc0ffee); + bytes testEncodedMessage; event GasPayment( bytes32 indexed messageId, @@ -44,6 +45,8 @@ contract InterchainGasPaymasterTest is Test { igp.initialize(address(this), beneficiary); oracle = new StorageGasOracle(); setGasOracle(testDestinationDomain, address(oracle)); + + testEncodedMessage = _encodeTestMessage(); } // ============ constructor ============ @@ -59,6 +62,34 @@ contract InterchainGasPaymasterTest is Test { igp.initialize(address(this), beneficiary); } + // ============ quoteDispatch ============ + + function testQuoteDispatch_defaultGasLimit() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 150 // 1 wei gas price + ); + + // 150 * 69_420 = 10_413_000 + assertEq(igp.quoteDispatch("", testEncodedMessage), 10_413_000); + } + + function testQuoteDispatch_customWithMetadata() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 150 // 1 wei gas price + ); + + bytes memory metadata = IGPMetadata.formatMetadata( + uint256(testGasAmount), // gas limit + testRefundAddress // refund address + ); + // 150 * 300_000 = 45_000_000 + assertEq(igp.quoteDispatch(metadata, testEncodedMessage), 45_000_000); + } + // ============ postDispatch ============ function testPostDispatch_defaultGasLimit() public { @@ -73,9 +104,8 @@ contract InterchainGasPaymasterTest is Test { uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420); uint256 _overpayment = 21000; - bytes memory message = _encodeTestMessage(); - igp.postDispatch{value: _quote + _overpayment}("", message); + igp.postDispatch{value: _quote + _overpayment}("", testEncodedMessage); uint256 _igpBalanceAfter = address(igp).balance; uint256 _refundAddressBalanceAfter = address(this).balance; diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 882599c94..dae29a651 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -132,6 +132,16 @@ contract OPStackIsmTest is Test { /// FORK TESTS /// /////////////////////////////////////////////////////////////////// + /* ============ hook.quoteDispatch ============ */ + + function testFork_quoteDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0); + } + /* ============ hook.postDispatch ============ */ function testFork_postDispatch() public {