Add `quoteDispatch` to `IPostDispatchHook` (#2660)

- `quoteDispatch` added to `IPostDispatchHook` interface and the
relevant hooks:
     - StaticProtocolFee
     - OPStackHook
     - InterchainGasPaymaster
     - MerkleTreeHook
     - PausableHook (no tests)
     - DomainRoutingHook (no tests)
     - ConfigFallbackDomainRoutingHook
     - ConfigurableDomainRoutingHook (no tests)

- `expectEmit` -> `expectCall`

- Quote in V3

Yes

Uint tests
pull/2736/head
Kunal Arora 1 year ago committed by Yorke Rhodes
parent 6a32287f00
commit 2b7ecfc312
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 32
      solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol
  2. 22
      solidity/contracts/hooks/DomainRoutingHook.sol
  3. 9
      solidity/contracts/hooks/ERC5164Hook.sol
  4. 7
      solidity/contracts/hooks/MerkleTreeHook.sol
  5. 12
      solidity/contracts/hooks/OPStackHook.sol
  6. 10
      solidity/contracts/hooks/PausableHook.sol
  7. 10
      solidity/contracts/hooks/StaticProtocolFee.sol
  8. 40
      solidity/contracts/igps/InterchainGasPaymaster.sol
  9. 16
      solidity/contracts/interfaces/hooks/IPostDispatchHook.sol
  10. 15
      solidity/contracts/libs/hooks/IGPMetadata.sol
  11. 12
      solidity/contracts/test/TestPostDispatchHook.sol
  12. 39
      solidity/test/hooks/FallbackDomainRoutingHook.t.sol
  13. 4
      solidity/test/hooks/StaticProtocolFee.t.sol
  14. 34
      solidity/test/igps/InterchainGasPaymaster.t.sol
  15. 10
      solidity/test/isms/OPStackIsm.t.sol

@ -35,14 +35,18 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook {
payable payable
override override
{ {
IPostDispatchHook configuredHook = customHooks[message.senderAddress()][ _getConfiguredHook(message).postDispatch{value: msg.value}(
message.destination() metadata,
][message.recipient()]; message
if (address(configuredHook) == address(0)) { );
configuredHook = mailbox.defaultHook();
} }
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( function setHook(
@ -52,4 +56,20 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook {
) external { ) external {
customHooks[msg.sender][destinationDomain][recipient] = hook; 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;
}
} }

@ -38,9 +38,29 @@ contract DomainRoutingHook is IPostDispatchHook, Ownable {
virtual virtual
override override
{ {
hooks[message.destination()].postDispatch{value: msg.value}( _getConfiguredHook(message).postDispatch{value: msg.value}(
metadata, metadata,
message 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()];
}
} }

@ -43,6 +43,15 @@ contract ERC5164Hook is AbstractMessageIdAuthHook {
dispatcher = IMessageDispatcher(_dispatcher); dispatcher = IMessageDispatcher(_dispatcher);
} }
function quoteDispatch(bytes calldata, bytes calldata)
external
pure
override
returns (uint256)
{
revert("not implemented");
}
function _sendMessageId( function _sendMessageId(
bytes calldata, /* metadata */ bytes calldata, /* metadata */
bytes memory payload bytes memory payload

@ -43,4 +43,11 @@ contract MerkleTreeHook is IPostDispatchHook, MailboxClient {
require(isLatestDispatched(id), "message not dispatching"); require(isLatestDispatched(id), "message not dispatching");
_tree.insert(id); _tree.insert(id);
} }
function quoteDispatch(
bytes calldata, /*metadata*/
bytes calldata /*message*/
) external pure override returns (uint256) {
return 0;
}
} }

@ -57,6 +57,18 @@ contract OPStackHook is AbstractMessageIdAuthHook {
l1Messenger = ICrossDomainMessenger(_messenger); 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 ============ // ============ Internal functions ============
/// @inheritdoc AbstractMessageIdAuthHook /// @inheritdoc AbstractMessageIdAuthHook

@ -13,6 +13,16 @@ contract PausableHook is IPostDispatchHook, Ownable, Pausable {
whenNotPaused whenNotPaused
{} {}
/// @inheritdoc IPostDispatchHook
function quoteDispatch(bytes calldata, bytes calldata)
external
pure
override
returns (uint256)
{
return 0;
}
function pause() external onlyOwner { function pause() external onlyOwner {
_pause(); _pause();
} }

@ -74,6 +74,16 @@ contract StaticProtocolFee is IPostDispatchHook, Ownable {
if (refund > 0) payable(message.senderAddress()).sendValue(refund); 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. * @notice Sets the protocol fee.
* @param _protocolFee The new protocol fee. * @param _protocolFee The new protocol fee.

@ -1,6 +1,18 @@
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0; pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============ // ============ Internal Imports ============
import {Message} from "../libs/Message.sol"; import {Message} from "../libs/Message.sol";
import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol"; import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol";
@ -86,20 +98,26 @@ contract InterchainGasPaymaster is
payable payable
override override
{ {
uint256 gasLimit; uint256 gasLimit = metadata.gasLimit(DEFAULT_GAS_USAGE);
address refundAddress; address refundAddress = metadata.refundAddress(message.senderAddress());
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();
}
payForGas(message.id(), message.destination(), gasLimit, refundAddress); 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. * @notice Transfers the entire native token balance to the beneficiary.
* @dev The beneficiary must be able to receive native tokens. * @dev The beneficiary must be able to receive native tokens.

@ -2,7 +2,23 @@
pragma solidity >=0.8.0; pragma solidity >=0.8.0;
interface IPostDispatchHook { 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) function postDispatch(bytes calldata metadata, bytes calldata message)
external external
payable; 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);
} }

@ -28,11 +28,12 @@ library IGPMetadata {
* @param _metadata ABI encoded IGP hook metadata. * @param _metadata ABI encoded IGP hook metadata.
* @return Gas limit for the message as uint256. * @return Gas limit for the message as uint256.
*/ */
function gasLimit(bytes calldata _metadata) function gasLimit(bytes calldata _metadata, uint256 _default)
internal internal
pure pure
returns (uint256) returns (uint256)
{ {
if (_metadata.length < GAS_LIMIT_OFFSET + 32) return _default;
return return
uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32])); uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32]));
} }
@ -42,17 +43,23 @@ library IGPMetadata {
* @param _metadata ABI encoded IGP hook metadata. * @param _metadata ABI encoded IGP hook metadata.
* @return Refund address for the message as address. * @return Refund address for the message as address.
*/ */
function refundAddress(bytes calldata _metadata) function refundAddress(bytes calldata _metadata, address _default)
internal internal
pure pure
returns (address) returns (address)
{ {
return address _refundAddress;
address( if (_metadata.length < REFUND_ADDRESS_OFFSET + 20) {
_refundAddress = _default;
} else {
_refundAddress = address(
bytes20( bytes20(
_metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20] _metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20]
) )
); );
if (_refundAddress == address(0)) _refundAddress = _default;
}
return _refundAddress;
} }
/** /**

@ -4,13 +4,19 @@ pragma solidity >=0.8.0;
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
contract TestPostDispatchHook is IPostDispatchHook { contract TestPostDispatchHook is IPostDispatchHook {
event PostDispatchHookCalled(); uint256 public mockGasQuote = 25000;
function postDispatch( function postDispatch(
bytes calldata, /*metadata*/ bytes calldata, /*metadata*/
bytes calldata /*message*/ bytes calldata /*message*/
) external payable override { ) external payable override {
// test - emit event // test - empty
emit PostDispatchHookCalled(); }
function quoteDispatch(
bytes calldata, /*metadata*/
bytes calldata /*message*/
) external view override returns (uint256) {
return mockGasQuote;
} }
} }

@ -34,22 +34,51 @@ contract FallbackDomainRoutingHookTest is Test {
mailbox.setDefaultHook(address(mailboxDefaultHook)); mailbox.setDefaultHook(address(mailboxDefaultHook));
} }
function test_postDispatchHook_configured() public payable { /* ============ hook.quoteDispatch ============ */
function test_quoteDispatchHook_configured() public {
fallbackHook.setHook( fallbackHook.setHook(
TEST_DESTINATION_DOMAIN, TEST_DESTINATION_DOMAIN,
address(testRecipient).addressToBytes32(), address(testRecipient).addressToBytes32(),
configuredTestHook configuredTestHook
); );
vm.expectEmit(false, false, false, false, address(configuredTestHook)); vm.expectCall(
emit PostDispatchHookCalled(); 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); fallbackHook.postDispatch{value: msg.value}("", testMessage);
} }
function test_postDispatch_default() public payable { function test_postDispatch_default() public payable {
vm.expectEmit(false, false, false, false, address(mailboxDefaultHook)); vm.expectCall(
emit PostDispatchHookCalled(); address(mailboxDefaultHook),
abi.encodeCall(mailboxDefaultHook.postDispatch, ("", testMessage))
);
fallbackHook.postDispatch{value: msg.value}("", testMessage); fallbackHook.postDispatch{value: msg.value}("", testMessage);
} }

@ -67,6 +67,10 @@ contract StaticProtocolFeeTest is Test {
assertEq(fees.beneficiary(), bob); assertEq(fees.beneficiary(), bob);
} }
function testQuoteDispatch() public {
assertEq(fees.quoteDispatch("", testMessage), 1e15);
}
function testFuzz_postDispatch_inusfficientFees( function testFuzz_postDispatch_inusfficientFees(
uint256 feeRequired, uint256 feeRequired,
uint256 feeSent uint256 feeSent

@ -28,6 +28,7 @@ contract InterchainGasPaymasterTest is Test {
bytes32 constant testMessageId = bytes32 constant testMessageId =
0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f; 0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f;
address constant testRefundAddress = address(0xc0ffee); address constant testRefundAddress = address(0xc0ffee);
bytes testEncodedMessage;
event GasPayment( event GasPayment(
bytes32 indexed messageId, bytes32 indexed messageId,
@ -44,6 +45,8 @@ contract InterchainGasPaymasterTest is Test {
igp.initialize(address(this), beneficiary); igp.initialize(address(this), beneficiary);
oracle = new StorageGasOracle(); oracle = new StorageGasOracle();
setGasOracle(testDestinationDomain, address(oracle)); setGasOracle(testDestinationDomain, address(oracle));
testEncodedMessage = _encodeTestMessage();
} }
// ============ constructor ============ // ============ constructor ============
@ -59,6 +62,34 @@ contract InterchainGasPaymasterTest is Test {
igp.initialize(address(this), beneficiary); 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 ============ // ============ postDispatch ============
function testPostDispatch_defaultGasLimit() public { function testPostDispatch_defaultGasLimit() public {
@ -73,9 +104,8 @@ contract InterchainGasPaymasterTest is Test {
uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420); uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420);
uint256 _overpayment = 21000; 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 _igpBalanceAfter = address(igp).balance;
uint256 _refundAddressBalanceAfter = address(this).balance; uint256 _refundAddressBalanceAfter = address(this).balance;

@ -132,6 +132,16 @@ contract OPStackIsmTest is Test {
/// FORK TESTS /// /// FORK TESTS ///
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
/* ============ hook.quoteDispatch ============ */
function testFork_quoteDispatch() public {
deployAll();
vm.selectFork(mainnetFork);
assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0);
}
/* ============ hook.postDispatch ============ */ /* ============ hook.postDispatch ============ */
function testFork_postDispatch() public { function testFork_postDispatch() public {

Loading…
Cancel
Save