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.
320 lines
10 KiB
320 lines
10 KiB
// SPDX-License-Identifier: MIT or Apache-2.0
|
|
pragma solidity ^0.8.13;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
|
|
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
|
|
import {Message} from "../../contracts/libs/Message.sol";
|
|
import {MessageUtils} from "./IsmTestUtils.sol";
|
|
import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol";
|
|
import {IOptimismPortal} from "../../contracts/interfaces/optimism/IOptimismPortal.sol";
|
|
import {ICrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol";
|
|
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol";
|
|
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
|
|
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
|
|
import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol";
|
|
import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol";
|
|
import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol";
|
|
|
|
contract OPL2ToL1IsmTest is Test {
|
|
uint8 internal constant HYPERLANE_VERSION = 1;
|
|
uint32 internal constant MAINNET_DOMAIN = 1;
|
|
uint32 internal constant OPTIMISM_DOMAIN = 10;
|
|
uint32 internal constant GAS_QUOTE = 120_000;
|
|
|
|
address internal constant L2_MESSENGER_ADDRESS =
|
|
0x4200000000000000000000000000000000000007;
|
|
|
|
uint256 internal constant MOCK_NONCE = 0;
|
|
|
|
TestMailbox public l2Mailbox;
|
|
TestRecipient internal testRecipient;
|
|
bytes internal testMessage =
|
|
abi.encodePacked("Hello from the other chain!");
|
|
bytes internal encodedMessage;
|
|
bytes internal testMetadata =
|
|
StandardHookMetadata.overrideRefundAddress(address(this));
|
|
bytes32 internal messageId;
|
|
|
|
MockOptimismPortal internal portal;
|
|
MockOptimismMessenger internal l1Messenger;
|
|
OPL2ToL1Hook public hook;
|
|
OPL2ToL1Ism public ism;
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
/// SETUP ///
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
function setUp() public {
|
|
// Optimism messenger mock setup
|
|
vm.etch(
|
|
L2_MESSENGER_ADDRESS,
|
|
address(new MockOptimismMessenger()).code
|
|
);
|
|
|
|
testRecipient = new TestRecipient();
|
|
|
|
encodedMessage = _encodeTestMessage();
|
|
messageId = Message.id(encodedMessage);
|
|
}
|
|
|
|
function deployHook() public {
|
|
l2Mailbox = new TestMailbox(OPTIMISM_DOMAIN);
|
|
hook = new OPL2ToL1Hook(
|
|
address(l2Mailbox),
|
|
MAINNET_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(ism)),
|
|
L2_MESSENGER_ADDRESS,
|
|
GAS_QUOTE
|
|
);
|
|
}
|
|
|
|
function deployIsm() public {
|
|
l1Messenger = new MockOptimismMessenger();
|
|
portal = new MockOptimismPortal();
|
|
l1Messenger.setPORTAL(address(portal));
|
|
|
|
ism = new OPL2ToL1Ism(address(l1Messenger));
|
|
}
|
|
|
|
function deployAll() public {
|
|
deployIsm();
|
|
deployHook();
|
|
|
|
l1Messenger.setXDomainMessageSender(address(hook));
|
|
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
|
|
}
|
|
|
|
function test_postDispatch() public {
|
|
deployAll();
|
|
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(messageId)
|
|
);
|
|
|
|
l2Mailbox.updateLatestDispatchedId(messageId);
|
|
|
|
vm.expectCall(
|
|
L2_MESSENGER_ADDRESS,
|
|
abi.encodeCall(
|
|
ICrossDomainMessenger.sendMessage,
|
|
(address(ism), encodedHookData, GAS_QUOTE)
|
|
)
|
|
);
|
|
|
|
hook.postDispatch{value: GAS_QUOTE}(testMetadata, encodedMessage);
|
|
}
|
|
|
|
function testFork_postDispatch_revertWhen_chainIDNotSupported() public {
|
|
deployAll();
|
|
|
|
bytes memory message = MessageUtils.formatMessage(
|
|
0,
|
|
uint32(0),
|
|
OPTIMISM_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(this)),
|
|
2, // wrong domain
|
|
TypeCasts.addressToBytes32(address(testRecipient)),
|
|
testMessage
|
|
);
|
|
|
|
l2Mailbox.updateLatestDispatchedId(Message.id(message));
|
|
vm.expectRevert(
|
|
"AbstractMessageIdAuthHook: invalid destination domain"
|
|
);
|
|
hook.postDispatch(testMetadata, message);
|
|
}
|
|
|
|
function test_postDispatch_revertWhen_notLastDispatchedMessage() public {
|
|
deployAll();
|
|
|
|
vm.expectRevert(
|
|
"AbstractMessageIdAuthHook: message not latest dispatched"
|
|
);
|
|
hook.postDispatch(testMetadata, encodedMessage);
|
|
}
|
|
|
|
function test_verify_directWithdrawalCall() public {
|
|
deployAll();
|
|
|
|
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx(
|
|
address(ism),
|
|
0,
|
|
messageId
|
|
);
|
|
|
|
assertTrue(ism.verify(encodedWithdrawalTx, encodedMessage));
|
|
}
|
|
|
|
function test_verify_directWithdrawalCall_revertsWhen_invalidSender()
|
|
public
|
|
{
|
|
deployAll();
|
|
l1Messenger.setXDomainMessageSender(address(this));
|
|
|
|
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx(
|
|
address(ism),
|
|
0,
|
|
messageId
|
|
);
|
|
|
|
vm.expectRevert(); // evmRevert in MockOptimismPortal
|
|
ism.verify(encodedWithdrawalTx, encodedMessage);
|
|
}
|
|
|
|
function test_verify_statefulVerify() public {
|
|
deployAll();
|
|
|
|
vm.deal(address(portal), 1 ether);
|
|
IOptimismPortal.WithdrawalTransaction
|
|
memory withdrawal = IOptimismPortal.WithdrawalTransaction({
|
|
nonce: MOCK_NONCE,
|
|
sender: L2_MESSENGER_ADDRESS,
|
|
target: address(l1Messenger),
|
|
value: 1 ether,
|
|
gasLimit: uint256(GAS_QUOTE),
|
|
data: _encodeMessengerCalldata(address(ism), 1 ether, messageId)
|
|
});
|
|
portal.finalizeWithdrawalTransaction(withdrawal);
|
|
|
|
vm.etch(address(portal), new bytes(0)); // this is a way to test that the portal isn't called again
|
|
assertTrue(ism.verify(new bytes(0), encodedMessage));
|
|
assertEq(address(testRecipient).balance, 1 ether); // testing msg.value
|
|
}
|
|
|
|
function test_verify_statefulAndDirectWithdrawal() public {
|
|
deployAll();
|
|
|
|
IOptimismPortal.WithdrawalTransaction
|
|
memory withdrawal = IOptimismPortal.WithdrawalTransaction({
|
|
nonce: MOCK_NONCE,
|
|
sender: L2_MESSENGER_ADDRESS,
|
|
target: address(l1Messenger),
|
|
value: 0,
|
|
gasLimit: uint256(GAS_QUOTE),
|
|
data: _encodeMessengerCalldata(address(ism), 0, messageId)
|
|
});
|
|
portal.finalizeWithdrawalTransaction(withdrawal);
|
|
|
|
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx(
|
|
address(ism),
|
|
0,
|
|
messageId
|
|
);
|
|
|
|
vm.etch(address(portal), new bytes(0)); // this is a way to test that the portal isn't called again
|
|
assertTrue(ism.verify(encodedWithdrawalTx, encodedMessage));
|
|
}
|
|
|
|
function test_verify_revertsWhen_noStatefulAndDirectWithdrawal() public {
|
|
deployAll();
|
|
|
|
vm.expectRevert();
|
|
ism.verify(new bytes(0), encodedMessage);
|
|
}
|
|
|
|
function test_verify_revertsWhen_invalidIsm() public {
|
|
deployAll();
|
|
|
|
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx(
|
|
address(this),
|
|
0,
|
|
messageId
|
|
);
|
|
|
|
vm.expectRevert(); // evmRevert in MockOptimismPortal
|
|
ism.verify(encodedWithdrawalTx, encodedMessage);
|
|
}
|
|
|
|
function test_verify_revertsWhen_incorrectMessageId() public {
|
|
deployAll();
|
|
|
|
bytes32 incorrectMessageId = keccak256("incorrect message id");
|
|
|
|
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx(
|
|
address(this),
|
|
0,
|
|
incorrectMessageId
|
|
);
|
|
|
|
// through portal call
|
|
vm.expectRevert("OPL2ToL1Ism: invalid message id");
|
|
ism.verify(encodedWithdrawalTx, encodedMessage);
|
|
|
|
// through statefulVerify
|
|
IOptimismPortal.WithdrawalTransaction
|
|
memory withdrawal = IOptimismPortal.WithdrawalTransaction({
|
|
nonce: MOCK_NONCE,
|
|
sender: L2_MESSENGER_ADDRESS,
|
|
target: address(l1Messenger),
|
|
value: 0,
|
|
gasLimit: uint256(GAS_QUOTE),
|
|
data: _encodeMessengerCalldata(
|
|
address(ism),
|
|
0,
|
|
incorrectMessageId
|
|
)
|
|
});
|
|
portal.finalizeWithdrawalTransaction(withdrawal);
|
|
|
|
vm.etch(address(portal), new bytes(0)); // to stop the portal route
|
|
vm.expectRevert(); // evmRevert()
|
|
assertFalse(ism.verify(new bytes(0), encodedMessage));
|
|
}
|
|
|
|
/* ============ helper functions ============ */
|
|
|
|
function _encodeTestMessage() internal view returns (bytes memory) {
|
|
return
|
|
MessageUtils.formatMessage(
|
|
HYPERLANE_VERSION,
|
|
uint32(0),
|
|
OPTIMISM_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(this)),
|
|
MAINNET_DOMAIN,
|
|
TypeCasts.addressToBytes32(address(testRecipient)),
|
|
testMessage
|
|
);
|
|
}
|
|
|
|
function _encodeMessengerCalldata(
|
|
address _ism,
|
|
uint256 _value,
|
|
bytes32 _messageId
|
|
) internal view returns (bytes memory) {
|
|
bytes memory encodedHookData = abi.encodeCall(
|
|
AbstractMessageIdAuthorizedIsm.verifyMessageId,
|
|
(_messageId)
|
|
);
|
|
|
|
return
|
|
abi.encodeCall(
|
|
ICrossDomainMessenger.relayMessage,
|
|
(
|
|
MOCK_NONCE,
|
|
address(hook),
|
|
_ism,
|
|
_value,
|
|
uint256(GAS_QUOTE),
|
|
encodedHookData
|
|
)
|
|
);
|
|
}
|
|
|
|
function _encodeFinalizeWithdrawalTx(
|
|
address _ism,
|
|
uint256 _value,
|
|
bytes32 _messageId
|
|
) internal view returns (bytes memory) {
|
|
return
|
|
abi.encode(
|
|
MOCK_NONCE,
|
|
L2_MESSENGER_ADDRESS,
|
|
l1Messenger,
|
|
_value,
|
|
uint256(GAS_QUOTE),
|
|
_encodeMessengerCalldata(_ism, _value, _messageId)
|
|
);
|
|
}
|
|
}
|
|
|