Add V2 Mailbox and Message contracts (#1185)
parent
57d84c91f6
commit
322c18e8d7
@ -0,0 +1,225 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.8.0; |
||||||
|
|
||||||
|
// ============ Internal Imports ============ |
||||||
|
import {Versioned} from "./upgrade/Versioned.sol"; |
||||||
|
import {MerkleLib} from "./libs/Merkle.sol"; |
||||||
|
import {MessageV2} from "./libs/MessageV2.sol"; |
||||||
|
import {TypeCasts} from "./libs/TypeCasts.sol"; |
||||||
|
import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol"; |
||||||
|
import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule.sol"; |
||||||
|
import {IMailboxV2} from "../interfaces/IMailboxV2.sol"; |
||||||
|
|
||||||
|
// ============ External Imports ============ |
||||||
|
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
||||||
|
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
||||||
|
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; |
||||||
|
|
||||||
|
contract MailboxV2 is |
||||||
|
IMailboxV2, |
||||||
|
OwnableUpgradeable, |
||||||
|
ReentrancyGuardUpgradeable, |
||||||
|
Versioned |
||||||
|
{ |
||||||
|
// ============ Libraries ============ |
||||||
|
|
||||||
|
using MerkleLib for MerkleLib.Tree; |
||||||
|
using MessageV2 for bytes; |
||||||
|
using TypeCasts for bytes32; |
||||||
|
using TypeCasts for address; |
||||||
|
|
||||||
|
// ============ Constants ============ |
||||||
|
|
||||||
|
// Maximum bytes per message = 2 KiB (somewhat arbitrarily set to begin) |
||||||
|
uint256 public constant MAX_MESSAGE_BODY_BYTES = 2 * 2**10; |
||||||
|
// Domain of chain on which the contract is deployed |
||||||
|
uint32 public immutable localDomain; |
||||||
|
|
||||||
|
// ============ Public Storage ============ |
||||||
|
|
||||||
|
// The default ISM, used if the recipient fails to specify one. |
||||||
|
IInterchainSecurityModule public defaultModule; |
||||||
|
// An incremental merkle tree used to store outbound message IDs. |
||||||
|
MerkleLib.Tree public tree; |
||||||
|
// Mapping of message ID to whether or not that message has been delivered. |
||||||
|
mapping(bytes32 => bool) public delivered; |
||||||
|
|
||||||
|
// ============ Upgrade Gap ============ |
||||||
|
|
||||||
|
// gap for upgrade safety |
||||||
|
uint256[47] private __GAP; |
||||||
|
|
||||||
|
// ============ Events ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when the default ISM is updated |
||||||
|
* @param module The new default ISM |
||||||
|
*/ |
||||||
|
event DefaultModuleSet(address indexed module); |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when a new message is dispatched via Hyperlane |
||||||
|
* @param messageId The unique message identifier |
||||||
|
* @param message Raw bytes of message |
||||||
|
*/ |
||||||
|
event Dispatch(bytes32 indexed messageId, bytes message); |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when a Hyperlane message is delivered |
||||||
|
* @param messageId The unique message identifier |
||||||
|
*/ |
||||||
|
event Process(bytes32 indexed messageId); |
||||||
|
|
||||||
|
// ============ Constructor ============ |
||||||
|
|
||||||
|
// solhint-disable-next-line no-empty-blocks |
||||||
|
constructor(uint32 _localDomain) { |
||||||
|
localDomain = _localDomain; |
||||||
|
} |
||||||
|
|
||||||
|
// ============ Initializer ============ |
||||||
|
|
||||||
|
function initialize(address _defaultModule) external initializer { |
||||||
|
__ReentrancyGuard_init(); |
||||||
|
__Ownable_init(); |
||||||
|
_setDefaultModule(_defaultModule); |
||||||
|
} |
||||||
|
|
||||||
|
// ============ External Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Sets the default ISM for the Mailbox. |
||||||
|
* @param _module The new default ISM. Must be a contract. |
||||||
|
*/ |
||||||
|
function setDefaultModule(address _module) external onlyOwner { |
||||||
|
_setDefaultModule(_module); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Dispatches a message to the destination domain & recipient. |
||||||
|
* @param _destinationDomain Domain of destination chain |
||||||
|
* @param _recipientAddress Address of recipient on destination chain as bytes32 |
||||||
|
* @param _messageBody Raw bytes content of message body |
||||||
|
* @return The message ID inserted into the Mailbox's merkle tree |
||||||
|
*/ |
||||||
|
function dispatch( |
||||||
|
uint32 _destinationDomain, |
||||||
|
bytes32 _recipientAddress, |
||||||
|
bytes calldata _messageBody |
||||||
|
) external override returns (bytes32) { |
||||||
|
require(_messageBody.length <= MAX_MESSAGE_BODY_BYTES, "msg too long"); |
||||||
|
// Format the message into packed bytes. |
||||||
|
bytes memory _message = MessageV2.formatMessage( |
||||||
|
VERSION, |
||||||
|
count(), |
||||||
|
localDomain, |
||||||
|
msg.sender.addressToBytes32(), |
||||||
|
_destinationDomain, |
||||||
|
_recipientAddress, |
||||||
|
_messageBody |
||||||
|
); |
||||||
|
|
||||||
|
// Insert the message ID into the merkle tree. |
||||||
|
bytes32 _id = _message.id(); |
||||||
|
tree.insert(_id); |
||||||
|
emit Dispatch(_id, _message); |
||||||
|
return _id; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Attempts to deliver `_message` to its recipient. Verifies |
||||||
|
* `_message` via the recipient's ISM using the provided `_metadata`. |
||||||
|
* @param _metadata Metadata used by the ISM to verify `_message`. |
||||||
|
* @param _message Formatted Hyperlane message (refer to Message.sol). |
||||||
|
*/ |
||||||
|
function process(bytes calldata _metadata, bytes calldata _message) |
||||||
|
external |
||||||
|
override |
||||||
|
nonReentrant |
||||||
|
{ |
||||||
|
// Check that the message was intended for this mailbox. |
||||||
|
require(_message.version() == VERSION, "!version"); |
||||||
|
require(_message.destination() == localDomain, "!destination"); |
||||||
|
|
||||||
|
// Check that the message hasn't already been delivered. |
||||||
|
bytes32 _id = _message.id(); |
||||||
|
require(delivered[_id] == false, "delivered"); |
||||||
|
delivered[_id] = true; |
||||||
|
|
||||||
|
// Verify the message via the ISM. |
||||||
|
IInterchainSecurityModule _ism = _recipientModule( |
||||||
|
ISpecifiesInterchainSecurityModule(_message.recipientAddress()) |
||||||
|
); |
||||||
|
require(_ism.verify(_metadata, _message), "!module"); |
||||||
|
|
||||||
|
// Deliver the message to the recipient. |
||||||
|
uint32 _origin = _message.origin(); |
||||||
|
IMessageRecipient(_message.recipientAddress()).handle( |
||||||
|
_origin, |
||||||
|
_message.sender(), |
||||||
|
_message.body() |
||||||
|
); |
||||||
|
emit Process(_id); |
||||||
|
} |
||||||
|
|
||||||
|
// ============ Public Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Calculates and returns tree's current root |
||||||
|
*/ |
||||||
|
function root() public view returns (bytes32) { |
||||||
|
return tree.root(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the number of inserted leaves in the tree |
||||||
|
*/ |
||||||
|
function count() public view returns (uint256) { |
||||||
|
return tree.count; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns a checkpoint representing the current merkle tree. |
||||||
|
* @return root The root of the Outbox's merkle tree. |
||||||
|
* @return index The index of the last element in the tree. |
||||||
|
*/ |
||||||
|
function latestCheckpoint() public view returns (bytes32, uint256) { |
||||||
|
return (root(), count() - 1); |
||||||
|
} |
||||||
|
|
||||||
|
// ============ Internal Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Sets the default ISM for the Mailbox. |
||||||
|
* @param _module The new default ISM. Must be a contract. |
||||||
|
*/ |
||||||
|
function _setDefaultModule(address _module) internal { |
||||||
|
require(Address.isContract(_module), "!contract"); |
||||||
|
defaultModule = IInterchainSecurityModule(_module); |
||||||
|
emit DefaultModuleSet(_module); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the ISM to use for the recipient, defaulting to the |
||||||
|
* default ISM if none is specified. |
||||||
|
* @param _recipient The message recipient whose ISM should be returned. |
||||||
|
* @return The ISM to use for `_recipient`. |
||||||
|
*/ |
||||||
|
function _recipientModule(ISpecifiesInterchainSecurityModule _recipient) |
||||||
|
internal |
||||||
|
view |
||||||
|
returns (IInterchainSecurityModule) |
||||||
|
{ |
||||||
|
// Use a default interchainSecurityModule if one is not specified by the |
||||||
|
// recipient. |
||||||
|
// This is useful for backwards compatibility and for convenience as |
||||||
|
// recipients are not mandated to specify an ISM. |
||||||
|
try _recipient.interchainSecurityModule() returns ( |
||||||
|
IInterchainSecurityModule _val |
||||||
|
) { |
||||||
|
return _val; |
||||||
|
} catch { |
||||||
|
return defaultModule; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.8.0; |
||||||
|
|
||||||
|
import {TypeCasts} from "./TypeCasts.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @title Hyperlane Message Library |
||||||
|
* @notice Library for formatted messages used by Mailbox |
||||||
|
**/ |
||||||
|
library MessageV2 { |
||||||
|
using TypeCasts for bytes32; |
||||||
|
|
||||||
|
uint256 private constant VERSION_OFFSET = 0; |
||||||
|
uint256 private constant NONCE_OFFSET = 1; |
||||||
|
uint256 private constant ORIGIN_OFFSET = 33; |
||||||
|
uint256 private constant SENDER_OFFSET = 37; |
||||||
|
uint256 private constant DESTINATION_OFFSET = 69; |
||||||
|
uint256 private constant RECIPIENT_OFFSET = 73; |
||||||
|
uint256 private constant BODY_OFFSET = 105; |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns formatted (packed) Hyperlane message with provided fields |
||||||
|
* @dev This function should only be used in memory message construction. |
||||||
|
* @param _version The version of the origin and destination Mailboxes |
||||||
|
* @param _nonce A nonce to uniquely identify the message on its origin chain |
||||||
|
* @param _originDomain Domain of origin chain |
||||||
|
* @param _sender Address of sender as bytes32 |
||||||
|
* @param _destinationDomain Domain of destination chain |
||||||
|
* @param _recipient Address of recipient on destination chain as bytes32 |
||||||
|
* @param _messageBody Raw bytes of message body |
||||||
|
* @return Formatted message |
||||||
|
*/ |
||||||
|
function formatMessage( |
||||||
|
uint8 _version, |
||||||
|
uint256 _nonce, |
||||||
|
uint32 _originDomain, |
||||||
|
bytes32 _sender, |
||||||
|
uint32 _destinationDomain, |
||||||
|
bytes32 _recipient, |
||||||
|
bytes calldata _messageBody |
||||||
|
) internal pure returns (bytes memory) { |
||||||
|
return |
||||||
|
abi.encodePacked( |
||||||
|
_version, |
||||||
|
_nonce, |
||||||
|
_originDomain, |
||||||
|
_sender, |
||||||
|
_destinationDomain, |
||||||
|
_recipient, |
||||||
|
_messageBody |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message ID. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return ID of `_message` |
||||||
|
*/ |
||||||
|
function id(bytes memory _message) internal pure returns (bytes32) { |
||||||
|
return keccak256(_message); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message version. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Version of `_message` |
||||||
|
*/ |
||||||
|
function version(bytes calldata _message) internal pure returns (uint8) { |
||||||
|
return uint8(bytes1(_message[VERSION_OFFSET:NONCE_OFFSET])); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message nonce. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Nonce of `_message` |
||||||
|
*/ |
||||||
|
function nonce(bytes calldata _message) internal pure returns (uint256) { |
||||||
|
return uint256(bytes32(_message[NONCE_OFFSET:ORIGIN_OFFSET])); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message origin domain. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Origin domain of `_message` |
||||||
|
*/ |
||||||
|
function origin(bytes calldata _message) internal pure returns (uint32) { |
||||||
|
return uint32(bytes4(_message[ORIGIN_OFFSET:SENDER_OFFSET])); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message sender as bytes32. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Sender of `_message` as bytes32 |
||||||
|
*/ |
||||||
|
function sender(bytes calldata _message) internal pure returns (bytes32) { |
||||||
|
return bytes32(_message[SENDER_OFFSET:DESTINATION_OFFSET]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message sender as address. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Sender of `_message` as address |
||||||
|
*/ |
||||||
|
function senderAddress(bytes calldata _message) |
||||||
|
internal |
||||||
|
pure |
||||||
|
returns (address) |
||||||
|
{ |
||||||
|
return sender(_message).bytes32ToAddress(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message destination domain. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Destination domain of `_message` |
||||||
|
*/ |
||||||
|
function destination(bytes calldata _message) |
||||||
|
internal |
||||||
|
pure |
||||||
|
returns (uint32) |
||||||
|
{ |
||||||
|
return uint32(bytes4(_message[DESTINATION_OFFSET:RECIPIENT_OFFSET])); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message recipient as bytes32. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Recipient of `_message` as bytes32 |
||||||
|
*/ |
||||||
|
function recipient(bytes calldata _message) |
||||||
|
internal |
||||||
|
pure |
||||||
|
returns (bytes32) |
||||||
|
{ |
||||||
|
return bytes32(_message[RECIPIENT_OFFSET:BODY_OFFSET]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message recipient as address. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Recipient of `_message` as address |
||||||
|
*/ |
||||||
|
function recipientAddress(bytes calldata _message) |
||||||
|
internal |
||||||
|
pure |
||||||
|
returns (address) |
||||||
|
{ |
||||||
|
return recipient(_message).bytes32ToAddress(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the message body. |
||||||
|
* @param _message ABI encoded Hyperlane message. |
||||||
|
* @return Body of `_message` |
||||||
|
*/ |
||||||
|
function body(bytes calldata _message) |
||||||
|
internal |
||||||
|
pure |
||||||
|
returns (bytes calldata) |
||||||
|
{ |
||||||
|
return bytes(_message[BODY_OFFSET:]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
|
||||||
|
import {MessageV2} from "../libs/MessageV2.sol"; |
||||||
|
|
||||||
|
contract TestMessageV2 { |
||||||
|
using MessageV2 for bytes; |
||||||
|
|
||||||
|
function version(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (uint32 _version) |
||||||
|
{ |
||||||
|
return _message.version(); |
||||||
|
} |
||||||
|
|
||||||
|
function nonce(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (uint256 _nonce) |
||||||
|
{ |
||||||
|
return _message.nonce(); |
||||||
|
} |
||||||
|
|
||||||
|
function body(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (bytes calldata _body) |
||||||
|
{ |
||||||
|
return _message.body(); |
||||||
|
} |
||||||
|
|
||||||
|
function origin(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (uint32 _origin) |
||||||
|
{ |
||||||
|
return _message.origin(); |
||||||
|
} |
||||||
|
|
||||||
|
function sender(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (bytes32 _sender) |
||||||
|
{ |
||||||
|
return _message.sender(); |
||||||
|
} |
||||||
|
|
||||||
|
function destination(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (uint32 _destination) |
||||||
|
{ |
||||||
|
return _message.destination(); |
||||||
|
} |
||||||
|
|
||||||
|
function recipient(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (bytes32 _recipient) |
||||||
|
{ |
||||||
|
return _message.recipient(); |
||||||
|
} |
||||||
|
|
||||||
|
function recipientAddress(bytes calldata _message) |
||||||
|
external |
||||||
|
pure |
||||||
|
returns (address _recipient) |
||||||
|
{ |
||||||
|
return _message.recipientAddress(); |
||||||
|
} |
||||||
|
|
||||||
|
function id(bytes calldata _message) external pure returns (bytes32) { |
||||||
|
return _message.id(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.8.0; |
||||||
|
|
||||||
|
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||||
|
|
||||||
|
contract TestModule is IInterchainSecurityModule { |
||||||
|
bool public accept; |
||||||
|
|
||||||
|
function setAccept(bool _val) external { |
||||||
|
accept = _val; |
||||||
|
} |
||||||
|
|
||||||
|
function verify(bytes calldata, bytes calldata) |
||||||
|
external |
||||||
|
view |
||||||
|
returns (bool) |
||||||
|
{ |
||||||
|
return accept; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
|
||||||
|
interface IInterchainSecurityModule { |
||||||
|
// Called by the Mailbox to determine whether or not the message should be accepted. |
||||||
|
function verify(bytes calldata _metadata, bytes calldata _message) |
||||||
|
external |
||||||
|
returns (bool); |
||||||
|
} |
||||||
|
|
||||||
|
interface ISpecifiesInterchainSecurityModule { |
||||||
|
function interchainSecurityModule() |
||||||
|
external |
||||||
|
view |
||||||
|
returns (IInterchainSecurityModule); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.8.0; |
||||||
|
|
||||||
|
interface IMailboxV2 { |
||||||
|
function localDomain() external view returns (uint32); |
||||||
|
|
||||||
|
function dispatch( |
||||||
|
uint32 _destinationDomain, |
||||||
|
bytes32 _recipientAddress, |
||||||
|
bytes calldata _messageBody |
||||||
|
) external returns (bytes32); |
||||||
|
|
||||||
|
function process(bytes calldata _metadata, bytes calldata _message) |
||||||
|
external; |
||||||
|
|
||||||
|
function count() external view returns (uint256); |
||||||
|
|
||||||
|
function root() external view returns (bytes32); |
||||||
|
|
||||||
|
function latestCheckpoint() external view returns (bytes32, uint256); |
||||||
|
} |
@ -0,0 +1,229 @@ |
|||||||
|
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||||
|
import { expect } from 'chai'; |
||||||
|
import { ethers } from 'hardhat'; |
||||||
|
|
||||||
|
import { utils } from '@hyperlane-xyz/utils'; |
||||||
|
|
||||||
|
import { |
||||||
|
BadRecipient1__factory, |
||||||
|
BadRecipient2__factory, |
||||||
|
BadRecipient3__factory, |
||||||
|
BadRecipient5__factory, |
||||||
|
BadRecipient6__factory, |
||||||
|
MailboxV2, |
||||||
|
MailboxV2__factory, |
||||||
|
TestModule, |
||||||
|
TestModule__factory, |
||||||
|
TestRecipient__factory, |
||||||
|
} from '../types'; |
||||||
|
|
||||||
|
import { inferMessageValues } from './lib/mailboxes'; |
||||||
|
|
||||||
|
const originDomain = 1000; |
||||||
|
const destDomain = 2000; |
||||||
|
const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; |
||||||
|
|
||||||
|
describe('Mailbox', async () => { |
||||||
|
let mailbox: MailboxV2, |
||||||
|
module: TestModule, |
||||||
|
signer: SignerWithAddress, |
||||||
|
nonOwner: SignerWithAddress; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
[signer, nonOwner] = await ethers.getSigners(); |
||||||
|
const moduleFactory = new TestModule__factory(signer); |
||||||
|
module = await moduleFactory.deploy(); |
||||||
|
const mailboxFactory = new MailboxV2__factory(signer); |
||||||
|
mailbox = await mailboxFactory.deploy(originDomain); |
||||||
|
await mailbox.initialize(module.address); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Cannot be initialized twice', async () => { |
||||||
|
await expect(mailbox.initialize(module.address)).to.be.revertedWith( |
||||||
|
'Initializable: contract is already initialized', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#dispatch', () => { |
||||||
|
let recipient: SignerWithAddress, message: string, id: string, body: string; |
||||||
|
before(async () => { |
||||||
|
[, recipient] = await ethers.getSigners(); |
||||||
|
({ message, id, body } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
destDomain, |
||||||
|
recipient.address, |
||||||
|
'message', |
||||||
|
)); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Does not dispatch too large messages', async () => { |
||||||
|
const longMessage = `0x${Buffer.alloc(3000).toString('hex')}`; |
||||||
|
await expect( |
||||||
|
mailbox.dispatch( |
||||||
|
destDomain, |
||||||
|
utils.addressToBytes32(recipient.address), |
||||||
|
longMessage, |
||||||
|
), |
||||||
|
).to.be.revertedWith('msg too long'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Dispatches a message', async () => { |
||||||
|
// Send message with signer address as msg.sender
|
||||||
|
await expect( |
||||||
|
mailbox |
||||||
|
.connect(signer) |
||||||
|
.dispatch( |
||||||
|
destDomain, |
||||||
|
utils.addressToBytes32(recipient.address), |
||||||
|
body, |
||||||
|
), |
||||||
|
) |
||||||
|
.to.emit(mailbox, 'Dispatch') |
||||||
|
.withArgs(id, message); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Returns the id of the dispatched message', async () => { |
||||||
|
const actualId = await mailbox |
||||||
|
.connect(signer) |
||||||
|
.callStatic.dispatch( |
||||||
|
destDomain, |
||||||
|
utils.addressToBytes32(recipient.address), |
||||||
|
body, |
||||||
|
); |
||||||
|
|
||||||
|
expect(actualId).equals(id); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#process', () => { |
||||||
|
const badRecipientFactories = [ |
||||||
|
BadRecipient1__factory, |
||||||
|
BadRecipient2__factory, |
||||||
|
BadRecipient3__factory, |
||||||
|
BadRecipient5__factory, |
||||||
|
BadRecipient6__factory, |
||||||
|
]; |
||||||
|
let message: string, id: string, recipient: string; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
await module.setAccept(true); |
||||||
|
const recipientF = new TestRecipient__factory(signer); |
||||||
|
recipient = utils.addressToBytes32((await recipientF.deploy()).address); |
||||||
|
({ message, id } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
originDomain, |
||||||
|
recipient, |
||||||
|
'message', |
||||||
|
)); |
||||||
|
}); |
||||||
|
|
||||||
|
it('processes a message', async () => { |
||||||
|
await expect(mailbox.process('0x', message)).to.emit(mailbox, 'Process'); |
||||||
|
expect(await mailbox.delivered(id)).to.be.true; |
||||||
|
}); |
||||||
|
|
||||||
|
it('Rejects an already-processed message', async () => { |
||||||
|
await expect(mailbox.process('0x', message)).to.emit(mailbox, 'Process'); |
||||||
|
|
||||||
|
// Try to process message again
|
||||||
|
await expect(mailbox.process('0x', message)).to.be.revertedWith( |
||||||
|
'delivered', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Fails to process message when rejected by module', async () => { |
||||||
|
await module.setAccept(false); |
||||||
|
await expect(mailbox.process('0x', message)).to.be.revertedWith( |
||||||
|
'!module', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
for (let i = 0; i < badRecipientFactories.length; i++) { |
||||||
|
it(`Fails to process a message for a badly implemented recipient (${ |
||||||
|
i + 1 |
||||||
|
})`, async () => {
|
||||||
|
const factory = new badRecipientFactories[i](signer); |
||||||
|
const badRecipient = await factory.deploy(); |
||||||
|
|
||||||
|
({ message } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
originDomain, |
||||||
|
badRecipient.address, |
||||||
|
'message', |
||||||
|
)); |
||||||
|
await expect(mailbox.process('0x', message)).to.be.reverted; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Fails to process with wrong version..
|
||||||
|
it('Fails to process message with wrong destination Domain', async () => { |
||||||
|
({ message } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
originDomain + 1, |
||||||
|
recipient, |
||||||
|
'message', |
||||||
|
)); |
||||||
|
|
||||||
|
await expect(mailbox.process('0x', message)).to.be.revertedWith( |
||||||
|
'!destination', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Fails to process message with wrong version', async () => { |
||||||
|
const version = await mailbox.VERSION(); |
||||||
|
({ message } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
originDomain, |
||||||
|
recipient, |
||||||
|
'message', |
||||||
|
version + 1, |
||||||
|
)); |
||||||
|
await expect(mailbox.process('0x', message)).to.be.revertedWith( |
||||||
|
'!version', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Fails to process message sent to a non-existent contract address', async () => { |
||||||
|
({ message } = await inferMessageValues( |
||||||
|
mailbox, |
||||||
|
signer.address, |
||||||
|
originDomain, |
||||||
|
'0x1234567890123456789012345678901234567890', // non-existent contract address
|
||||||
|
'message', |
||||||
|
)); |
||||||
|
await expect(mailbox.process('0x', message)).to.be.reverted; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#setDefaultModule', async () => { |
||||||
|
let newModule: TestModule; |
||||||
|
before(async () => { |
||||||
|
const moduleFactory = new TestModule__factory(signer); |
||||||
|
newModule = await moduleFactory.deploy(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Allows owner to update the default ISM', async () => { |
||||||
|
await expect(mailbox.setDefaultModule(newModule.address)) |
||||||
|
.to.emit(mailbox, 'DefaultModuleSet') |
||||||
|
.withArgs(newModule.address); |
||||||
|
expect(await mailbox.defaultModule()).to.equal(newModule.address); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Does not allow non-owner to update the default ISM', async () => { |
||||||
|
await expect( |
||||||
|
mailbox.connect(nonOwner).setDefaultModule(newModule.address), |
||||||
|
).to.be.revertedWith(ONLY_OWNER_REVERT_MSG); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Reverts if the provided ISM is not a contract', async () => { |
||||||
|
await expect(mailbox.setDefaultModule(signer.address)).to.be.revertedWith( |
||||||
|
'!contract', |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,95 @@ |
|||||||
|
import { expect } from 'chai'; |
||||||
|
import { ethers } from 'hardhat'; |
||||||
|
|
||||||
|
import { utils } from '@hyperlane-xyz/utils'; |
||||||
|
|
||||||
|
import { TestMessageV2, TestMessageV2__factory } from '../types'; |
||||||
|
|
||||||
|
const testCases = require('../../vectors/message.json'); |
||||||
|
|
||||||
|
const remoteDomain = 1000; |
||||||
|
const localDomain = 2000; |
||||||
|
const version = 0; |
||||||
|
const nonce = 11; |
||||||
|
|
||||||
|
describe('MessageV2', async () => { |
||||||
|
let messageLib: TestMessageV2; |
||||||
|
|
||||||
|
before(async () => { |
||||||
|
const [signer] = await ethers.getSigners(); |
||||||
|
|
||||||
|
const Message = new TestMessageV2__factory(signer); |
||||||
|
messageLib = await Message.deploy(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Returns fields from a message', async () => { |
||||||
|
const [sender, recipient] = await ethers.getSigners(); |
||||||
|
const body = ethers.utils.formatBytes32String('message'); |
||||||
|
|
||||||
|
const message = utils.formatMessageV2( |
||||||
|
version, |
||||||
|
nonce, |
||||||
|
remoteDomain, |
||||||
|
sender.address, |
||||||
|
localDomain, |
||||||
|
recipient.address, |
||||||
|
body, |
||||||
|
); |
||||||
|
|
||||||
|
expect(await messageLib.version(message)).to.equal(version); |
||||||
|
expect(await messageLib.nonce(message)).to.equal(nonce); |
||||||
|
expect(await messageLib.origin(message)).to.equal(remoteDomain); |
||||||
|
expect(await messageLib.sender(message)).to.equal( |
||||||
|
utils.addressToBytes32(sender.address), |
||||||
|
); |
||||||
|
expect(await messageLib.destination(message)).to.equal(localDomain); |
||||||
|
expect(await messageLib.recipient(message)).to.equal( |
||||||
|
utils.addressToBytes32(recipient.address), |
||||||
|
); |
||||||
|
expect(await messageLib.recipientAddress(message)).to.equal( |
||||||
|
recipient.address, |
||||||
|
); |
||||||
|
expect(await messageLib.body(message)).to.equal(body); |
||||||
|
}); |
||||||
|
|
||||||
|
// TODO: Update rust output to new message format
|
||||||
|
it.skip('Matches Rust-output HyperlaneMessage and leaf', async () => { |
||||||
|
const origin = 1000; |
||||||
|
const sender = '0x1111111111111111111111111111111111111111'; |
||||||
|
const destination = 2000; |
||||||
|
const recipient = '0x2222222222222222222222222222222222222222'; |
||||||
|
const body = '0x1234'; |
||||||
|
|
||||||
|
const hyperlaneMessage = utils.formatMessageV2( |
||||||
|
version, |
||||||
|
nonce, |
||||||
|
origin, |
||||||
|
sender, |
||||||
|
destination, |
||||||
|
recipient, |
||||||
|
body, |
||||||
|
); |
||||||
|
|
||||||
|
const { |
||||||
|
origin: testOrigin, |
||||||
|
sender: testSender, |
||||||
|
destination: testDestination, |
||||||
|
recipient: testRecipient, |
||||||
|
body: testBody, |
||||||
|
messageHash, |
||||||
|
} = testCases[0]; |
||||||
|
|
||||||
|
expect(await messageLib.origin(hyperlaneMessage)).to.equal(testOrigin); |
||||||
|
expect(await messageLib.sender(hyperlaneMessage)).to.equal(testSender); |
||||||
|
expect(await messageLib.destination(hyperlaneMessage)).to.equal( |
||||||
|
testDestination, |
||||||
|
); |
||||||
|
expect(await messageLib.recipient(hyperlaneMessage)).to.equal( |
||||||
|
testRecipient, |
||||||
|
); |
||||||
|
expect(await messageLib.body(hyperlaneMessage)).to.equal( |
||||||
|
ethers.utils.hexlify(testBody), |
||||||
|
); |
||||||
|
expect(utils.messageIdV2(hyperlaneMessage)).to.equal(messageHash); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue