Modifying IGP to be a hook (#2638)

- IGP as a standalone hook, implementing postDispatch to call payForGas
directly
- Setting a DEFAULT_GAS_USAGE if metadata not specified and
message.senderAddress() as refund address if not specified.

- None

Fixes https://github.com/hyperlane-xyz/issues/issues/511

Yes, same interface as the previous IGP but for Mailbox V3

Unit Tests

---------

Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
pull/2736/head
Kunal Arora 1 year ago committed by Yorke Rhodes
parent 0e10306d4b
commit f38660e70a
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 2
      solidity/contracts/Mailbox.sol
  2. 6
      solidity/contracts/hooks/AbstractMessageIdAuthHook.sol
  3. 60
      solidity/contracts/hooks/DefaultHook.sol
  4. 2
      solidity/contracts/hooks/DomainRoutingHook.sol
  5. 89
      solidity/contracts/igps/InterchainGasPaymaster.sol
  6. 71
      solidity/contracts/libs/hooks/IGPMetadata.sol
  7. 4
      solidity/contracts/test/TestMailbox.sol
  8. 86
      solidity/test/igps/InterchainGasPaymaster.t.sol

@ -30,8 +30,6 @@ contract Mailbox is IMailbox, Versioned, Ownable {
// A monotonically increasing nonce for outbound unique message IDs.
uint32 public nonce;
// The latest dispatched message ID used for auth in post-dispatch hooks.
bytes32 public latestDispatchedId;
// The default ISM, used if the recipient fails to specify one.

@ -21,14 +21,10 @@ import {OPStackHookMetadata} from "../libs/hooks/OPStackHookMetadata.sol";
import {MailboxClient} from "../client/MailboxClient.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title AbstractMessageIdAuthHook
* @notice Message hook to inform an Abstract Message ID ISM of messages published through
* the native OPStack bridge.
* a third-party bridge.
* @dev V3 WIP
*/
abstract contract AbstractMessageIdAuthHook is

@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {DomainRoutingHook} from "./DomainRoutingHook.sol";
contract ConfigurableDomainRoutingHook is DomainRoutingHook {
using Message for bytes;
/// @notice mapping of destination domain and recipient to custom hook
mapping(bytes32 => address) public customHooks;
constructor(address mailbox, address owner) DomainRoutingHook(owner) {}
function postDispatch(bytes calldata metadata, bytes calldata message)
public
payable
override
{
bytes32 hookKey = keccak256(
abi.encodePacked(message.destination(), message.recipient())
);
address customHookPreset = customHooks[hookKey];
if (customHookPreset != address(0)) {
IPostDispatchHook(customHookPreset).postDispatch{value: msg.value}(
metadata,
message
);
} else {
super.postDispatch(metadata, message);
}
}
// TODO: need to restrict sender
function configCustomHook(
uint32 destinationDomain,
bytes32 recipient,
address hook
) external {
bytes32 hookKey = keccak256(
abi.encodePacked(destinationDomain, recipient)
);
require(customHooks[hookKey] == address(0), "hook already set");
customHooks[hookKey] = hook;
}
}

@ -33,7 +33,7 @@ contract DomainRoutingHook is IPostDispatchHook, Ownable {
}
function postDispatch(bytes calldata metadata, bytes calldata message)
external
public
payable
virtual
override

@ -2,10 +2,14 @@
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {Message} from "../libs/Message.sol";
import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol";
import {IGasOracle} from "../interfaces/IGasOracle.sol";
import {IInterchainGasPaymaster} from "../interfaces/IInterchainGasPaymaster.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/**
@ -15,13 +19,19 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own
*/
contract InterchainGasPaymaster is
IInterchainGasPaymaster,
IPostDispatchHook,
IGasOracle,
OwnableUpgradeable
{
using Address for address payable;
using Message for bytes;
using IGPMetadata for bytes;
// ============ Constants ============
/// @notice The scale of gas oracle token exchange rates.
uint256 internal constant TOKEN_EXCHANGE_RATE_SCALE = 1e10;
/// @notice default for user call if metadata not provided
uint256 internal immutable DEFAULT_GAS_USAGE = 69_420;
// ============ Public Storage ============
@ -67,36 +77,27 @@ contract InterchainGasPaymaster is
}
/**
* @notice Deposits msg.value as a payment for the relaying of a message
* to its destination chain.
* @dev Overpayment will result in a refund of native tokens to the _refundAddress.
* Callers should be aware that this may present reentrancy issues.
* @param _messageId The ID of the message to pay for.
* @param _destinationDomain The domain of the message's destination chain.
* @param _gasAmount The amount of destination gas to pay for.
* @param _refundAddress The address to refund any overpayment to.
* @notice pay for gas as a hook
* @param metadata The metadata as gasConfig.
* @param message The message to pay for.
*/
function payForGas(
bytes32 _messageId,
uint32 _destinationDomain,
uint256 _gasAmount,
address _refundAddress
) external payable override {
uint256 _requiredPayment = quoteGasPayment(
_destinationDomain,
_gasAmount
);
require(
msg.value >= _requiredPayment,
"insufficient interchain gas payment"
);
uint256 _overpayment = msg.value - _requiredPayment;
if (_overpayment > 0) {
(bool _success, ) = _refundAddress.call{value: _overpayment}("");
require(_success, "Interchain gas payment refund failed");
function postDispatch(bytes calldata metadata, bytes calldata message)
external
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();
}
emit GasPayment(_messageId, _gasAmount, _requiredPayment);
payForGas(message.id(), message.destination(), gasLimit, refundAddress);
}
/**
@ -133,6 +134,38 @@ contract InterchainGasPaymaster is
// ============ Public Functions ============
/**
* @notice Deposits msg.value as a payment for the relaying of a message
* to its destination chain.
* @dev Overpayment will result in a refund of native tokens to the _refundAddress.
* Callers should be aware that this may present reentrancy issues.
* @param _messageId The ID of the message to pay for.
* @param _destinationDomain The domain of the message's destination chain.
* @param _gasAmount The amount of destination gas to pay for.
* @param _refundAddress The address to refund any overpayment to.
*/
function payForGas(
bytes32 _messageId,
uint32 _destinationDomain,
uint256 _gasAmount,
address _refundAddress
) public payable override {
uint256 _requiredPayment = quoteGasPayment(
_destinationDomain,
_gasAmount
);
require(
msg.value >= _requiredPayment,
"insufficient interchain gas payment"
);
uint256 _overpayment = msg.value - _requiredPayment;
if (_overpayment > 0) {
payable(_refundAddress).sendValue(_overpayment);
}
emit GasPayment(_messageId, _gasAmount, _requiredPayment);
}
/**
* @notice Quotes the amount of native tokens to pay for interchain gas.
* @param _destinationDomain The domain of the message's destination chain.

@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
/**
* Format of metadata:
*
* [0:32] Gas limit for message
* [32:52] Refund address for message
*/
library IGPMetadata {
uint8 private constant GAS_LIMIT_OFFSET = 0;
uint8 private constant REFUND_ADDRESS_OFFSET = 32;
/**
* @notice Returns the specified gas limit for the message.
* @param _metadata ABI encoded IGP hook metadata.
* @return Gas limit for the message as uint256.
*/
function gasLimit(bytes calldata _metadata)
internal
pure
returns (uint256)
{
return
uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32]));
}
/**
* @notice Returns the specified refund address for the message.
* @param _metadata ABI encoded IGP hook metadata.
* @return Refund address for the message as address.
*/
function refundAddress(bytes calldata _metadata)
internal
pure
returns (address)
{
return
address(
bytes20(
_metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20]
)
);
}
/**
* @notice Formats the specified gas limit and refund address into IGP hook metadata.
* @param _gasLimit Gas limit for the message.
* @param _refundAddress Refund address for the message.
* @return ABI encoded IGP hook metadata.
*/
function formatMetadata(uint256 _gasLimit, address _refundAddress)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(bytes32(_gasLimit), bytes20(_refundAddress));
}
}

@ -39,4 +39,8 @@ contract TestMailbox is Mailbox {
_body
);
}
function updateLatestDispatchedId(bytes32 _id) external {
latestDispatchedId = _id;
}
}

@ -2,18 +2,29 @@
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {IGPMetadata} from "../../contracts/libs/hooks/IGPMetadata.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "../isms/IsmTestUtils.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {InterchainGasPaymaster} from "../../contracts/igps/InterchainGasPaymaster.sol";
import {StorageGasOracle} from "../../contracts/igps/gas-oracles/StorageGasOracle.sol";
import {IGasOracle} from "../../contracts/interfaces/IGasOracle.sol";
contract InterchainGasPaymasterTest is Test {
using IGPMetadata for bytes;
using TypeCasts for address;
using MessageUtils for bytes;
InterchainGasPaymaster igp;
StorageGasOracle oracle;
address constant beneficiary = address(0x444444);
uint32 constant testOriginDomain = 22222;
uint32 constant testDestinationDomain = 11111;
uint256 constant testGasAmount = 300000;
bytes constant testMessage = "hello world";
bytes32 constant testMessageId =
0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f;
address constant testRefundAddress = address(0xc0ffee);
@ -48,6 +59,66 @@ contract InterchainGasPaymasterTest is Test {
igp.initialize(address(this), beneficiary);
}
// ============ postDispatch ============
function testPostDispatch_defaultGasLimit() public {
setRemoteGasData(
testDestinationDomain,
1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local)
1 // 1 wei gas price
);
uint256 _igpBalanceBefore = address(igp).balance;
uint256 _refundAddressBalanceBefore = address(this).balance;
uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420);
uint256 _overpayment = 21000;
bytes memory message = _encodeTestMessage();
igp.postDispatch{value: _quote + _overpayment}("", message);
uint256 _igpBalanceAfter = address(igp).balance;
uint256 _refundAddressBalanceAfter = address(this).balance;
assertEq(_igpBalanceAfter - _igpBalanceBefore, _quote);
assertEq(
_refundAddressBalanceBefore - _refundAddressBalanceAfter,
_quote
);
}
function testPostDispatch_customWithMetadata() public {
setRemoteGasData(
testDestinationDomain,
1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local)
1 // 1 wei gas price
);
uint256 _igpBalanceBefore = address(igp).balance;
uint256 _refundAddressBalanceBefore = testRefundAddress.balance;
uint256 _quote = igp.quoteGasPayment(
testDestinationDomain,
testGasAmount
);
uint256 _overpayment = 25000;
bytes memory metadata = IGPMetadata.formatMetadata(
uint256(testGasAmount), // gas limit
testRefundAddress // refund address
);
bytes memory message = _encodeTestMessage();
igp.postDispatch{value: _quote + _overpayment}(metadata, message);
uint256 _igpBalanceAfter = address(igp).balance;
uint256 _refundAddressBalanceAfter = testRefundAddress.balance;
assertEq(_igpBalanceAfter - _igpBalanceBefore, _quote);
assertEq(
_refundAddressBalanceAfter - _refundAddressBalanceBefore,
_overpayment
);
}
// ============ payForGas ============
function testPayForGas() public {
@ -290,4 +361,19 @@ contract InterchainGasPaymasterTest is Test {
})
);
}
function _encodeTestMessage() internal view returns (bytes memory) {
return
MessageUtils.formatMessage(
uint8(0),
uint32(0),
testOriginDomain,
TypeCasts.addressToBytes32(address(this)),
testDestinationDomain,
TypeCasts.addressToBytes32(address(0x1)),
testMessage
);
}
receive() external payable {}
}

Loading…
Cancel
Save