The home for Hyperlane core contracts, sdk packages, and other infrastructure
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.
 
 
 
 
 
 
hyperlane-monorepo/solidity/core/test/inbox.test.ts

353 lines
11 KiB

import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { types, utils } from '@abacus-network/utils';
import {
BadRecipient1__factory,
BadRecipient3__factory,
BadRecipient5__factory,
BadRecipient6__factory,
BadRecipientHandle__factory,
TestInbox,
TestInbox__factory,
TestRecipient__factory,
TestValidatorManager,
TestValidatorManager__factory,
} from '../types';
const merkleTestCases = require('../../../vectors/merkle.json');
const proveAndProcessTestCases = require('../../../vectors/proveAndProcess.json');
const localDomain = 2000;
const remoteDomain = 1000;
describe('Inbox', async () => {
const badRecipientFactories = [
BadRecipient1__factory,
BadRecipient3__factory,
BadRecipient5__factory,
BadRecipient6__factory,
];
let inbox: TestInbox,
signer: SignerWithAddress,
abacusMessageSender: SignerWithAddress,
validatorManager: TestValidatorManager;
before(async () => {
[signer, abacusMessageSender] = await ethers.getSigners();
// Inbox.initialize will ensure the validator manager is a contract.
// TestValidatorManager doesn't have any special logic, it just submits
// checkpoints without any signature verification.
const testValidatorManagerFactory = new TestValidatorManager__factory(
signer,
);
validatorManager = await testValidatorManagerFactory.deploy();
});
beforeEach(async () => {
const inboxFactory = new TestInbox__factory(signer);
inbox = await inboxFactory.deploy(localDomain);
await inbox.initialize(
remoteDomain,
validatorManager.address,
ethers.constants.HashZero,
0,
);
});
it('Cannot be initialized twice', async () => {
await expect(
inbox.initialize(
remoteDomain,
validatorManager.address,
ethers.constants.HashZero,
0,
),
).to.be.revertedWith('Initializable: contract is already initialized');
});
it('Accepts checkpoint from validator manager', async () => {
const root = ethers.utils.formatBytes32String('first new root');
const index = 1;
await validatorManager.checkpoint(inbox.address, root, index);
const [croot, cindex] = await inbox.latestCheckpoint();
expect(croot).to.equal(root);
expect(cindex).to.equal(index);
});
it('Rejects checkpoint from non-validator manager', async () => {
const root = ethers.utils.formatBytes32String('first new root');
const index = 1;
await expect(inbox.checkpoint(root, index)).to.be.revertedWith(
'!validatorManager',
);
});
it('Rejects old checkpoint from validator manager', async () => {
let root = ethers.utils.formatBytes32String('first new root');
let index = 10;
await validatorManager.checkpoint(inbox.address, root, index);
const [croot, cindex] = await inbox.latestCheckpoint();
expect(croot).to.equal(root);
expect(cindex).to.equal(index);
root = ethers.utils.formatBytes32String('second new root');
index = 9;
await expect(
validatorManager.checkpoint(inbox.address, root, index),
).to.be.revertedWith('old checkpoint');
});
it('Proves a valid message', async () => {
// Use 1st proof of 1st merkle vector test case
const testCase = merkleTestCases[0];
let { leaf, index, path } = testCase.proofs[0];
await inbox.setCheckpoint(testCase.expectedRoot, 1);
// Ensure proper static call return value
expect(await inbox.callStatic.prove(leaf, path as types.BytesArray, index))
.to.be.true;
await inbox.prove(leaf, path as types.BytesArray, index);
expect(await inbox.messages(leaf)).to.equal(types.MessageStatus.PENDING);
});
it('Rejects an already-proven message', async () => {
const testCase = merkleTestCases[0];
let { leaf, index, path } = testCase.proofs[0];
await inbox.setCheckpoint(testCase.expectedRoot, 1);
// Prove message, which changes status to MessageStatus.Pending
await inbox.prove(leaf, path as types.BytesArray, index);
expect(await inbox.messages(leaf)).to.equal(types.MessageStatus.PENDING);
// Try to prove message again
await expect(
inbox.prove(leaf, path as types.BytesArray, index),
).to.be.revertedWith('!MessageStatus.None');
});
it('Rejects invalid message proof', async () => {
// Use 1st proof of 1st merkle vector test case
const testCase = merkleTestCases[0];
let { leaf, index, path } = testCase.proofs[0];
// Switch ordering of proof hashes
// NB: We copy 'path' here to avoid mutating the test cases for
// other tests.
const newPath = [...path];
newPath[0] = path[1];
newPath[1] = path[0];
await inbox.setCheckpoint(testCase.expectedRoot, 1);
expect(
await inbox.callStatic.prove(leaf, newPath as types.BytesArray, index),
).to.be.false;
await inbox.prove(leaf, newPath as types.BytesArray, index);
expect(await inbox.messages(leaf)).to.equal(types.MessageStatus.NONE);
});
it('Processes a proved message', async () => {
const sender = abacusMessageSender;
const testRecipientFactory = new TestRecipient__factory(signer);
const testRecipient = await testRecipientFactory.deploy();
const nonce = 0;
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
testRecipient.address,
'0x',
);
// Set message status to types.MessageStatus.Pending
await inbox.setMessageProven(abacusMessage);
const processTx = inbox.process(abacusMessage);
await expect(processTx)
.to.emit(inbox, 'Process')
.withArgs(utils.messageHash(abacusMessage));
});
it('Fails to process an unproved message', async () => {
const [sender, recipient] = await ethers.getSigners();
const nonce = 0;
const body = ethers.utils.formatBytes32String('message');
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
recipient.address,
body,
);
await expect(inbox.process(abacusMessage)).to.be.revertedWith('!proven');
});
for (let i = 0; i < badRecipientFactories.length; i++) {
it(`Fails to process a message for a badly implemented recipient (${
i + 1
})`, async () => {
const sender = abacusMessageSender;
const factory = new badRecipientFactories[i](signer);
const badRecipient = await factory.deploy();
const nonce = 0;
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
badRecipient.address,
'0x',
);
// Set message status to MessageStatus.Pending
await inbox.setMessageProven(abacusMessage);
await expect(inbox.process(abacusMessage)).to.be.reverted;
});
}
it('Fails to process message with wrong destination Domain', async () => {
const [sender, recipient] = await ethers.getSigners();
const nonce = 0;
const body = ethers.utils.formatBytes32String('message');
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
// Wrong destination Domain
localDomain + 5,
recipient.address,
body,
);
await expect(inbox.process(abacusMessage)).to.be.revertedWith(
'!destination',
);
});
it('Fails to process message sent to a non-existent contract address', async () => {
const nonce = 0;
const body = ethers.utils.formatBytes32String('message');
const abacusMessage = utils.formatMessage(
remoteDomain,
abacusMessageSender.address,
nonce,
localDomain,
'0x1234567890123456789012345678901234567890', // non-existent contract address
body,
);
// Set message status to types.MessageStatus.Pending
await inbox.setMessageProven(abacusMessage);
await expect(inbox.process(abacusMessage)).to.be.reverted;
});
it('Fails to process a message for bad handler function', async () => {
const sender = abacusMessageSender;
const [recipient] = await ethers.getSigners();
const factory = new BadRecipientHandle__factory(recipient);
const testRecipient = await factory.deploy();
const nonce = 0;
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
testRecipient.address,
'0x',
);
// Set message status to MessageStatus.Pending
await inbox.setMessageProven(abacusMessage);
// Ensure bad handler function causes process to fail
await expect(inbox.process(abacusMessage)).to.be.reverted;
});
it('Proves and processes a message', async () => {
const sender = abacusMessageSender;
const testRecipientFactory = new TestRecipient__factory(signer);
const testRecipient = await testRecipientFactory.deploy();
const nonce = 0;
// Note that hash of this message specifically matches leaf of 1st
// proveAndProcess test case
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
testRecipient.address,
'0x',
);
// Assert above message and test case have matching leaves
const { path, index } = proveAndProcessTestCases[0];
const hash = utils.messageHash(abacusMessage);
// Set inbox's current root to match newly computed root that includes
// the new leaf (normally root will have already been computed and path
// simply verifies leaf is in tree but because it is cryptographically
// impossible to find the inputs that create a pre-determined root, we
// simply recalculate root with the leaf using branchRoot)
const proofRoot = await inbox.testBranchRoot(
hash,
path as types.BytesArray,
index,
);
await inbox.setCheckpoint(proofRoot, 1);
await inbox.proveAndProcess(abacusMessage, path as types.BytesArray, index);
expect(await inbox.messages(hash)).to.equal(types.MessageStatus.PROCESSED);
});
it('Has proveAndProcess fail if prove fails', async () => {
const [sender, recipient] = await ethers.getSigners();
const nonce = 0;
// Use 1st proof of 1st merkle vector test case
const testCase = merkleTestCases[0];
let { leaf, index, path } = testCase.proofs[0];
// Create arbitrary message (contents not important)
const abacusMessage = utils.formatMessage(
remoteDomain,
sender.address,
nonce,
localDomain,
recipient.address,
'0x',
);
// Ensure root given in proof and actual root don't match so that
// inbox.prove(...) will fail
const proofRoot = await inbox.testBranchRoot(
leaf,
path as types.BytesArray,
index,
);
const rootIndex = await inbox.checkpoints(proofRoot);
expect(rootIndex).to.equal(0);
await expect(
inbox.proveAndProcess(abacusMessage, path as types.BytesArray, index),
).to.be.revertedWith('!prove');
});
});