From 322c18e8d732f5517c65215f7d42f5ec9111086d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Nov 2022 10:40:06 -0400 Subject: [PATCH] Add V2 Mailbox and Message contracts (#1185) --- .github/workflows/node.yml | 23 +- .github/workflows/rust.yml | 2 +- solidity/contracts/MailboxV2.sol | 225 +++++++++++++++++ solidity/contracts/libs/MessageV2.sol | 163 +++++++++++++ solidity/contracts/test/TestMessageV2.sol | 76 ++++++ solidity/contracts/test/TestModule.sol | 20 ++ .../interfaces/IInterchainSecurityModule.sol | 16 ++ solidity/interfaces/IMailboxV2.sol | 21 ++ solidity/test/lib/mailboxes.ts | 32 ++- solidity/test/mailboxv2.test.ts | 229 ++++++++++++++++++ solidity/test/messagev2.test.ts | 95 ++++++++ typescript/utils/src/utils.ts | 32 ++- 12 files changed, 919 insertions(+), 15 deletions(-) create mode 100644 solidity/contracts/MailboxV2.sol create mode 100644 solidity/contracts/libs/MessageV2.sol create mode 100644 solidity/contracts/test/TestMessageV2.sol create mode 100644 solidity/contracts/test/TestModule.sol create mode 100644 solidity/interfaces/IInterchainSecurityModule.sol create mode 100644 solidity/interfaces/IMailboxV2.sol create mode 100644 solidity/test/mailboxv2.test.ts create mode 100644 solidity/test/messagev2.test.ts diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 419a0b003..448a975e0 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -1,11 +1,11 @@ name: node on: - # Triggers the workflow on push or pull request events but only for the main branch + # Triggers the workflow on push or pull request events but only for the main and v2 branches push: - branches: [main] + branches: [main, v2] pull_request: - branches: [main] + branches: [main, v2] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -24,14 +24,13 @@ jobs: # Check out the lockfile from main, reinstall, and then # verify the lockfile matches what was committed. run: | - yarn install - CHANGES=$(git status -s) - if [[ ! -z $CHANGES ]]; then - echo "Changes found: $CHANGES" - git diff - exit 1 - fi - + yarn install + CHANGES=$(git status -s) + if [[ ! -z $CHANGES ]]; then + echo "Changes found: $CHANGES" + git diff + exit 1 + fi yarn-build: runs-on: ubuntu-latest @@ -95,7 +94,7 @@ jobs: test-sol: env: - ETHERSCAN_API_KEY: "" + ETHERSCAN_API_KEY: '' runs-on: ubuntu-latest needs: [yarn-build] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 73774ecfa..bc76ed601 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: rust on: push: - branches: [main] + branches: [main, v2] pull_request: paths: - 'rust/**' diff --git a/solidity/contracts/MailboxV2.sol b/solidity/contracts/MailboxV2.sol new file mode 100644 index 000000000..55575270f --- /dev/null +++ b/solidity/contracts/MailboxV2.sol @@ -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; + } + } +} diff --git a/solidity/contracts/libs/MessageV2.sol b/solidity/contracts/libs/MessageV2.sol new file mode 100644 index 000000000..bab560f74 --- /dev/null +++ b/solidity/contracts/libs/MessageV2.sol @@ -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:]); + } +} diff --git a/solidity/contracts/test/TestMessageV2.sol b/solidity/contracts/test/TestMessageV2.sol new file mode 100644 index 000000000..3ab80edef --- /dev/null +++ b/solidity/contracts/test/TestMessageV2.sol @@ -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(); + } +} diff --git a/solidity/contracts/test/TestModule.sol b/solidity/contracts/test/TestModule.sol new file mode 100644 index 000000000..caeb00a50 --- /dev/null +++ b/solidity/contracts/test/TestModule.sol @@ -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; + } +} diff --git a/solidity/interfaces/IInterchainSecurityModule.sol b/solidity/interfaces/IInterchainSecurityModule.sol new file mode 100644 index 000000000..3a568ebf8 --- /dev/null +++ b/solidity/interfaces/IInterchainSecurityModule.sol @@ -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); +} diff --git a/solidity/interfaces/IMailboxV2.sol b/solidity/interfaces/IMailboxV2.sol new file mode 100644 index 000000000..e8487e591 --- /dev/null +++ b/solidity/interfaces/IMailboxV2.sol @@ -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); +} diff --git a/solidity/test/lib/mailboxes.ts b/solidity/test/lib/mailboxes.ts index 13873df96..415c2e2c2 100644 --- a/solidity/test/lib/mailboxes.ts +++ b/solidity/test/lib/mailboxes.ts @@ -3,7 +3,7 @@ import { ethers } from 'ethers'; import { utils } from '@hyperlane-xyz/utils'; -import { TestOutbox } from '../../types'; +import { MailboxV2, TestOutbox } from '../../types'; import { DispatchEvent } from '../../types/contracts/Outbox'; export const dispatchMessage = async ( @@ -55,3 +55,33 @@ export interface MerkleProof { index: number; message: string; } + +export const inferMessageValues = async ( + mailbox: MailboxV2, + sender: string, + destination: number, + recipient: string, + messageStr: string, + version?: number, +) => { + const body = utils.ensure0x( + Buffer.from(ethers.utils.toUtf8Bytes(messageStr)).toString('hex'), + ); + const nonce = await mailbox.count(); + const localDomain = await mailbox.localDomain(); + const message = utils.formatMessageV2( + version ?? (await mailbox.VERSION()), + nonce, + localDomain, + sender, + destination, + recipient, + body, + ); + const id = utils.messageIdV2(message); + return { + message, + id, + body, + }; +}; diff --git a/solidity/test/mailboxv2.test.ts b/solidity/test/mailboxv2.test.ts new file mode 100644 index 000000000..e363f21e1 --- /dev/null +++ b/solidity/test/mailboxv2.test.ts @@ -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', + ); + }); + }); +}); diff --git a/solidity/test/messagev2.test.ts b/solidity/test/messagev2.test.ts new file mode 100644 index 000000000..5a06bfd6b --- /dev/null +++ b/solidity/test/messagev2.test.ts @@ -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); + }); +}); diff --git a/typescript/utils/src/utils.ts b/typescript/utils/src/utils.ts index c0b1c448e..4e3db82c2 100644 --- a/typescript/utils/src/utils.ts +++ b/typescript/utils/src/utils.ts @@ -1,4 +1,4 @@ -import { ethers, utils } from 'ethers'; +import { BigNumber, ethers, utils } from 'ethers'; import { Checkpoint } from './types'; import { Address, Domain, HexString, ParsedMessage } from './types'; @@ -55,6 +55,36 @@ export const formatMessage = ( ); }; +export const formatMessageV2 = ( + version: number | BigNumber, + nonce: number | BigNumber, + originDomain: Domain, + senderAddr: Address, + destinationDomain: Domain, + recipientAddr: Address, + body: HexString, +): string => { + senderAddr = addressToBytes32(senderAddr); + recipientAddr = addressToBytes32(recipientAddr); + + return ethers.utils.solidityPack( + ['uint8', 'uint256', 'uint32', 'bytes32', 'uint32', 'bytes32', 'bytes'], + [ + version, + nonce, + originDomain, + senderAddr, + destinationDomain, + recipientAddr, + body, + ], + ); +}; + +export function messageIdV2(message: HexString): string { + return ethers.utils.solidityKeccak256(['bytes'], [message]); +} + /** * Parse a serialized Abacus message from raw bytes. *