You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
544 lines
17 KiB
544 lines
17 KiB
// 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";
|
|
import {TypeCasts} from "../../contracts/libs/TypeCasts.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";
|
|
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
|
|
import {NotCrossChainCall} from "../../contracts/isms/hook/crossChainEnabled/errors.sol";
|
|
|
|
import {AddressAliasHelper} from "@eth-optimism/contracts/standards/AddressAliasHelper.sol";
|
|
import {ICrossDomainMessenger, IL2CrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol";
|
|
|
|
contract OPStackIsmTest is Test {
|
|
using LibBit for uint256;
|
|
using TypeCasts for address;
|
|
using MessageUtils for bytes;
|
|
|
|
uint256 internal mainnetFork;
|
|
uint256 internal optimismFork;
|
|
|
|
address internal constant L1_MESSENGER_ADDRESS =
|
|
0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1;
|
|
address internal constant L1_CANNONICAL_CHAIN =
|
|
0x5E4e65926BA27467555EB562121fac00D24E9dD2;
|
|
address internal constant L2_MESSENGER_ADDRESS =
|
|
0x4200000000000000000000000000000000000007;
|
|
|
|
uint8 internal constant VERSION = 0;
|
|
uint256 internal constant DEFAULT_GAS_LIMIT = 1_920_000;
|
|
|
|
address internal alice = address(0x1);
|
|
|
|
ICrossDomainMessenger internal l1Messenger;
|
|
IL2CrossDomainMessenger internal l2Messenger;
|
|
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 internal encodedMessage;
|
|
bytes32 internal messageId;
|
|
|
|
uint32 internal constant MAINNET_DOMAIN = 1;
|
|
uint32 internal constant OPTIMISM_DOMAIN = 10;
|
|
|
|
event SentMessage(
|
|
address indexed target,
|
|
address sender,
|
|
bytes message,
|
|
uint256 messageNonce,
|
|
uint256 gasLimit
|
|
);
|
|
|
|
event RelayedMessage(bytes32 indexed msgHash);
|
|
|
|
event FailedRelayedMessage(bytes32 indexed msgHash);
|
|
|
|
event ReceivedMessage(bytes32 indexed messageId);
|
|
|
|
function setUp() public {
|
|
// block numbers to fork from, chain data is cached to ../../forge-cache/
|
|
mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 17_586_909);
|
|
optimismFork = vm.createFork(vm.rpcUrl("optimism"), 106_233_774);
|
|
|
|
testRecipient = new TestRecipient();
|
|
|
|
encodedMessage = _encodeTestMessage();
|
|
messageId = Message.id(encodedMessage);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
/// SETUP ///
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
function deployOptimismHook() public {
|
|
vm.selectFork(mainnetFork);
|
|
|
|
l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS);
|
|
l1Mailbox = new TestMailbox(MAINNET_DOMAIN);
|
|
|
|
opHook = new OPStackHook(
|
|
address(l1Mailbox),
|
|
OPTIMISM_DOMAIN,
|
|
address(opISM),
|
|
L1_MESSENGER_ADDRESS
|
|
);
|
|
|
|
vm.makePersistent(address(opHook));
|
|
}
|
|
|
|
function deployOPStackIsm() public {
|
|
vm.selectFork(optimismFork);
|
|
|
|
l2Messenger = IL2CrossDomainMessenger(L2_MESSENGER_ADDRESS);
|
|
opISM = new OPStackIsm(L2_MESSENGER_ADDRESS);
|
|
|
|
vm.makePersistent(address(opISM));
|
|
}
|
|
|
|
function deployAll() public {
|
|
deployOPStackIsm();
|
|
deployOptimismHook();
|
|
|
|
vm.selectFork(optimismFork);
|
|
|
|
opISM.setAuthorizedHook(address(opHook));
|
|
// for sending value
|
|
vm.deal(
|
|
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS),
|
|
2**255
|
|
);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
/// FORK TESTS ///
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
/* ============ hook.quoteDispatch ============ */
|
|
|
|
function testFork_quoteDispatch() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(mainnetFork);
|
|
|
|
assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0);
|
|
}
|
|
|
|
/* ============ hook.postDispatch ============ */
|
|
|
|
function testFork_postDispatch() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(mainnetFork);
|
|
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
uint40 testNonce = 123;
|
|
l1Mailbox.updateLatestDispatchedId(messageId);
|
|
|
|
vm.expectEmit(true, true, true, false, L1_MESSENGER_ADDRESS);
|
|
emit SentMessage(
|
|
address(opISM),
|
|
address(opHook),
|
|
encodedHookData,
|
|
testNonce,
|
|
DEFAULT_GAS_LIMIT
|
|
);
|
|
opHook.postDispatch(testMetadata, encodedMessage);
|
|
}
|
|
|
|
function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(mainnetFork);
|
|
|
|
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 ============ */
|
|
|
|
function testFork_verifyMessageId() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(optimismFork);
|
|
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
(uint240 nonce, uint16 version) = decodeVersionedNonce(
|
|
l2Messenger.messageNonce()
|
|
);
|
|
uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version);
|
|
|
|
bytes32 versionedHash = hashCrossDomainMessageV1(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
0,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
vm.startPrank(
|
|
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)
|
|
);
|
|
|
|
vm.expectEmit(true, false, false, false, address(opISM));
|
|
emit ReceivedMessage(messageId);
|
|
|
|
vm.expectEmit(true, false, false, false, L2_MESSENGER_ADDRESS);
|
|
emit RelayedMessage(versionedHash);
|
|
|
|
l2Messenger.relayMessage(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
0,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
assertTrue(opISM.verifiedMessages(messageId).isBitSet(255));
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function testFork_verifyMessageId_RevertWhen_NotAuthorized() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(optimismFork);
|
|
|
|
// needs to be called by the cannonical messenger on Optimism
|
|
vm.expectRevert(NotCrossChainCall.selector);
|
|
opISM.verifyMessageId(messageId);
|
|
|
|
// set the xDomainMessageSender storage slot as alice
|
|
bytes32 key = bytes32(uint256(204));
|
|
bytes32 value = TypeCasts.addressToBytes32(alice);
|
|
vm.store(address(l2Messenger), key, value);
|
|
|
|
vm.startPrank(L2_MESSENGER_ADDRESS);
|
|
|
|
// needs to be called by the authorized hook contract on Ethereum
|
|
vm.expectRevert(
|
|
"AbstractMessageIdAuthorizedIsm: sender is not the hook"
|
|
);
|
|
opISM.verifyMessageId(messageId);
|
|
}
|
|
|
|
/* ============ ISM.verify ============ */
|
|
|
|
function testFork_verify() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(optimismFork);
|
|
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
(uint240 nonce, uint16 version) = decodeVersionedNonce(
|
|
l2Messenger.messageNonce()
|
|
);
|
|
uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version);
|
|
|
|
vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS));
|
|
l2Messenger.relayMessage(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
0,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
bool verified = opISM.verify(new bytes(0), encodedMessage);
|
|
assertTrue(verified);
|
|
}
|
|
|
|
/// 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(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
(uint240 nonce, uint16 version) = decodeVersionedNonce(
|
|
l2Messenger.messageNonce()
|
|
);
|
|
uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version);
|
|
|
|
vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS));
|
|
l2Messenger.relayMessage{value: _msgValue}(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
_msgValue,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
bool verified = opISM.verify(new bytes(0), encodedMessage);
|
|
assertTrue(verified);
|
|
|
|
assertEq(address(opISM).balance, 0);
|
|
assertEq(address(testRecipient).balance, _msgValue);
|
|
}
|
|
|
|
// sending over invalid message
|
|
function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public {
|
|
deployAll();
|
|
|
|
vm.selectFork(optimismFork);
|
|
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
(uint240 nonce, uint16 version) = decodeVersionedNonce(
|
|
l2Messenger.messageNonce()
|
|
);
|
|
uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version);
|
|
|
|
vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS));
|
|
l2Messenger.relayMessage(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
0,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
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);
|
|
}
|
|
|
|
// 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(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(_messageId)
|
|
);
|
|
|
|
(uint240 nonce, uint16 version) = decodeVersionedNonce(
|
|
l2Messenger.messageNonce()
|
|
);
|
|
uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version);
|
|
|
|
vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS));
|
|
l2Messenger.relayMessage(
|
|
versionedNonce,
|
|
address(opHook),
|
|
address(opISM),
|
|
0,
|
|
DEFAULT_GAS_LIMIT,
|
|
encodedHookData
|
|
);
|
|
|
|
bool verified = opISM.verify(new bytes(0), encodedMessage);
|
|
assertFalse(verified);
|
|
}
|
|
|
|
/* ============ helper functions ============ */
|
|
|
|
function _encodeTestMessage() internal view returns (bytes memory) {
|
|
return
|
|
MessageUtils.formatMessage(
|
|
VERSION,
|
|
uint32(0),
|
|
MAINNET_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(this)),
|
|
OPTIMISM_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(testRecipient)),
|
|
testMessage
|
|
);
|
|
}
|
|
|
|
/// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol
|
|
/// @notice Hashes a cross domain message based on the V1 (current) encoding.
|
|
/// @param _nonce Message nonce.
|
|
/// @param _sender Address of the sender of the message.
|
|
/// @param _target Address of the target of the message.
|
|
/// @param _value ETH value to send to the target.
|
|
/// @param _gasLimit Gas limit to use for the message.
|
|
/// @param _data Data to send with the message.
|
|
/// @return Hashed cross domain message.
|
|
function hashCrossDomainMessageV1(
|
|
uint256 _nonce,
|
|
address _sender,
|
|
address _target,
|
|
uint256 _value,
|
|
uint256 _gasLimit,
|
|
bytes memory _data
|
|
) internal pure returns (bytes32) {
|
|
return
|
|
keccak256(
|
|
encodeCrossDomainMessageV1(
|
|
_nonce,
|
|
_sender,
|
|
_target,
|
|
_value,
|
|
_gasLimit,
|
|
_data
|
|
)
|
|
);
|
|
}
|
|
|
|
/// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol
|
|
/// @notice Encodes a cross domain message based on the V1 (current) encoding.
|
|
/// @param _nonce Message nonce.
|
|
/// @param _sender Address of the sender of the message.
|
|
/// @param _target Address of the target of the message.
|
|
/// @param _value ETH value to send to the target.
|
|
/// @param _gasLimit Gas limit to use for the message.
|
|
/// @param _data Data to send with the message.
|
|
/// @return Encoded cross domain message.
|
|
function encodeCrossDomainMessageV1(
|
|
uint256 _nonce,
|
|
address _sender,
|
|
address _target,
|
|
uint256 _value,
|
|
uint256 _gasLimit,
|
|
bytes memory _data
|
|
) internal pure returns (bytes memory) {
|
|
return
|
|
abi.encodeWithSignature(
|
|
"relayMessage(uint256,address,address,uint256,uint256,bytes)",
|
|
_nonce,
|
|
_sender,
|
|
_target,
|
|
_value,
|
|
_gasLimit,
|
|
_data
|
|
);
|
|
}
|
|
|
|
/// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol
|
|
/// @notice Adds a version number into the first two bytes of a message nonce.
|
|
/// @param _nonce Message nonce to encode into.
|
|
/// @param _version Version number to encode into the message nonce.
|
|
/// @return Message nonce with version encoded into the first two bytes.
|
|
function encodeVersionedNonce(uint240 _nonce, uint16 _version)
|
|
internal
|
|
pure
|
|
returns (uint256)
|
|
{
|
|
uint256 nonce;
|
|
assembly {
|
|
nonce := or(shl(240, _version), _nonce)
|
|
}
|
|
return nonce;
|
|
}
|
|
|
|
/// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol
|
|
/// @notice Pulls the version out of a version-encoded nonce.
|
|
/// @param _nonce Message nonce with version encoded into the first two bytes.
|
|
/// @return Nonce without encoded version.
|
|
/// @return Version of the message.
|
|
function decodeVersionedNonce(uint256 _nonce)
|
|
internal
|
|
pure
|
|
returns (uint240, uint16)
|
|
{
|
|
uint240 nonce;
|
|
uint16 version;
|
|
assembly {
|
|
nonce := and(
|
|
_nonce,
|
|
0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
|
)
|
|
version := shr(240, _nonce)
|
|
}
|
|
return (nonce, version);
|
|
}
|
|
}
|
|
|