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.
446 lines
14 KiB
446 lines
14 KiB
1 year ago
|
// SPDX-License-Identifier: UNLICENSED
|
||
|
pragma solidity ^0.8.13;
|
||
|
|
||
|
import "forge-std/Test.sol";
|
||
|
import "../contracts/test/TestMailbox.sol";
|
||
|
import "../contracts/upgrade/Versioned.sol";
|
||
|
import "../contracts/test/TestPostDispatchHook.sol";
|
||
|
import "../contracts/test/TestIsm.sol";
|
||
|
import "../contracts/test/TestRecipient.sol";
|
||
|
import "../contracts/hooks/MerkleTreeHook.sol";
|
||
|
|
||
|
import {TypeCasts} from "../contracts/libs/TypeCasts.sol";
|
||
|
|
||
|
contract MailboxTest is Test, Versioned {
|
||
|
using TypeCasts for address;
|
||
|
using Message for bytes;
|
||
|
|
||
|
uint32 localDomain = 1;
|
||
|
uint32 remoteDomain = 2;
|
||
|
TestMailbox mailbox;
|
||
|
|
||
|
MerkleTreeHook merkleHook;
|
||
|
|
||
|
TestPostDispatchHook defaultHook;
|
||
|
TestPostDispatchHook overrideHook;
|
||
|
TestPostDispatchHook requiredHook;
|
||
|
|
||
|
TestIsm defaultIsm;
|
||
|
TestRecipient recipient;
|
||
|
bytes32 recipientb32;
|
||
|
|
||
|
address owner;
|
||
|
|
||
|
function setUp() public {
|
||
|
mailbox = new TestMailbox(localDomain);
|
||
|
recipient = new TestRecipient();
|
||
|
recipientb32 = address(recipient).addressToBytes32();
|
||
|
defaultHook = new TestPostDispatchHook();
|
||
|
merkleHook = new MerkleTreeHook(address(mailbox));
|
||
|
requiredHook = new TestPostDispatchHook();
|
||
|
overrideHook = new TestPostDispatchHook();
|
||
|
defaultIsm = new TestIsm();
|
||
|
|
||
|
owner = msg.sender;
|
||
|
mailbox.initialize(
|
||
|
owner,
|
||
|
address(defaultIsm),
|
||
|
address(defaultHook),
|
||
|
address(requiredHook)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function test_localDomain() public {
|
||
|
assertEq(mailbox.localDomain(), localDomain);
|
||
|
}
|
||
|
|
||
|
function test_initialize() public {
|
||
|
assertEq(mailbox.owner(), owner);
|
||
|
assertEq(address(mailbox.defaultIsm()), address(defaultIsm));
|
||
|
assertEq(address(mailbox.defaultHook()), address(defaultHook));
|
||
|
assertEq(address(mailbox.requiredHook()), address(requiredHook));
|
||
|
}
|
||
|
|
||
|
function test_initialize_revertsWhenCalledTwice() public {
|
||
|
vm.expectRevert("Initializable: contract is already initialized");
|
||
|
mailbox.initialize(
|
||
|
owner,
|
||
|
address(defaultIsm),
|
||
|
address(defaultHook),
|
||
|
address(requiredHook)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function test_recipientIsm() public {
|
||
|
IInterchainSecurityModule ism = mailbox.recipientIsm(
|
||
|
address(recipient)
|
||
|
);
|
||
|
assertEq(address(mailbox.defaultIsm()), address(ism));
|
||
|
TestIsm newIsm = new TestIsm();
|
||
|
recipient.setInterchainSecurityModule(address(newIsm));
|
||
|
ism = mailbox.recipientIsm(address(recipient));
|
||
|
assertEq(address(ism), address(newIsm));
|
||
|
}
|
||
|
|
||
|
event DefaultIsmSet(address indexed module);
|
||
|
|
||
|
function test_setDefaultIsm() public {
|
||
|
TestIsm newIsm = new TestIsm();
|
||
|
|
||
|
// prank owner
|
||
|
vm.startPrank(owner);
|
||
|
vm.expectEmit(true, false, false, false, address(mailbox));
|
||
|
emit DefaultIsmSet(address(newIsm));
|
||
|
mailbox.setDefaultIsm(address(newIsm));
|
||
|
assertEq(address(mailbox.defaultIsm()), address(newIsm));
|
||
|
|
||
|
vm.expectRevert("Mailbox: default ISM not contract");
|
||
|
mailbox.setDefaultIsm(owner);
|
||
|
vm.stopPrank();
|
||
|
|
||
|
vm.expectRevert("Ownable: caller is not the owner");
|
||
|
mailbox.setDefaultIsm(address(newIsm));
|
||
|
}
|
||
|
|
||
|
event DefaultHookSet(address indexed module);
|
||
|
|
||
|
function test_setDefaultHook() public {
|
||
|
TestPostDispatchHook newHook = new TestPostDispatchHook();
|
||
|
|
||
|
// prank owner
|
||
|
vm.startPrank(owner);
|
||
|
vm.expectEmit(true, false, false, false, address(mailbox));
|
||
|
emit DefaultHookSet(address(newHook));
|
||
|
mailbox.setDefaultHook(address(newHook));
|
||
|
assertEq(address(mailbox.defaultHook()), address(newHook));
|
||
|
|
||
|
vm.expectRevert("Mailbox: default hook not contract");
|
||
|
mailbox.setDefaultHook(owner);
|
||
|
vm.stopPrank();
|
||
|
|
||
|
vm.expectRevert("Ownable: caller is not the owner");
|
||
|
mailbox.setDefaultHook(address(newHook));
|
||
|
}
|
||
|
|
||
|
event RequiredHookSet(address indexed module);
|
||
|
|
||
|
function test_setRequiredHook() public {
|
||
|
TestPostDispatchHook newHook = new TestPostDispatchHook();
|
||
|
|
||
|
// prank owner
|
||
|
vm.startPrank(owner);
|
||
|
vm.expectEmit(true, false, false, false, address(mailbox));
|
||
|
emit RequiredHookSet(address(newHook));
|
||
|
mailbox.setRequiredHook(address(newHook));
|
||
|
assertEq(address(mailbox.requiredHook()), address(newHook));
|
||
|
|
||
|
vm.expectRevert("Mailbox: required hook not contract");
|
||
|
mailbox.setRequiredHook(owner);
|
||
|
vm.stopPrank();
|
||
|
|
||
|
vm.expectRevert("Ownable: caller is not the owner");
|
||
|
mailbox.setRequiredHook(address(newHook));
|
||
|
}
|
||
|
|
||
|
function expectHookQuote(
|
||
|
IPostDispatchHook hook,
|
||
|
bytes calldata metadata,
|
||
|
bytes memory message
|
||
|
) internal {
|
||
|
vm.expectCall(
|
||
|
address(hook),
|
||
|
abi.encodeCall(IPostDispatchHook.quoteDispatch, (metadata, message))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function expectHookPost(
|
||
|
IPostDispatchHook hook,
|
||
|
bytes calldata metadata,
|
||
|
bytes memory message,
|
||
|
uint256 value
|
||
|
) internal {
|
||
|
vm.expectCall(
|
||
|
address(hook),
|
||
|
value,
|
||
|
abi.encodeCall(IPostDispatchHook.postDispatch, (metadata, message))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function test_quoteDispatch(
|
||
|
uint256 requiredFee,
|
||
|
uint256 defaultFee,
|
||
|
uint256 overrideFee,
|
||
|
bytes calldata body,
|
||
|
bytes calldata metadata
|
||
|
) public {
|
||
|
vm.assume(
|
||
|
requiredFee < type(uint128).max &&
|
||
|
defaultFee < type(uint128).max &&
|
||
|
overrideFee < type(uint128).max
|
||
|
);
|
||
|
defaultHook.setFee(defaultFee);
|
||
|
requiredHook.setFee(requiredFee);
|
||
|
overrideHook.setFee(overrideFee);
|
||
|
|
||
|
bytes memory message = mailbox.buildOutboundMessage(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body
|
||
|
);
|
||
|
bytes calldata defaultMetadata = metadata[0:0];
|
||
|
|
||
|
expectHookQuote(requiredHook, defaultMetadata, message);
|
||
|
expectHookQuote(defaultHook, defaultMetadata, message);
|
||
|
uint256 quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
address(recipient).addressToBytes32(),
|
||
|
body
|
||
|
);
|
||
|
assertEq(quote, defaultFee + requiredFee);
|
||
|
|
||
|
expectHookQuote(requiredHook, metadata, message);
|
||
|
expectHookQuote(defaultHook, metadata, message);
|
||
|
quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
address(recipient).addressToBytes32(),
|
||
|
body,
|
||
|
metadata
|
||
|
);
|
||
|
assertEq(quote, defaultFee + requiredFee);
|
||
|
|
||
|
expectHookQuote(requiredHook, metadata, message);
|
||
|
expectHookQuote(overrideHook, metadata, message);
|
||
|
quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
address(recipient).addressToBytes32(),
|
||
|
body,
|
||
|
metadata,
|
||
|
overrideHook
|
||
|
);
|
||
|
assertEq(quote, overrideFee + requiredFee);
|
||
|
}
|
||
|
|
||
|
event Dispatch(
|
||
|
address indexed sender,
|
||
|
uint32 indexed destination,
|
||
|
bytes32 indexed recipient,
|
||
|
bytes message
|
||
|
);
|
||
|
|
||
|
event DispatchId(bytes32 indexed messageId);
|
||
|
|
||
|
function expectDispatch(
|
||
|
TestPostDispatchHook firstHook,
|
||
|
TestPostDispatchHook hook,
|
||
|
bytes calldata metadata,
|
||
|
bytes calldata body
|
||
|
) internal {
|
||
|
bytes memory message = mailbox.buildOutboundMessage(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body
|
||
|
);
|
||
|
expectHookQuote(firstHook, metadata, message);
|
||
|
expectHookPost(firstHook, metadata, message, firstHook.fee());
|
||
|
expectHookPost(hook, metadata, message, hook.fee());
|
||
|
vm.expectEmit(true, true, true, true, address(mailbox));
|
||
|
emit Dispatch(address(this), remoteDomain, recipientb32, message);
|
||
|
vm.expectEmit(true, false, false, false, address(mailbox));
|
||
|
emit DispatchId(message.id());
|
||
|
}
|
||
|
|
||
|
function test_dispatch(
|
||
|
uint8 n,
|
||
|
bytes calldata body,
|
||
|
bytes calldata metadata
|
||
|
) public {
|
||
|
bytes calldata defaultMetadata = metadata[0:0];
|
||
|
uint256 quote;
|
||
|
uint32 nonce;
|
||
|
bytes32 id;
|
||
|
|
||
|
for (uint256 i = 0; i < n; i += 3) {
|
||
|
nonce = mailbox.nonce();
|
||
|
assertEq(nonce, i);
|
||
|
|
||
|
// default hook and no metadata
|
||
|
quote = mailbox.quoteDispatch(remoteDomain, recipientb32, body);
|
||
|
expectDispatch(requiredHook, defaultHook, defaultMetadata, body);
|
||
|
id = mailbox.dispatch{value: quote}(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body
|
||
|
);
|
||
|
assertEq(mailbox.latestDispatchedId(), id);
|
||
|
nonce = mailbox.nonce();
|
||
|
assertEq(nonce, i + 1);
|
||
|
|
||
|
// default hook with metadata
|
||
|
quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
metadata
|
||
|
);
|
||
|
expectDispatch(requiredHook, defaultHook, metadata, body);
|
||
|
id = mailbox.dispatch{value: quote}(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
metadata
|
||
|
);
|
||
|
assertEq(mailbox.latestDispatchedId(), id);
|
||
|
nonce = mailbox.nonce();
|
||
|
assertEq(nonce, i + 2);
|
||
|
|
||
|
// override default hook with metadata
|
||
|
quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
metadata,
|
||
|
overrideHook
|
||
|
);
|
||
|
expectDispatch(requiredHook, overrideHook, metadata, body);
|
||
|
id = mailbox.dispatch{value: quote}(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
metadata,
|
||
|
overrideHook
|
||
|
);
|
||
|
assertEq(mailbox.latestDispatchedId(), id);
|
||
|
nonce = mailbox.nonce();
|
||
|
assertEq(nonce, i + 3);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// for instrumenting gas costs of merkleHook.postDispatch after several insertions
|
||
|
function test_100dispatch_withMerkleTreeHook(bytes calldata body) public {
|
||
|
uint256 quote = mailbox.quoteDispatch(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
body[0:0],
|
||
|
merkleHook
|
||
|
);
|
||
|
for (uint256 i = 0; i < 100; i++) {
|
||
|
mailbox.dispatch{value: quote}(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body,
|
||
|
body[0:0],
|
||
|
merkleHook
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
event ProcessId(bytes32 indexed messageId);
|
||
|
|
||
|
event Process(
|
||
|
uint32 indexed origin,
|
||
|
bytes32 indexed sender,
|
||
|
address indexed recipient
|
||
|
);
|
||
|
|
||
|
function expectProcess(
|
||
|
bytes calldata metadata,
|
||
|
bytes memory message,
|
||
|
bytes calldata body,
|
||
|
uint256 value
|
||
|
) internal {
|
||
|
bytes32 sender = msg.sender.addressToBytes32();
|
||
|
IInterchainSecurityModule ism = mailbox.recipientIsm(
|
||
|
address(recipient)
|
||
|
);
|
||
|
vm.expectEmit(true, true, true, false, address(mailbox));
|
||
|
emit Process(remoteDomain, sender, address(recipient));
|
||
|
vm.expectEmit(true, false, false, false, address(mailbox));
|
||
|
emit ProcessId(message.id());
|
||
|
vm.expectCall(
|
||
|
address(ism),
|
||
|
abi.encodeCall(ism.verify, (metadata, message))
|
||
|
);
|
||
|
vm.expectCall(
|
||
|
address(recipient),
|
||
|
value,
|
||
|
abi.encodeCall(recipient.handle, (remoteDomain, sender, body))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function test_process(
|
||
|
bytes calldata body,
|
||
|
bytes calldata metadata,
|
||
|
uint256 value
|
||
|
) public {
|
||
|
vm.assume(value < address(this).balance);
|
||
|
bytes memory message = mailbox.buildInboundMessage(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
msg.sender.addressToBytes32(),
|
||
|
body
|
||
|
);
|
||
|
bytes32 id = keccak256(message);
|
||
|
assertEq(mailbox.delivered(id), false);
|
||
|
expectProcess(metadata, message, body, value);
|
||
|
mailbox.process{value: value}(metadata, message);
|
||
|
assertEq(mailbox.delivered(id), true);
|
||
|
assertEq(mailbox.processor(id), address(this));
|
||
|
assertEq(mailbox.processedAt(id), uint48(block.timestamp));
|
||
|
}
|
||
|
|
||
|
function test_process_revertsWhenAlreadyDelivered() public {
|
||
|
bytes memory message = mailbox.buildInboundMessage(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
address(this).addressToBytes32(),
|
||
|
"0x"
|
||
|
);
|
||
|
mailbox.process("", message);
|
||
|
vm.expectRevert("Mailbox: already delivered");
|
||
|
mailbox.process("", message);
|
||
|
}
|
||
|
|
||
|
function test_process_revertsWhenBadVersion(bytes calldata body) public {
|
||
|
bytes memory message = Message.formatMessage(
|
||
|
VERSION + 1,
|
||
|
0,
|
||
|
localDomain,
|
||
|
address(this).addressToBytes32(),
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body
|
||
|
);
|
||
|
vm.expectRevert("Mailbox: bad version");
|
||
|
mailbox.process("", message);
|
||
|
}
|
||
|
|
||
|
function test_process_revertsWhenBadDestination(bytes calldata body)
|
||
|
public
|
||
|
{
|
||
|
bytes memory message = Message.formatMessage(
|
||
|
VERSION,
|
||
|
0,
|
||
|
remoteDomain,
|
||
|
address(this).addressToBytes32(),
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
body
|
||
|
);
|
||
|
vm.expectRevert("Mailbox: unexpected destination");
|
||
|
mailbox.process("", message);
|
||
|
}
|
||
|
|
||
|
function test_process_revertsWhenISMFails(bytes calldata body) public {
|
||
|
bytes memory message = mailbox.buildInboundMessage(
|
||
|
remoteDomain,
|
||
|
recipientb32,
|
||
|
msg.sender.addressToBytes32(),
|
||
|
body
|
||
|
);
|
||
|
defaultIsm.setVerify(false);
|
||
|
vm.expectRevert("Mailbox: ISM verification failed");
|
||
|
mailbox.process("", message);
|
||
|
}
|
||
|
}
|