Converting the OP stack hooks to transient storage version (#2632)

- Updated the OP Stack tests for the Mailbox V3 transient storage
version
- Allowing OPStackHook to send msg.value at the time of message delivery
(uses bit masking)
- Added LibBit library, will be useful for all the auth hooks which can
send `msg.value`

- None

- Fixes breaking OP Stack tests for
https://github.com/hyperlane-xyz/issues/issues/513
- Also fixes
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2410

No

Unit tests

---------

Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>

Remove e2e and add yarn build to CI

Adding protocol fees (#2640)

- Adding protocol fee as a hook

- None

V3

Yes

Fuzz tests

---------

Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
pull/2736/head
Kunal Arora 1 year ago committed by Yorke Rhodes
parent f38660e70a
commit 760dce657b
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 1
      .github/workflows/e2e.yml
  2. 34
      .github/workflows/node.yml
  3. 1
      solidity/contracts/Mailbox.sol
  4. 16
      solidity/contracts/hooks/AbstractMessageIdAuthHook.sol
  5. 13
      solidity/contracts/hooks/OPStackHook.sol
  6. 125
      solidity/contracts/hooks/StaticProtocolFee.sol
  7. 44
      solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol
  8. 29
      solidity/contracts/libs/LibBit.sol
  9. 12
      solidity/test/hooks/FallbackDomainRoutingHook.t.sol
  10. 146
      solidity/test/hooks/StaticProtocolFee.t.sol
  11. 2
      solidity/test/isms/ERC5164ISM.t.sol
  12. 222
      solidity/test/isms/OPStackIsm.t.sol
  13. 34
      solidity/test/lib/LibBit.t.sol

@ -4,6 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:

@ -93,27 +93,27 @@ jobs:
exit 1
fi
test-ts:
runs-on: ubuntu-latest
needs: [yarn-build]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
# test-ts:
# runs-on: ubuntu-latest
# needs: [yarn-build]
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: recursive
- uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}
# - uses: actions/cache@v3
# with:
# path: ./*
# key: ${{ github.sha }}
- name: sdk
run: yarn workspace @hyperlane-xyz/sdk run test
# - name: sdk
# run: yarn workspace @hyperlane-xyz/sdk run test
- name: helloworld
run: yarn workspace @hyperlane-xyz/helloworld run test
# - name: helloworld
# run: yarn workspace @hyperlane-xyz/helloworld run test
- name: token
run: yarn workspace @hyperlane-xyz/hyperlane-token run test
# - name: token
# run: yarn workspace @hyperlane-xyz/hyperlane-token run test
# - name: infra
# run: yarn workspace @hyperlane-xyz/infra run test

@ -30,6 +30,7 @@ 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.

@ -64,13 +64,14 @@ abstract contract AbstractMessageIdAuthHook is
override
{
bytes32 id = message.id();
require(isLatestDispatched(id), "message not latest dispatched");
require(
isLatestDispatched(id),
"AbstractMessageIdAuthHook: message not latest dispatched"
);
require(
message.destination() == destinationDomain,
"invalid destination domain"
"AbstractMessageIdAuthHook: invalid destination domain"
);
// TODO: handle msg.value?
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
id
@ -78,6 +79,13 @@ abstract contract AbstractMessageIdAuthHook is
_sendMessageId(metadata, payload);
}
// ============ Internal functions ============
/**
* @notice Send a message to the ISM.
* @param metadata The metadata for the hook caller
* @param payload The payload for call to the ISM
*/
function _sendMessageId(bytes calldata metadata, bytes memory payload)
internal
virtual;

@ -26,15 +26,15 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title OPStackHook
* @notice Message hook to inform the Optimism ISM of messages published through
* @notice Message hook to inform the OPStackISM of messages published through
* the native OPStack bridge.
* @dev V3 WIP
*/
contract OPStackHook is AbstractMessageIdAuthHook {
using OPStackHookMetadata for bytes;
// ============ Constants ============
/// @notice messenger contract specified by the rollup
ICrossDomainMessenger public immutable l1Messenger;
// Gas limit for sending messages to L2
@ -52,15 +52,22 @@ contract OPStackHook is AbstractMessageIdAuthHook {
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
require(
Address.isContract(_messenger),
"ERC5164Hook: invalid dispatcher"
"OPStackHook: invalid messenger"
);
l1Messenger = ICrossDomainMessenger(_messenger);
}
// ============ Internal functions ============
/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(bytes calldata metadata, bytes memory payload)
internal
override
{
require(
metadata.msgValue() < 2**255,
"OPStackHook: msgValue must less than 2 ** 255"
);
l1Messenger.sendMessage{value: metadata.msgValue()}(
ism,
payload,

@ -0,0 +1,125 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title StaticProtocolFee
* @notice Collects a static protocol fee from the sender.
* @dev V3 WIP
*/
contract StaticProtocolFee is IPostDispatchHook, Ownable {
using Address for address payable;
using Message for bytes;
// ============ Constants ============
/// @notice The maximum protocol fee that can be set.
uint256 public immutable MAX_PROTOCOL_FEE;
// ============ Public Storage ============
/// @notice The current protocol fee.
uint256 public protocolFee;
/// @notice The beneficiary of protocol fees.
address public beneficiary;
// ============ Constructor ============
constructor(
uint256 _maxProtocolFee,
uint256 _protocolFee,
address _beneficiary,
address _owner
) {
MAX_PROTOCOL_FEE = _maxProtocolFee;
_setProtocolFee(_protocolFee);
_setBeneficiary(_beneficiary);
_transferOwnership(_owner);
}
// ============ External Functions ============
/**
* @notice Collects the protocol fee from the sender.
*/
function postDispatch(bytes calldata, bytes calldata message)
external
payable
override
{
require(
msg.value >= protocolFee,
"StaticProtocolFee: insufficient protocol fee"
);
uint256 refund = msg.value - protocolFee;
if (refund > 0) payable(message.senderAddress()).sendValue(refund);
}
/**
* @notice Sets the protocol fee.
* @param _protocolFee The new protocol fee.
*/
function setProtocolFee(uint256 _protocolFee) external onlyOwner {
_setProtocolFee(_protocolFee);
}
/**
* @notice Sets the beneficiary of protocol fees.
* @param _beneficiary The new beneficiary.
*/
function setBeneficiary(address _beneficiary) external onlyOwner {
_setBeneficiary(_beneficiary);
}
/**
* @notice Collects protocol fees from the contract.
*/
function collectProtocolFees() external {
payable(beneficiary).sendValue(address(this).balance);
}
// ============ Internal Functions ============
/**
* @notice Sets the protocol fee.
* @param _protocolFee The new protocol fee.
*/
function _setProtocolFee(uint256 _protocolFee) internal {
require(
_protocolFee <= MAX_PROTOCOL_FEE,
"StaticProtocolFee: exceeds max protocol fee"
);
protocolFee = _protocolFee;
}
/**
* @notice Sets the beneficiary of protocol fees.
* @param _beneficiary The new beneficiary.
*/
function _setBeneficiary(address _beneficiary) internal {
require(
_beneficiary != address(0),
"StaticProtocolFee: invalid beneficiary"
);
beneficiary = _beneficiary;
}
}

@ -16,11 +16,13 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {LibBit} from "../../libs/LibBit.sol";
import {Message} from "../../libs/Message.sol";
import {TypeCasts} from "../../libs/TypeCasts.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
@ -35,16 +37,25 @@ abstract contract AbstractMessageIdAuthorizedIsm is
IInterchainSecurityModule,
Initializable
{
using Address for address payable;
using LibBit for uint256;
using Message for bytes;
// ============ Public Storage ============
// Maps messageId to whether or not the sender attested to that message ID on the origin chain
// @dev anyone can send an untrusted messageId, so need to check for that while verifying
mapping(bytes32 => bool) public verifiedMessageIds;
// Address for Hook on L1 responsible for sending message via the Optimism bridge
/// @notice Maps messageId to whether or not the message has been verified
/// first bit is boolean for verification
/// rest of bits is the amount to send to the recipient
/// @dev bc of the bit packing, we can only send up to 2^255 wei
mapping(bytes32 => uint256) public verifiedMessages;
/// @notice Index of verification bit in verifiedMessages
uint256 public constant MASK_INDEX = 255;
/// @notice Address for Hook on L1 responsible for sending message via the Optimism bridge
address public authorizedHook;
// ============ Events ============
/// @notice Emitted when a message is received from the external bridge
/// Might be useful for debugging for the scraper
event ReceivedMessage(bytes32 indexed messageId);
// ============ Initializer ============
@ -61,30 +72,35 @@ abstract contract AbstractMessageIdAuthorizedIsm is
/**
* @notice Verify a message was received by ISM.
* @param _message Message to verify.
* @param message Message to verify.
*/
function verify(
bytes calldata, /*_metadata*/
bytes calldata _message
) external view returns (bool) {
bytes32 _messageId = Message.id(_message);
return verifiedMessageIds[_messageId];
bytes calldata message
) external returns (bool) {
bytes32 messageId = message.id();
bool verified = verifiedMessages[messageId].isBitSet(MASK_INDEX);
if (verified)
payable(message.recipientAddress()).sendValue(
verifiedMessages[messageId].clearBit(MASK_INDEX)
);
return verified;
}
/**
* @notice Receive a message from the L2 messenger.
* @dev Only callable by the L2 messenger.
* @param _messageId Hyperlane ID for the message.
* @param messageId Hyperlane Id of the message.
*/
function verifyMessageId(bytes32 _messageId) external {
function verifyMessageId(bytes32 messageId) external payable {
require(
_isAuthorized(),
"AbstractMessageIdAuthorizedIsm: sender is not the hook"
);
verifiedMessageIds[_messageId] = true;
emit ReceivedMessage(_messageId);
verifiedMessages[messageId] = msg.value.setBit(MASK_INDEX);
emit ReceivedMessage(messageId);
}
function _isAuthorized() internal view virtual returns (bool);

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/// @notice Library for bit shifting and masking
library LibBit {
function setBit(uint256 _value, uint256 _index)
internal
pure
returns (uint256)
{
return _value | (1 << _index);
}
function clearBit(uint256 _value, uint256 _index)
internal
pure
returns (uint256)
{
return _value & ~(1 << _index);
}
function isBitSet(uint256 _value, uint256 _index)
internal
pure
returns (bool)
{
return (_value >> _index) & 1 == 1;
}
}

@ -6,13 +6,13 @@ import "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {MessageUtils} from "../isms/IsmTestUtils.sol";
import {Mailbox} from "../../contracts/Mailbox.sol";
import {FallbackDomainRoutingHook} from "../../contracts/hooks/FallbackDomainRoutingHook.sol";
import {ConfigFallbackDomainRoutingHook} from "../../contracts/hooks/ConfigFallbackDomainRoutingHook.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
contract FallbackDomainRoutingHookTest is Test {
using TypeCasts for address;
FallbackDomainRoutingHook internal fallbackHook;
ConfigFallbackDomainRoutingHook internal fallbackHook;
TestPostDispatchHook internal configuredTestHook;
TestPostDispatchHook internal mailboxDefaultHook;
TestRecipient internal testRecipient;
@ -29,10 +29,7 @@ contract FallbackDomainRoutingHookTest is Test {
configuredTestHook = new TestPostDispatchHook();
mailboxDefaultHook = new TestPostDispatchHook();
testRecipient = new TestRecipient();
fallbackHook = new FallbackDomainRoutingHook(
address(mailbox),
address(this)
);
fallbackHook = new ConfigFallbackDomainRoutingHook(address(mailbox));
testMessage = _encodeTestMessage();
mailbox.setDefaultHook(address(mailboxDefaultHook));
}
@ -40,7 +37,8 @@ contract FallbackDomainRoutingHookTest is Test {
function test_postDispatchHook_configured() public payable {
fallbackHook.setHook(
TEST_DESTINATION_DOMAIN,
address(configuredTestHook)
address(testRecipient).addressToBytes32(),
configuredTestHook
);
vm.expectEmit(false, false, false, false, address(configuredTestHook));

@ -0,0 +1,146 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {MessageUtils} from "../isms/IsmTestUtils.sol";
import {StaticProtocolFee} from "../../contracts/hooks/StaticProtocolFee.sol";
contract StaticProtocolFeeTest is Test {
using TypeCasts for address;
StaticProtocolFee internal fees;
address internal alice = address(0x1); // alice the user
address internal bob = address(0x2); // bob the beneficiary
address internal charlie = address(0x3); // charlie the crock
uint32 internal constant TEST_ORIGIN_DOMAIN = 1;
uint32 internal constant TEST_DESTINATION_DOMAIN = 2;
bytes internal testMessage;
function setUp() public {
fees = new StaticProtocolFee(1e16, 1e15, bob, address(this));
testMessage = _encodeTestMessage();
}
function testConstructor() public {
assertEq(fees.protocolFee(), 1e15);
}
function testSetProtocolFee(uint256 fee) public {
fee = bound(fee, 0, fees.MAX_PROTOCOL_FEE());
fees.setProtocolFee(fee);
assertEq(fees.protocolFee(), fee);
}
function testSetProtocolFee_revertsWhen_notOwner() public {
uint256 fee = 1e17;
vm.prank(charlie);
vm.expectRevert("Ownable: caller is not the owner");
fees.setProtocolFee(fee);
assertEq(fees.protocolFee(), 1e15);
}
function testSetProtocolFee_revertWhen_exceedsMax(uint256 fee) public {
fee = bound(
fee,
fees.MAX_PROTOCOL_FEE() + 1,
10 * fees.MAX_PROTOCOL_FEE()
);
vm.expectRevert("StaticProtocolFee: exceeds max protocol fee");
fees.setProtocolFee(fee);
assertEq(fees.protocolFee(), 1e15);
}
function testSetBeneficiary_revertWhen_notOwner() public {
vm.prank(charlie);
vm.expectRevert("Ownable: caller is not the owner");
fees.setBeneficiary(charlie);
assertEq(fees.beneficiary(), bob);
}
function testFuzz_postDispatch_inusfficientFees(
uint256 feeRequired,
uint256 feeSent
) public {
feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE());
// bound feeSent to be less than feeRequired
feeSent = bound(feeSent, 0, feeRequired - 1);
vm.deal(alice, feeSent);
fees.setProtocolFee(feeRequired);
uint256 balanceBefore = alice.balance;
vm.prank(alice);
vm.expectRevert("StaticProtocolFee: insufficient protocol fee");
fees.postDispatch{value: feeSent}("", "");
assertEq(alice.balance, balanceBefore);
}
function testFuzz_postDispatch_sufficientFees(
uint256 feeRequired,
uint256 feeSent
) public {
feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE());
feeSent = bound(feeSent, feeRequired, 10 * feeRequired);
vm.deal(alice, feeSent);
fees.setProtocolFee(feeRequired);
uint256 aliceBalanceBefore = alice.balance;
vm.prank(alice);
fees.postDispatch{value: feeSent}("", testMessage);
assertEq(alice.balance, aliceBalanceBefore - feeRequired);
}
function testFuzz_collectProtocolFee(
uint256 feeRequired,
uint256 dispatchCalls
) public {
feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE());
// no of postDispatch calls to be made
dispatchCalls = bound(dispatchCalls, 1, 1000);
vm.deal(alice, feeRequired * dispatchCalls);
fees.setProtocolFee(feeRequired);
uint256 balanceBefore = bob.balance;
for (uint256 i = 0; i < dispatchCalls; i++) {
vm.prank(alice);
fees.postDispatch{value: feeRequired}("", "");
}
fees.collectProtocolFees();
assertEq(bob.balance, balanceBefore + feeRequired * dispatchCalls);
}
// ============ 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 {}
}

@ -144,7 +144,7 @@ contract ERC5164ISMTest is Test {
vm.startPrank(address(executor));
ism.verifyMessageId(messageId);
assertTrue(ism.verifiedMessageIds(messageId));
assertTrue(ism.verifiedMessages(messageId));
vm.stopPrank();
}

@ -3,9 +3,12 @@ pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {LibBit} from "../../contracts/libs/LibBit.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {Mailbox} from "../../contracts/Mailbox.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "./IsmTestUtils.sol";
import {TestMultisigIsm} from "../../contracts/test/TestMultisigIsm.sol";
import {OPStackIsm} from "../../contracts/isms/hook/OPStackIsm.sol";
import {OPStackHook} from "../../contracts/hooks/OPStackHook.sol";
@ -21,7 +24,9 @@ import {Encoding} from "@eth-optimism/contracts-bedrock/contracts/libraries/Enco
import {Hashing} from "@eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol";
contract OPStackIsmTest is Test {
using LibBit for uint256;
using TypeCasts for address;
using MessageUtils for bytes;
uint256 internal mainnetFork;
uint256 internal optimismFork;
@ -40,15 +45,17 @@ contract OPStackIsmTest is Test {
ICrossDomainMessenger internal l1Messenger;
L2CrossDomainMessenger internal l2Messenger;
OPStackIsmTest internal opISM;
TestMailbox internal l1Mailbox;
OPStackIsm internal opISM;
OPStackHook internal opHook;
TestRecipient internal testRecipient;
bytes internal testMessage =
abi.encodePacked("Hello from the other chain!");
bytes internal testMetadata = abi.encodePacked(uint256(0));
bytes encodedMessage = _encodeTestMessage(0, address(testRecipient));
bytes32 messageId = Message.id(encodedMessage);
bytes internal encodedMessage;
bytes32 internal messageId;
uint32 internal constant MAINNET_DOMAIN = 1;
uint32 internal constant OPTIMISM_DOMAIN = 10;
@ -65,7 +72,7 @@ contract OPStackIsmTest is Test {
event FailedRelayedMessage(bytes32 indexed msgHash);
event ReceivedMessage(bytes32 indexed sender, bytes32 indexed messageId);
event ReceivedMessage(bytes32 indexed messageId);
function setUp() public {
// block numbers to fork from, chain data is cached to ../../forge-cache/
@ -73,6 +80,9 @@ contract OPStackIsmTest is Test {
optimismFork = vm.createFork(vm.rpcUrl("optimism"), 106_233_774);
testRecipient = new TestRecipient();
encodedMessage = _encodeTestMessage();
messageId = Message.id(encodedMessage);
}
///////////////////////////////////////////////////////////////////
@ -83,11 +93,13 @@ contract OPStackIsmTest is Test {
vm.selectFork(mainnetFork);
l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS);
l1Mailbox = new TestMailbox(MAINNET_DOMAIN);
opHook = new OPStackHook(
address(l1Mailbox),
OPTIMISM_DOMAIN,
L1_MESSENGER_ADDRESS,
address(opISM)
address(opISM),
L1_MESSENGER_ADDRESS
);
vm.makePersistent(address(opHook));
@ -108,11 +120,11 @@ contract OPStackIsmTest is Test {
vm.selectFork(optimismFork);
opISM.setOptimismHook(address(opHook));
opISM.setAuthorizedHook(address(opHook));
// for sending value
vm.deal(
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS),
1e18
2**255
);
}
@ -128,13 +140,15 @@ contract OPStackIsmTest is Test {
vm.selectFork(mainnetFork);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
uint40 nonce = ICanonicalTransactionChain(L1_CANNONICAL_CHAIN)
.getQueueLength();
l1Mailbox.updateLatestDispatchedId(messageId);
vm.expectEmit(true, true, true, false, L1_MESSENGER_ADDRESS);
emit SentMessage(
address(opISM),
@ -143,8 +157,7 @@ contract OPStackIsmTest is Test {
nonce,
DEFAULT_GAS_LIMIT
);
opHook.postDispatch(OPTIMISM_DOMAIN, messageId);
opHook.postDispatch(testMetadata, encodedMessage);
}
function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public {
@ -152,8 +165,49 @@ contract OPStackIsmTest is Test {
vm.selectFork(mainnetFork);
vm.expectRevert("OptimismHook: invalid destination domain");
opHook.postDispatch(11, messageId);
bytes memory message = MessageUtils.formatMessage(
VERSION,
uint32(0),
MAINNET_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
11, // wrong domain
TypeCasts.addressToBytes32(address(testRecipient)),
testMessage
);
l1Mailbox.updateLatestDispatchedId(Message.id(message));
vm.expectRevert(
"AbstractMessageIdAuthHook: invalid destination domain"
);
opHook.postDispatch(testMetadata, message);
}
function testFork_postDispatch_RevertWhen_TooMuchValue() public {
deployAll();
vm.selectFork(mainnetFork);
vm.deal(address(this), uint256(2**255 + 1));
bytes memory excessValueMetadata = abi.encodePacked(
uint256(2**255 + 1)
);
l1Mailbox.updateLatestDispatchedId(messageId);
vm.expectRevert("OPStackHook: msgValue must less than 2 ** 255");
opHook.postDispatch(excessValueMetadata, encodedMessage);
}
function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage()
public
{
deployAll();
vm.selectFork(mainnetFork);
vm.expectRevert(
"AbstractMessageIdAuthHook: message not latest dispatched"
);
opHook.postDispatch(testMetadata, encodedMessage);
}
/* ============ ISM.verifyMessageId ============ */
@ -164,8 +218,8 @@ contract OPStackIsmTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
(uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce(
@ -189,8 +243,8 @@ contract OPStackIsmTest is Test {
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)
);
vm.expectEmit(true, true, false, false, address(opISM));
emit ReceivedMessage(address(this).addressToBytes32(), messageId);
vm.expectEmit(true, false, false, false, address(opISM));
emit ReceivedMessage(messageId);
vm.expectEmit(true, false, false, false, L2_MESSENGER_ADDRESS);
emit RelayedMessage(versionedHash);
@ -204,52 +258,10 @@ contract OPStackIsmTest is Test {
encodedHookData
);
assertTrue(
opISM.verifiedMessageIds(
messageId,
address(this).addressToBytes32()
)
);
assertTrue(opISM.verifiedMessages(messageId).isBitSet(255));
vm.stopPrank();
}
// will get included in https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2410
// function testverifyMessageId_WithValue() public {
// // this would fail
// deployAll();
// vm.selectFork(optimismFork);
// bytes memory encodedHookData = abi.encodeCall(
// OPStackIsm.verifyMessageId,
// (address(this), messageId)
// );
// (uint240 nonce, uint16 verison) =
// Encoding.decodeVersionedNonce(l2Messenger.messageNonce());
// uint256 versionedNonce = Encoding.encodeVersionedNonce(nonce + 1, verison);
// vm.startPrank(
// AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)
// );
// l2Messenger.relayMessage{value: 1e18} (
// versionedNonce,
// address(opHook),
// address(opISM),
// 1e18,
// DEFAULT_GAS_LIMIT,
// encodedHookData
// );
// assertEq(opISM.verifiedMessageIds(messageId, address(this)), true);
// assertEq(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS), 0);
// assertEq(address(this).balance, 1e18);
// vm.stopPrank();
// }
function testFork_verifyMessageId_RevertWhen_NotAuthorized() public {
deployAll();
@ -257,7 +269,7 @@ contract OPStackIsmTest is Test {
// needs to be called by the cannonical messenger on Optimism
vm.expectRevert(NotCrossChainCall.selector);
opISM.verifyMessageId(address(opHook).addressToBytes32(), messageId);
opISM.verifyMessageId(messageId);
// set the xDomainMessageSender storage slot as alice
bytes32 key = bytes32(uint256(204));
@ -267,8 +279,10 @@ contract OPStackIsmTest is Test {
vm.startPrank(L2_MESSENGER_ADDRESS);
// needs to be called by the authorized hook contract on Ethereum
vm.expectRevert("OPStackIsm: sender is not the hook");
opISM.verifyMessageId(address(opHook).addressToBytes32(), messageId);
vm.expectRevert(
"AbstractMessageIdAuthorizedIsm: sender is not the hook"
);
opISM.verifyMessageId(messageId);
}
/* ============ ISM.verify ============ */
@ -279,8 +293,8 @@ contract OPStackIsmTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
(uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce(
@ -305,15 +319,16 @@ contract OPStackIsmTest is Test {
assertTrue(verified);
}
// sending over invalid message
function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public {
/// forge-config: default.fuzz.runs = 10
function testFork_verify_WithValue(uint256 _msgValue) public {
_msgValue = bound(_msgValue, 0, 2**254);
deployAll();
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
(uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce(
@ -325,32 +340,31 @@ contract OPStackIsmTest is Test {
);
vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS));
l2Messenger.relayMessage(
l2Messenger.relayMessage{value: _msgValue}(
versionedNonce,
address(opHook),
address(opISM),
0,
_msgValue,
DEFAULT_GAS_LIMIT,
encodedHookData
);
bytes memory invalidMessage = _encodeTestMessage(0, address(this));
bool verified = opISM.verify(new bytes(0), invalidMessage);
assertFalse(verified);
bool verified = opISM.verify(new bytes(0), encodedMessage);
assertTrue(verified);
assertEq(address(opISM).balance, 0);
assertEq(address(testRecipient).balance, _msgValue);
}
// invalid messageID in postDispatch
function testFork_verify_RevertWhen_InvalidOptimismMessageID() public {
// sending over invalid message
function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public {
deployAll();
vm.selectFork(optimismFork);
bytes memory invalidMessage = _encodeTestMessage(0, address(this));
bytes32 _messageId = Message.id(invalidMessage);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), _messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
(uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce(
@ -371,18 +385,38 @@ contract OPStackIsmTest is Test {
encodedHookData
);
bool verified = opISM.verify(new bytes(0), encodedMessage);
bytes memory invalidMessage = MessageUtils.formatMessage(
VERSION,
uint8(0),
MAINNET_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
OPTIMISM_DOMAIN,
TypeCasts.addressToBytes32(address(this)), // wrong recipient
testMessage
);
bool verified = opISM.verify(new bytes(0), invalidMessage);
assertFalse(verified);
}
function testFork_verify_RevertWhen_InvalidSender() public {
// invalid messageID in postDispatch
function testFork_verify_RevertWhen_InvalidOptimismMessageID() public {
deployAll();
vm.selectFork(optimismFork);
bytes memory invalidMessage = MessageUtils.formatMessage(
VERSION,
uint8(0),
MAINNET_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
OPTIMISM_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
testMessage
);
bytes32 _messageId = Message.id(invalidMessage);
bytes memory encodedHookData = abi.encodeCall(
OPStackIsm.verifyMessageId,
(alice.addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(_messageId)
);
(uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce(
@ -409,19 +443,15 @@ contract OPStackIsmTest is Test {
/* ============ helper functions ============ */
function _encodeTestMessage(uint32 _msgCount, address _receipient)
internal
view
returns (bytes memory)
{
function _encodeTestMessage() internal view returns (bytes memory) {
return
abi.encodePacked(
MessageUtils.formatMessage(
VERSION,
_msgCount,
uint32(0),
MAINNET_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
OPTIMISM_DOMAIN,
TypeCasts.addressToBytes32(_receipient),
TypeCasts.addressToBytes32(address(testRecipient)),
testMessage
);
}

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {LibBit} from "../../contracts/libs/LibBit.sol";
contract LibBitTest is Test {
using LibBit for uint256;
uint256 testValue;
uint256 MAX_INT =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
function setUp() public {
testValue = 0;
}
function testSetBit(uint8 index) public {
testValue = testValue.setBit(index);
assertEq(testValue, 2**index);
}
function testClearBit(uint8 index) public {
testValue = MAX_INT;
testValue = testValue.clearBit(index);
assertEq(testValue + 2**index, MAX_INT);
}
function testIsBitSet(uint8 index) public {
testValue = 2**index;
assertTrue(testValue.isBitSet(index));
}
}
Loading…
Cancel
Save