fix(contracts): quote management for L2->L1 hooks (#4552)

### Description

- check for sufficient fees in `AbstractMessageIdAuthHook` and refund
surplus
- add a child hook to OPL2ToL1Hook and ArbL2ToL1Hook to use the igp to
pay for the destination gas fees


~~Note: LayerzeroL2Hook currently also refunds from msg.value, will make
it into issue to be fixed later as we're using the layerzero hooks right
now.~~

### Drive-by changes

- None

### Related issues

- fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/10

### Backward compatibility

No

### Testing

Fuzz
pull/4792/head
Kunal Arora 3 weeks ago committed by GitHub
parent 1c0ef45c35
commit 469f2f3403
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/cuddly-baboons-drive.md
  2. 35
      solidity/contracts/hooks/ArbL2ToL1Hook.sol
  3. 35
      solidity/contracts/hooks/OPL2ToL1Hook.sol
  4. 15
      solidity/contracts/hooks/OPStackHook.sol
  5. 13
      solidity/contracts/hooks/PolygonPosHook.sol
  6. 11
      solidity/contracts/hooks/aggregation/ERC5164Hook.sol
  7. 14
      solidity/contracts/hooks/layer-zero/LayerZeroV2Hook.sol
  8. 30
      solidity/contracts/hooks/libs/AbstractMessageIdAuthHook.sol
  9. 84
      solidity/script/DeployArbHook.s.sol
  10. 10
      solidity/test/hooks/layerzero/LayerZeroV2Hook.t.sol
  11. 20
      solidity/test/isms/ArbL2ToL1Ism.t.sol
  12. 2
      solidity/test/isms/ERC5164ISM.t.sol
  13. 45
      solidity/test/isms/ExternalBridgeTest.sol
  14. 23
      solidity/test/isms/OPL2ToL1Ism.t.sol
  15. 4
      typescript/sdk/src/hook/EvmHookModule.ts
  16. 3
      typescript/sdk/src/hook/EvmHookReader.ts
  17. 1
      typescript/sdk/src/hook/schemas.ts
  18. 25
      typescript/sdk/src/ism/metadata/arbL2ToL1.hardhat-test.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/sdk': minor
'@hyperlane-xyz/core': minor
---
Checking for sufficient fees in `AbstractMessageIdAuthHook` and refund surplus

@ -14,17 +14,16 @@ pragma solidity >=0.8.0;
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {AbstractPostDispatchHook} from "./libs/AbstractMessageIdAuthHook.sol";
import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol";
import {Mailbox} from "../Mailbox.sol";
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol";
import {Message} from "../libs/Message.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {MailboxClient} from "../client/MailboxClient.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
/**
@ -35,13 +34,14 @@ import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
*/
contract ArbL2ToL1Hook is AbstractMessageIdAuthHook {
using StandardHookMetadata for bytes;
using Message for bytes;
// ============ Constants ============
// precompile contract on L2 for sending messages to L1
ArbSys public immutable arbSys;
// Immutable quote amount
uint256 public immutable GAS_QUOTE;
// child hook to call first
IPostDispatchHook public immutable childHook;
// ============ Constructor ============
@ -50,21 +50,24 @@ contract ArbL2ToL1Hook is AbstractMessageIdAuthHook {
uint32 _destinationDomain,
bytes32 _ism,
address _arbSys,
uint256 _gasQuote
address _childHook
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
arbSys = ArbSys(_arbSys);
GAS_QUOTE = _gasQuote;
childHook = AbstractPostDispatchHook(_childHook);
}
/// @inheritdoc IPostDispatchHook
function hookType() external pure override returns (uint8) {
return uint8(IPostDispatchHook.Types.ARB_L2_TO_L1);
}
/// @inheritdoc AbstractPostDispatchHook
function _quoteDispatch(
bytes calldata,
bytes calldata
bytes calldata metadata,
bytes calldata message
) internal view override returns (uint256) {
return GAS_QUOTE;
return
metadata.msgValue(0) + childHook.quoteDispatch(metadata, message);
}
// ============ Internal functions ============
@ -72,8 +75,16 @@ contract ArbL2ToL1Hook is AbstractMessageIdAuthHook {
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal override {
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
childHook.postDispatch{
value: childHook.quoteDispatch(metadata, message)
}(metadata, message);
arbSys.sendTxToL1{value: metadata.msgValue(0)}(
TypeCasts.bytes32ToAddress(ism),
payload

@ -14,10 +14,13 @@ pragma solidity >=0.8.0;
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {Message} from "../libs/Message.sol";
import {AbstractPostDispatchHook, AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {InterchainGasPaymaster} from "./igp/InterchainGasPaymaster.sol";
// ============ External Imports ============
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol";
@ -30,13 +33,16 @@ import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenge
*/
contract OPL2ToL1Hook is AbstractMessageIdAuthHook {
using StandardHookMetadata for bytes;
using Message for bytes;
// ============ Constants ============
// precompile contract on L2 for sending messages to L1
ICrossDomainMessenger public immutable l2Messenger;
// Immutable quote amount
uint32 public immutable GAS_QUOTE;
// child hook to call first
IPostDispatchHook public immutable childHook;
// Minimum gas limit that the message can be executed with - OP specific
uint32 public constant MIN_GAS_LIMIT = 300_000;
// ============ Constructor ============
@ -45,10 +51,10 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook {
uint32 _destinationDomain,
bytes32 _ism,
address _l2Messenger,
uint32 _gasQuote
address _childHook
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
GAS_QUOTE = _gasQuote;
l2Messenger = ICrossDomainMessenger(_l2Messenger);
childHook = AbstractPostDispatchHook(_childHook);
}
/// @inheritdoc IPostDispatchHook
@ -58,10 +64,11 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook {
/// @inheritdoc AbstractPostDispatchHook
function _quoteDispatch(
bytes calldata,
bytes calldata
bytes calldata metadata,
bytes calldata message
) internal view override returns (uint256) {
return GAS_QUOTE;
return
metadata.msgValue(0) + childHook.quoteDispatch(metadata, message);
}
// ============ Internal functions ============
@ -69,16 +76,20 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook {
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal override {
require(
msg.value >= metadata.msgValue(0) + GAS_QUOTE,
"OPL2ToL1Hook: insufficient msg.value"
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
childHook.postDispatch{
value: childHook.quoteDispatch(metadata, message)
}(metadata, message);
l2Messenger.sendMessage{value: metadata.msgValue(0)}(
TypeCasts.bytes32ToAddress(ism),
payload,
GAS_QUOTE
MIN_GAS_LIMIT
);
}
}

@ -18,6 +18,7 @@ import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol";
import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {Message} from "../libs/Message.sol";
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
@ -32,6 +33,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
*/
contract OPStackHook is AbstractMessageIdAuthHook {
using StandardHookMetadata for bytes;
using Message for bytes;
// ============ Constants ============
@ -60,21 +62,22 @@ contract OPStackHook is AbstractMessageIdAuthHook {
// ============ Internal functions ============
function _quoteDispatch(
bytes calldata,
bytes calldata metadata,
bytes calldata
) internal pure override returns (uint256) {
return 0; // gas subsidized by the L2
return metadata.msgValue(0); // gas subsidized by the L2
}
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal override {
require(
metadata.msgValue(0) < 2 ** 255,
"OPStackHook: msgValue must be less than 2 ** 255"
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
l1Messenger.sendMessage{value: metadata.msgValue(0)}(
TypeCasts.bytes32ToAddress(ism),
payload,

@ -19,6 +19,7 @@ import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
// ============ External Imports ============
import {FxBaseRootTunnel} from "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol";
@ -31,6 +32,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
*/
contract PolygonPosHook is AbstractMessageIdAuthHook, FxBaseRootTunnel {
using StandardHookMetadata for bytes;
using Message for bytes;
// ============ Constructor ============
@ -56,22 +58,27 @@ contract PolygonPosHook is AbstractMessageIdAuthHook, FxBaseRootTunnel {
// ============ Internal functions ============
function _quoteDispatch(
bytes calldata,
bytes calldata metadata,
bytes calldata
) internal pure override returns (uint256) {
return 0;
return metadata.msgValue(0);
}
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal override {
require(
metadata.msgValue(0) == 0,
"PolygonPosHook: does not support msgValue"
);
require(msg.value == 0, "PolygonPosHook: does not support msgValue");
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
_sendMessageToChild(payload);
}

@ -15,9 +15,11 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {Message} from "../../libs/Message.sol";
import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol";
import {IMessageDispatcher} from "../../interfaces/hooks/IMessageDispatcher.sol";
import {AbstractMessageIdAuthHook} from "../libs/AbstractMessageIdAuthHook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
@ -28,6 +30,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
* any of the 5164 adapters.
*/
contract ERC5164Hook is AbstractMessageIdAuthHook {
using Message for bytes;
IMessageDispatcher public immutable dispatcher;
constructor(
@ -55,9 +59,14 @@ contract ERC5164Hook is AbstractMessageIdAuthHook {
function _sendMessageId(
bytes calldata,
/* metadata */
bytes memory payload
bytes calldata message
) internal override {
require(msg.value == 0, "ERC5164Hook: no value allowed");
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
dispatcher.dispatchMessage(
destinationDomain,
TypeCasts.bytes32ToAddress(ism),

@ -18,6 +18,7 @@ import {TypeCasts} from "../../libs/TypeCasts.sol";
import {Indexed} from "../../libs/Indexed.sol";
import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol";
import {AbstractMessageIdAuthHook} from "../libs/AbstractMessageIdAuthHook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol";
struct LayerZeroV2Metadata {
@ -55,8 +56,13 @@ contract LayerZeroV2Hook is AbstractMessageIdAuthHook {
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal override {
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
message.id()
);
bytes calldata lZMetadata = metadata.getCustomMetadata();
(
uint32 eid,
@ -72,7 +78,9 @@ contract LayerZeroV2Hook is AbstractMessageIdAuthHook {
options,
false // payInLzToken
);
lZEndpoint.send{value: msg.value}(msgParams, refundAddress);
uint256 quote = _quoteDispatch(metadata, message);
lZEndpoint.send{value: quote}(msgParams, refundAddress);
}
/// @dev payInZRO is hardcoded to false because zro tokens should not be directly accepted
@ -96,7 +104,7 @@ contract LayerZeroV2Hook is AbstractMessageIdAuthHook {
message.senderAddress()
);
return msgFee.nativeFee;
return metadata.msgValue(0) + msgFee.nativeFee;
}
/**

@ -22,6 +22,9 @@ import {Message} from "../../libs/Message.sol";
import {StandardHookMetadata} from "./StandardHookMetadata.sol";
import {MailboxClient} from "../../client/MailboxClient.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title AbstractMessageIdAuthHook
* @notice Message hook to inform an Abstract Message ID ISM of messages published through
@ -31,8 +34,10 @@ abstract contract AbstractMessageIdAuthHook is
AbstractPostDispatchHook,
MailboxClient
{
using Address for address payable;
using StandardHookMetadata for bytes;
using Message for bytes;
using TypeCasts for bytes32;
// ============ Constants ============
@ -68,7 +73,7 @@ abstract contract AbstractMessageIdAuthHook is
function _postDispatch(
bytes calldata metadata,
bytes calldata message
) internal override {
) internal virtual override {
bytes32 id = message.id();
require(
_isLatestDispatched(id),
@ -82,20 +87,29 @@ abstract contract AbstractMessageIdAuthHook is
metadata.msgValue(0) < 2 ** 255,
"AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255"
);
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
id
);
_sendMessageId(metadata, payload);
_sendMessageId(metadata, message);
uint256 _overpayment = msg.value - _quoteDispatch(metadata, message);
if (_overpayment > 0) {
address _refundAddress = metadata.refundAddress(
message.sender().bytes32ToAddress()
);
require(
_refundAddress != address(0),
"AbstractPostDispatchHook: no refund address"
);
payable(_refundAddress).sendValue(_overpayment);
}
}
/**
* @notice Send a message to the ISM.
* @param metadata The metadata for the hook caller
* @param payload The payload for call to the ISM
* @param message The message to send to the ISM
*/
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
bytes calldata message
) internal virtual;
}

@ -1,84 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Script.sol";
import {Mailbox} from "../../contracts/Mailbox.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol";
import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {TestIsm} from "../../contracts/test/TestIsm.sol";
contract DeployArbHook is Script {
uint256 deployerPrivateKey;
ArbL2ToL1Hook hook;
ArbL2ToL1Ism ism;
uint32 constant L1_DOMAIN = 11155111;
address constant L1_MAILBOX = 0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766;
address constant L1_BRIDGE = 0x38f918D0E9F1b721EDaA41302E399fa1B79333a9;
address constant L1_ISM = 0x096A1c034c7Ad113B6dB786b7BA852cB67025458; // placeholder
bytes32 TEST_RECIPIENT =
0x000000000000000000000000155b1cd2f7cbc58d403b9be341fab6cd77425175; // placeholder
address constant ARBSYS = 0x0000000000000000000000000000000000000064;
address constant L2_MAILBOX = 0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8;
address constant L2_HOOK = 0xd9d99AC1C645563576b8Df22cBebFC23FB60Ec73; // placeholder
function deployIsm() external {
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
ism = new ArbL2ToL1Ism(L1_BRIDGE);
TestRecipient testRecipient = new TestRecipient();
testRecipient.setInterchainSecurityModule(address(ism));
vm.stopBroadcast();
}
function deployHook() external {
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
hook = new ArbL2ToL1Hook(
L2_MAILBOX,
L1_DOMAIN,
TypeCasts.addressToBytes32(L1_ISM),
ARBSYS,
200_000 // estimated gas amount used for verify
);
vm.stopBroadcast();
}
function deployTestRecipient() external {
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
TestIsm noopIsm = new TestIsm();
noopIsm.setVerify(true);
TestRecipient testRecipient = new TestRecipient();
testRecipient.setInterchainSecurityModule(address(noopIsm));
console.log("TestRecipient address: %s", address(testRecipient));
vm.stopBroadcast();
}
function setAuthorizedHook() external {
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
ism = ArbL2ToL1Ism(L1_ISM);
ism.setAuthorizedHook(TypeCasts.addressToBytes32(L2_HOOK));
vm.stopBroadcast();
}
}

@ -147,15 +147,7 @@ contract LayerZeroV2HookTest is Test {
vm.assume(balance < nativeFee - 1);
vm.deal(address(this), balance);
vm.expectRevert(
abi.encodeWithSelector(
Errors.InsufficientFee.selector,
100,
balance,
0,
0
)
);
vm.expectRevert(); // OutOfFunds
mailbox.dispatch{value: balance}(
HYPERLANE_DEST_DOMAIN,
address(crossChainCounterApp).addressToBytes32(),

@ -12,6 +12,7 @@ import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol";
import {MockArbBridge, MockArbSys} from "../../contracts/mock/MockArbBridge.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {ExternalBridgeTest} from "./ExternalBridgeTest.sol";
import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol";
contract ArbL2ToL1IsmTest is ExternalBridgeTest {
uint256 internal constant MOCK_LEAF_INDEX = 40160;
@ -22,10 +23,10 @@ contract ArbL2ToL1IsmTest is ExternalBridgeTest {
0x0000000000000000000000000000000000000064;
MockArbBridge internal arbBridge;
TestInterchainGasPaymaster internal mockOverheadIgp;
function setUp() public override {
// Arbitrum bridge mock setup
GAS_QUOTE = 120_000;
vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code);
deployAll();
@ -38,12 +39,13 @@ contract ArbL2ToL1IsmTest is ExternalBridgeTest {
function deployHook() public {
originMailbox = new TestMailbox(ORIGIN_DOMAIN);
mockOverheadIgp = new TestInterchainGasPaymaster();
hook = new ArbL2ToL1Hook(
address(originMailbox),
DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(ism)),
L2_ARBSYS_ADDRESS,
GAS_QUOTE
address(mockOverheadIgp)
);
}
@ -60,6 +62,20 @@ contract ArbL2ToL1IsmTest is ExternalBridgeTest {
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
}
function test_postDispatch_childHook() public {
bytes memory encodedHookData = _encodeHookData(messageId);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit(
78_000
);
uint256 quote = hook.quoteDispatch(igpMetadata, encodedMessage);
assertEq(quote, mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, 78_000));
hook.postDispatch{value: quote}(igpMetadata, encodedMessage);
}
/* ============ helper functions ============ */
function _expectOriginExternalBridgeCall(

@ -150,6 +150,8 @@ contract ERC5164IsmTest is ExternalBridgeTest {
function test_verify_valueAlreadyClaimed(uint256) public override {}
function testFuzz_postDispatch_refundsExtraValue(uint256) public override {}
function test_verify_false_arbitraryCall() public override {}
/* ============ helper functions ============ */

@ -18,6 +18,7 @@ abstract contract ExternalBridgeTest is Test {
uint8 internal constant HYPERLANE_VERSION = 1;
uint32 internal constant ORIGIN_DOMAIN = 1;
uint32 internal constant DESTINATION_DOMAIN = 2;
uint256 internal constant MSG_VALUE = 1 ether;
uint256 internal constant MAX_MSG_VALUE = 2 ** 255 - 1;
uint256 internal GAS_QUOTE;
@ -53,7 +54,8 @@ abstract contract ExternalBridgeTest is Test {
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
hook.postDispatch{value: GAS_QUOTE}(testMetadata, encodedMessage);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
hook.postDispatch{value: quote}(testMetadata, encodedMessage);
}
function test_postDispatch_revertWhen_chainIDNotSupported() public {
@ -89,6 +91,37 @@ abstract contract ExternalBridgeTest is Test {
hook.postDispatch(excessValueMetadata, encodedMessage);
}
function testFuzz_postDispatch_refundsExtraValue(
uint256 extraValue
) public virtual {
vm.assume(extraValue < MAX_MSG_VALUE);
vm.deal(address(this), address(this).balance + extraValue);
uint256 valueBefore = address(this).balance;
bytes memory encodedHookData = _encodeHookData(messageId);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
hook.postDispatch{value: quote + extraValue}(
testMetadata,
encodedMessage
);
assertEq(address(this).balance, valueBefore - quote);
}
function test_postDispatch_revertWhen_insufficientValue() public {
bytes memory encodedHookData = _encodeHookData(messageId);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
vm.expectRevert(); //arithmetic underflow
hook.postDispatch{value: quote - 1}(testMetadata, encodedMessage);
}
/* ============ ISM.verifyMessageId ============ */
function test_verifyMessageId_asyncCall() public {
@ -122,17 +155,17 @@ abstract contract ExternalBridgeTest is Test {
function test_verify_msgValue_asyncCall() public virtual {
bytes memory encodedHookData = _encodeHookData(messageId);
_externalBridgeDestinationCall(encodedHookData, 1 ether);
_externalBridgeDestinationCall(encodedHookData, MSG_VALUE);
assertTrue(ism.verify(new bytes(0), encodedMessage));
assertEq(address(testRecipient).balance, 1 ether);
assertEq(address(testRecipient).balance, MSG_VALUE);
}
function test_verify_msgValue_externalBridgeCall() public virtual {
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(ism),
1 ether,
MSG_VALUE,
messageId
);
assertTrue(ism.verify(externalCalldata, encodedMessage));
@ -279,6 +312,8 @@ abstract contract ExternalBridgeTest is Test {
address _sender
) internal virtual returns (bytes memory) {}
// meant to mock an arbitrary successful call made by the external bridge
receive() external payable {}
// meant to be mock an arbitrary successful call made by the external bridge
function verifyMessageId(bytes32 /*messageId*/) public payable {}
}

@ -14,6 +14,7 @@ import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/Mo
import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol";
import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol";
import {ExternalBridgeTest} from "./ExternalBridgeTest.sol";
import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol";
contract OPL2ToL1IsmTest is ExternalBridgeTest {
address internal constant L2_MESSENGER_ADDRESS =
@ -21,6 +22,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest {
uint256 internal constant MOCK_NONCE = 0;
TestInterchainGasPaymaster internal mockOverheadIgp;
MockOptimismPortal internal portal;
MockOptimismMessenger internal l1Messenger;
@ -30,7 +32,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest {
function setUp() public override {
// Optimism messenger mock setup
GAS_QUOTE = 120_000;
// GAS_QUOTE = 300_000;
vm.etch(
L2_MESSENGER_ADDRESS,
address(new MockOptimismMessenger()).code
@ -42,12 +44,13 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest {
function deployHook() public {
originMailbox = new TestMailbox(ORIGIN_DOMAIN);
mockOverheadIgp = new TestInterchainGasPaymaster();
hook = new OPL2ToL1Hook(
address(originMailbox),
DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(ism)),
L2_MESSENGER_ADDRESS,
uint32(GAS_QUOTE)
address(mockOverheadIgp)
);
}
@ -67,6 +70,20 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest {
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
}
function test_postDispatch_childHook() public {
bytes memory encodedHookData = _encodeHookData(messageId);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit(
78_000
);
uint256 quote = hook.quoteDispatch(igpMetadata, encodedMessage);
assertEq(quote, mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, 78_000));
hook.postDispatch{value: quote}(igpMetadata, encodedMessage);
}
/* ============ helper functions ============ */
function _expectOriginExternalBridgeCall(
@ -76,7 +93,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest {
L2_MESSENGER_ADDRESS,
abi.encodeCall(
ICrossDomainMessenger.sendMessage,
(address(ism), _encodedHookData, uint32(GAS_QUOTE))
(address(ism), _encodedHookData, uint32(300_000))
)
);
}

@ -862,6 +862,8 @@ export class EvmHookModule extends HyperlaneModule<
this.multiProvider.getSignerOrProvider(config.destinationChain),
);
const childHook = await this.deploy({ config: config.childHook });
// deploy arbL1ToL1 hook
const hook = await this.deployer.deployContract(
chain,
@ -871,7 +873,7 @@ export class EvmHookModule extends HyperlaneModule<
this.multiProvider.getDomainId(config.destinationChain),
addressToBytes32(arbL2ToL1IsmAddress),
config.arbSys,
BigNumber.from(200_000), // 2x estimate of executeTransaction call overhead
childHook.address,
],
);
// set authorized hook on arbL2ToL1 ism

@ -365,11 +365,14 @@ export class EvmHookReader extends HyperlaneReader implements HookReader {
const destinationChainName =
this.multiProvider.getChainName(destinationDomain);
const childHookAddress = await hook.childHook();
const childHookConfig = await this.deriveHookConfig(childHookAddress);
const config: WithAddress<ArbL2ToL1HookConfig> = {
address,
type: HookType.ARB_L2_TO_L1,
destinationChain: destinationChainName,
arbSys,
childHook: childHookConfig,
};
this._cache.set(address, config);

@ -46,6 +46,7 @@ export const ArbL2ToL1HookSchema = z.object({
'address of the bridge contract on L1, optional only needed for non @arbitrum/sdk chains',
),
destinationChain: z.string(),
childHook: z.lazy((): z.ZodSchema => HookConfigSchema),
});
export const IgpSchema = OwnableSchema.extend({

@ -41,7 +41,7 @@ import { ArbL2ToL1MetadataBuilder } from './arbL2ToL1.js';
import { MetadataContext } from './builder.js';
describe('ArbL2ToL1MetadataBuilder', () => {
const origin: ChainName = 'test1';
const origin: ChainName = 'test4';
const destination: ChainName = 'test2';
let core: HyperlaneCore;
let ismFactory: HyperlaneIsmFactory;
@ -93,14 +93,29 @@ describe('ArbL2ToL1MetadataBuilder', () => {
[],
);
hookConfig = {
test1: {
test4: {
type: HookType.ARB_L2_TO_L1,
arbSys: mockArbSys.address,
destinationChain: destination,
childHook: {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: relayer.address,
owner: relayer.address,
oracleKey: relayer.address,
overhead: {
[destination]: 200000,
},
oracleConfig: {
[destination]: {
gasPrice: '20',
tokenExchangeRate: '10000000000',
},
},
},
},
};
factoryContracts = contractsMap.test1;
factoryContracts = contractsMap.test4;
proxyFactoryAddresses = Object.keys(factoryContracts).reduce((acc, key) => {
acc[key] =
contractsMap[origin][key as keyof ProxyFactoryFactories].address;
@ -111,11 +126,11 @@ describe('ArbL2ToL1MetadataBuilder', () => {
new MockArbBridge__factory(),
[],
);
hookConfig.test1.bridge = arbBridge.address;
hookConfig.test4.bridge = arbBridge.address;
const hookModule = await EvmHookModule.create({
chain: origin,
config: hookConfig.test1,
config: hookConfig.test4,
proxyFactoryFactories: proxyFactoryAddresses,
coreAddresses: core.getAddresses(origin),
multiProvider,

Loading…
Cancel
Save