aere-contracts/test/hyperlane-mailbox-v3.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

271 lines
12 KiB
JavaScript

// AereMailboxV3 + AereIGPV3 — fix for the quoteDispatch gap in AERE's
// Hyperlane core (chain 2800).
//
// Diagnosed root cause (on-chain, 2026-07-10):
// - Live AereMessenger 0xe54c...7325 has NO quoteDispatch selector at all
// (0x9c42bd18 absent from bytecode) -> empty revert on every call.
// - Live AereIGP 0x61B4...6837 has zero destination gas configs -> quote 0.
// - Both are Foundation-owned, so no deployer-side config fix is possible.
//
// This suite proves the V3 replacement:
// quoteDispatch returns a sane nonzero quote, dispatch enforces + forwards
// the fee, Dispatch/DispatchId/GasPayment events fire, inbound process
// verifies validator signatures over the Hyperlane v3 packed message.
//
// Run: npx hardhat test test/hyperlane-mailbox-v3.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const AERE_DOMAIN = 2800;
const ETH_DOMAIN = 1;
const BASE_DOMAIN = 8453;
const DEFAULT_GAS_USAGE = 50_000n;
// Documented sane placeholder pricing (owner-tunable):
// fee = (gasOverhead + gasLimit) * gasPriceAereWei
const ETH_OVERHEAD = 150_000n;
const ETH_PRICE = 30n * 10n ** 9n; // 30 gwei-AERE per unit of Ethereum gas
const BASE_OVERHEAD = 100_000n;
const BASE_PRICE = 2n * 10n ** 9n;
function b32(addr) {
return ethers.zeroPadValue(addr, 32);
}
// Hyperlane v3 packed message.
function formatMessage(nonce, origin, sender, destination, recipient, body) {
return ethers.solidityPacked(
["uint8", "uint32", "uint32", "bytes32", "uint32", "bytes32", "bytes"],
[3, nonce, origin, sender, destination, recipient, body]
);
}
// StandardHookMetadata: variant(2) || msgValue(32) || gasLimit(32) || refundAddress(20)
function hookMetadata(gasLimit, refundAddress) {
return ethers.solidityPacked(
["uint16", "uint256", "uint256", "address"],
[1, 0, gasLimit, refundAddress]
);
}
describe("AereMailboxV3 + AereIGPV3 — quoteDispatch fix", function () {
let deployer, validator, alice, foundation;
let mailbox, igp, recipient;
beforeEach(async function () {
[deployer, validator, alice, foundation] = await ethers.getSigners();
igp = await (await ethers.getContractFactory("AereIGPV3")).deploy(foundation.address);
await igp.waitForDeployment();
mailbox = await (await ethers.getContractFactory("AereMailboxV3")).deploy(AERE_DOMAIN);
await mailbox.waitForDeployment();
await (await mailbox.setDefaultHook(await igp.getAddress())).wait();
await (await mailbox.addValidator(validator.address)).wait();
await (
await igp.setDestinationGasConfigs([
{ remoteDomain: ETH_DOMAIN, config: { gasOverhead: ETH_OVERHEAD, gasPriceAereWei: ETH_PRICE } },
{ remoteDomain: BASE_DOMAIN, config: { gasOverhead: BASE_OVERHEAD, gasPriceAereWei: BASE_PRICE } },
])
).wait();
recipient = await (await ethers.getContractFactory("TestRecipientV3")).deploy();
await recipient.waitForDeployment();
});
describe("quoteDispatch (the missing function)", function () {
it("returns a sane nonzero quote for a configured domain", async function () {
const body = ethers.toUtf8Bytes("hello ethereum");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body);
// (150k overhead + 50k default gas) * 30 gwei = 0.006 AERE
expect(quote).to.equal((ETH_OVERHEAD + DEFAULT_GAS_USAGE) * ETH_PRICE);
expect(quote).to.equal(ethers.parseEther("0.006"));
});
it("prices different domains differently", async function () {
const body = ethers.toUtf8Bytes("hello base");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](BASE_DOMAIN, b32(alice.address), body);
expect(quote).to.equal((BASE_OVERHEAD + DEFAULT_GAS_USAGE) * BASE_PRICE);
});
it("honours a custom gasLimit via StandardHookMetadata", async function () {
const body = ethers.toUtf8Bytes("big message");
const md = hookMetadata(300_000n, ethers.ZeroAddress);
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes,bytes)"](
ETH_DOMAIN, b32(alice.address), body, md
);
expect(quote).to.equal((ETH_OVERHEAD + 300_000n) * ETH_PRICE);
});
it("reverts with a CLEAR error (not empty data) for an unconfigured domain", async function () {
const body = ethers.toUtf8Bytes("hello nowhere");
await expect(
mailbox["quoteDispatch(uint32,bytes32,bytes)"](424242, b32(alice.address), body)
).to.be.revertedWith("AereIGPV3: domain not configured");
});
it("quotes zero when no hooks are set (fee-free mode)", async function () {
const bare = await (await ethers.getContractFactory("AereMailboxV3")).deploy(AERE_DOMAIN);
await bare.waitForDeployment();
const q = await bare["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), "0x");
expect(q).to.equal(0n);
});
});
describe("dispatch pays the quote", function () {
it("reverts if msg.value < quote", async function () {
const body = ethers.toUtf8Bytes("underpaid");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body);
await expect(
mailbox.connect(alice)["dispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body, {
value: quote - 1n,
})
).to.be.revertedWith("AereMailboxV3: insufficient hook payment");
});
it("dispatch at exactly the quote emits Dispatch + DispatchId + GasPayment and funds the IGP", async function () {
const body = ethers.toUtf8Bytes("paid in full");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body);
const expectedMessage = formatMessage(
0, AERE_DOMAIN, b32(alice.address), ETH_DOMAIN, b32(alice.address), body
);
const expectedId = ethers.keccak256(expectedMessage);
const tx = mailbox.connect(alice)["dispatch(uint32,bytes32,bytes)"](
ETH_DOMAIN, b32(alice.address), body, { value: quote }
);
await expect(tx)
.to.emit(mailbox, "Dispatch").withArgs(alice.address, ETH_DOMAIN, b32(alice.address), expectedMessage)
.and.to.emit(mailbox, "DispatchId").withArgs(expectedId)
.and.to.emit(igp, "GasPayment").withArgs(expectedId, ETH_DOMAIN, DEFAULT_GAS_USAGE, quote);
expect(await ethers.provider.getBalance(await igp.getAddress())).to.equal(quote);
expect(await mailbox.nonce()).to.equal(1);
expect(await mailbox.latestDispatchedId()).to.equal(expectedId);
});
it("refunds overpayment to the sender", async function () {
const body = ethers.toUtf8Bytes("overpaid");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body);
const balBefore = await ethers.provider.getBalance(alice.address);
const tx = await mailbox.connect(alice)["dispatch(uint32,bytes32,bytes)"](
ETH_DOMAIN, b32(alice.address), body, { value: quote + ethers.parseEther("1") }
);
const rc = await tx.wait();
const gasCost = rc.gasUsed * rc.gasPrice;
// Alice net spend = quote + tx gas (the extra 1 AERE came back).
const balAfter = await ethers.provider.getBalance(alice.address);
expect(balBefore - balAfter).to.equal(quote + gasCost);
// IGP kept only the quote.
expect(await ethers.provider.getBalance(await igp.getAddress())).to.equal(quote);
});
it("dispatch to an unconfigured domain reverts clearly", async function () {
await expect(
mailbox.connect(alice)["dispatch(uint32,bytes32,bytes)"](424242, b32(alice.address), "0x", {
value: ethers.parseEther("1"),
})
).to.be.revertedWith("AereIGPV3: domain not configured");
});
});
describe("inbound process (Hyperlane v3 packed message + validator multisig)", function () {
async function signedInbound(body) {
const message = formatMessage(
7, ETH_DOMAIN, b32(alice.address), AERE_DOMAIN, b32(await recipient.getAddress()), body
);
const messageId = ethers.keccak256(message);
const sig = await validator.signMessage(ethers.getBytes(messageId));
return { message, messageId, sig };
}
it("delivers a validator-signed message to the recipient", async function () {
const body = ethers.toUtf8Bytes("inbound payload");
const { message, messageId, sig } = await signedInbound(body);
await expect(mailbox.process(sig, message))
.to.emit(mailbox, "Process").withArgs(ETH_DOMAIN, b32(alice.address), await recipient.getAddress())
.and.to.emit(mailbox, "ProcessId").withArgs(messageId)
.and.to.emit(recipient, "Handled");
expect(await recipient.lastOrigin()).to.equal(ETH_DOMAIN);
expect(await recipient.lastSender()).to.equal(b32(alice.address));
expect(ethers.toUtf8String(await recipient.lastBody())).to.equal("inbound payload");
expect(await mailbox.delivered(messageId)).to.equal(true);
});
it("rejects replay", async function () {
const { message, sig } = await signedInbound(ethers.toUtf8Bytes("once only"));
await (await mailbox.process(sig, message)).wait();
await expect(mailbox.process(sig, message)).to.be.revertedWith("AereMailboxV3: already delivered");
});
it("rejects a non-validator signature", async function () {
const body = ethers.toUtf8Bytes("forged");
const message = formatMessage(
8, ETH_DOMAIN, b32(alice.address), AERE_DOMAIN, b32(await recipient.getAddress()), body
);
const badSig = await alice.signMessage(ethers.getBytes(ethers.keccak256(message)));
await expect(mailbox.process(badSig, message)).to.be.revertedWith("AereMailboxV3: not a validator");
});
it("rejects a message addressed to another domain", async function () {
const body = ethers.toUtf8Bytes("misrouted");
const message = formatMessage(9, ETH_DOMAIN, b32(alice.address), 9999, b32(await recipient.getAddress()), body);
const sig = await validator.signMessage(ethers.getBytes(ethers.keccak256(message)));
await expect(mailbox.process(sig, message)).to.be.revertedWith("AereMailboxV3: wrong destination");
});
});
describe("IGP hook surface + payout", function () {
it("reports Hyperlane hookType INTERCHAIN_GAS_PAYMASTER (4)", async function () {
expect(await igp.hookType()).to.equal(4);
expect(await igp.supportsMetadata("0x")).to.equal(true);
expect(await igp.supportsMetadata(hookMetadata(1n, ethers.ZeroAddress))).to.equal(true);
});
it("claim() pays the whole balance to the beneficiary", async function () {
const body = ethers.toUtf8Bytes("fund the igp");
const quote = await mailbox["quoteDispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body);
await (
await mailbox.connect(alice)["dispatch(uint32,bytes32,bytes)"](ETH_DOMAIN, b32(alice.address), body, {
value: quote,
})
).wait();
const before = await ethers.provider.getBalance(foundation.address);
await (await igp.connect(alice).claim()).wait(); // permissionless, pays beneficiary only
const after = await ethers.provider.getBalance(foundation.address);
expect(after - before).to.equal(quote);
expect(await ethers.provider.getBalance(await igp.getAddress())).to.equal(0n);
});
it("only the owner can set gas configs or beneficiary", async function () {
await expect(
igp.connect(alice).setDestinationGasConfigs([
{ remoteDomain: 1, config: { gasOverhead: 1n, gasPriceAereWei: 1n } },
])
).to.be.revertedWith("Ownable: caller is not the owner");
await expect(igp.connect(alice).setBeneficiary(alice.address)).to.be.revertedWith(
"Ownable: caller is not the owner"
);
await expect(mailbox.connect(alice).setDefaultHook(alice.address)).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
it("rejects zero gas price configs", async function () {
await expect(
igp.setDestinationGasConfigs([{ remoteDomain: 1, config: { gasOverhead: 1n, gasPriceAereWei: 0n } }])
).to.be.revertedWith("AereIGPV3: zero gas price");
});
});
});