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.
271 lines
12 KiB
JavaScript
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");
|
|
});
|
|
});
|
|
});
|