aere-contracts/test/warp-route-v3-bridge-conservation-invariant-property.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

547 lines
28 KiB
JavaScript

// =============================================================================
// SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for the LIVE
// USDC.e Hyperlane v3 Warp Route on the AERE side (chain 2800).
//
// Source under test (matched to the live addresses in sdk-js/src/addresses.ts):
// contracts/bridge/AereWarpRouteV3.sol -> AereWarpRouteV3 0x1f44573684aB6bC617e7200A19b940b05e4EE098
// contracts/stablecoins/USDCe.sol -> the synthetic USDC.e bridged token
//
// MESSAGING LAYER: the REAL contracts/bridge/AereMailboxV3.sol (+ AereIGPV3),
// driven end-to-end with a real ECDSA validator signature through process().
// We deliberately DO NOT use the toy MockWarpMailboxV3 (which has no replay
// guard) for the inbound path, because the "cannot be processed twice" and
// "only the configured mailbox can deliver" invariants live in the mailbox and
// must be exercised against the real code. Any finding therefore cannot be
// blamed on a toy relayer. The ONLY mock is a second AereMailboxV3 instance
// used to prove a non-configured mailbox cannot drive the router, plus the
// plain MockERC20Lending token used for the Lock mode collateral. These are
// disclosed honestly here.
//
// TOOLCHAIN: hardhat-based seeded randomized invariant fuzzing (NOT Foundry:
// `forge` is not installed; the repo builds through hardhat with OZ 4.9.6 +
// viaIR). Every random choice comes from mulberry32(seed); the base seed is
// printed at suite start and pinnable via FUZZ_SEED. On any break the failing
// (mode, campaign, step, actor, action, values) is embedded in the assertion
// message for a minimal repro.
//
// SCOPE: run this file in ISOLATION (the full suite OOM/segfaults on the
// unrelated ML-DSA PQC KAT tests):
// npx hardhat test test/warp-route-v3-bridge-conservation-invariant-property.test.js
// No deploy to any live node, no contract-logic change, one new test file only.
//
// INVARIANTS FUZZED (exactly the brief):
// * BRIDGE CONSERVATION: a synthetic mint happens ONLY via handle() driven by
// a mailbox/validator-verified inbound message; locked collateral is
// released ONLY via such a message. Modelled: totalReceivedIn == messagedIn,
// totalSentOut == messagedOut, and net backing holds:
// MintBurn: USDC.e.totalSupply == messagedIn - messagedOut
// Lock: router token balance == messagedOut - messagedIn (>= 0)
// * NO UNAUTHORIZED MINT/RELEASE: a direct handle() from any non-mailbox
// caller reverts; a message delivered by a DIFFERENT (non-configured)
// mailbox reverts; supply/collateral unchanged.
// * NO DOUBLE-PROCESS: replaying an already-delivered message reverts
// ("already delivered") and mints/releases nothing a second time.
// * SENDER AUTH: an otherwise-valid, validator-signed message whose sender is
// not the enrolled remote router reverts (UnknownRouter); nothing minted.
// * CONFIG THEFT: a non-owner cannot enroll/unenroll a remote router; the
// mailbox / token / mode are immutable and cannot be repointed by anyone.
//
// A real invariant violation is a valuable result: it is reported as the exact
// minimal failing (mode, campaign, step, action, values), never papered over.
//
// No em-dashes anywhere in this file.
// =============================================================================
const { expect } = require("chai");
const { ethers } = require("hardhat");
/* ------------------------------- seeded PRNG ------------------------------ */
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const BASE_SEED = process.env.FUZZ_SEED ? Number(process.env.FUZZ_SEED) : 0x11AE7C;
function rngFor(tag, campaign) {
let h = BASE_SEED ^ (campaign * 0x9e3779b1);
for (let i = 0; i < tag.length; i++) h = (Math.imul(h, 31) + tag.charCodeAt(i)) | 0;
return mulberry32(h >>> 0);
}
const ri = (rng, min, max) => min + Math.floor(rng() * (max - min + 1)); // inclusive
const pick = (rng, arr) => arr[Math.floor(rng() * arr.length)];
const chance = (rng, p) => rng() < p;
/* ------------------------------- misc helpers ----------------------------- */
const SIX = 10n ** 6n;
const U = (n) => BigInt(Math.round(Number(n) * 1000)) * (SIX / 1000n); // 6dp USDC-ish
const b32 = (addr) => ethers.zeroPadValue(addr, 32);
const MAXU = ethers.MaxUint256;
const AERE_DOMAIN = 2800; // local (this side)
const ETH_DOMAIN = 1; // remote (the sister collateral chain)
// Deterministic validator key for the mailbox (this account signs inbound
// messages; it never sends a transaction).
const VALIDATOR_PK = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
// Mode enum on AereWarpRouteV3: 0 = Lock, 1 = MintBurn.
const MODE = { Lock: 0, MintBurn: 1 };
const N = {
CAMPAIGNS: Number(process.env.WARP_CAMPAIGNS || 5),
STEPS: Number(process.env.WARP_STEPS || 70),
};
function packBody(recipientB32, amount) {
// Hyperlane HypERC20 convention: body = bytes32 recipient || uint256 amount.
return ethers.solidityPacked(["bytes32", "uint256"], [recipientB32, amount]);
}
// Full Hyperlane v3 wire message the real AereMailboxV3 parses:
// version(1) || nonce(4) || origin(4) || sender(32) || destination(4) || recipient(32) || body
function packMessage(nonce, origin, senderB32, destination, recipientB32, body) {
return ethers.solidityPacked(
["uint8", "uint32", "uint32", "bytes32", "uint32", "bytes32", "bytes"],
[3, nonce, origin, senderB32, destination, recipientB32, body]
);
}
describe("INVARIANT: AereWarpRouteV3 (USDC.e Hyperlane v3 warp route) bridge conservation", function () {
this.timeout(0);
let signers, validator;
let mailboxF, igpF, routerF, usdceF, erc20F;
before(async function () {
signers = await ethers.getSigners();
validator = new ethers.Wallet(VALIDATOR_PK, ethers.provider);
mailboxF = await ethers.getContractFactory("AereMailboxV3");
igpF = await ethers.getContractFactory("AereIGPV3");
routerF = await ethers.getContractFactory("AereWarpRouteV3");
usdceF = await ethers.getContractFactory("USDCe");
erc20F = await ethers.getContractFactory("MockERC20Lending");
console.log(
` [warp] base seed 0x${BASE_SEED.toString(16)} | ${N.CAMPAIGNS}x${N.STEPS} randomized steps per mode`
);
});
// ----- deploy the real messaging layer + the router in a chosen mode -----
async function deployStack(mode) {
const deployer = signers[0];
// Real mailbox on the AERE side (localDomain 2800) + real IGP hook.
const mailbox = await mailboxF.connect(deployer).deploy(AERE_DOMAIN);
await mailbox.waitForDeployment();
const igp = await igpF.connect(deployer).deploy(deployer.address); // beneficiary
await igp.waitForDeployment();
await (await mailbox.setDefaultHook(await igp.getAddress())).wait();
// Price destination gas so quoteDispatch/dispatch work end to end.
await (
await igp.setDestinationGasConfigs([
{ remoteDomain: ETH_DOMAIN, config: { gasOverhead: 100000, gasPriceAereWei: 1_000_000_000n } },
])
).wait();
await (await mailbox.addValidator(validator.address)).wait(); // threshold already 1
// A SECOND, non-configured mailbox used only to prove a foreign mailbox
// cannot drive the router's handle().
const foreignMailbox = await mailboxF.connect(deployer).deploy(AERE_DOMAIN);
await foreignMailbox.waitForDeployment();
await (await foreignMailbox.setDefaultHook(await igp.getAddress())).wait();
await (await foreignMailbox.addValidator(validator.address)).wait();
let token, router;
if (mode === MODE.MintBurn) {
// USDC.e's BRIDGE is immutable, so predict the router address (deployer
// nonce + 1) and deploy USDC.e pointing at it, then the router.
const nonce = await ethers.provider.getTransactionCount(deployer.address);
const futureRouter = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });
token = await usdceF.connect(deployer).deploy(futureRouter);
await token.waitForDeployment();
router = await routerF
.connect(deployer)
.deploy(await mailbox.getAddress(), await igp.getAddress(), await token.getAddress(), MODE.MintBurn);
await router.waitForDeployment();
expect((await router.getAddress()).toLowerCase()).to.equal(futureRouter.toLowerCase());
expect(await token.BRIDGE()).to.equal(await router.getAddress());
} else {
// Lock mode: a pre-existing token the router does NOT mint.
token = await erc20F.connect(deployer).deploy("Bridged USD Coin", "USDC.e", 6);
await token.waitForDeployment();
router = await routerF
.connect(deployer)
.deploy(await mailbox.getAddress(), await igp.getAddress(), await token.getAddress(), MODE.Lock);
await router.waitForDeployment();
}
// The Ethereum-side sister router (arbitrary address; enrolled as the only
// trusted remote for ETH_DOMAIN).
const remoteRouter = ethers.Wallet.createRandom().address;
await (await router.enrollRemoteRouter(ETH_DOMAIN, b32(remoteRouter))).wait();
return {
mailbox,
foreignMailbox,
igp,
router,
token,
routerAddr: await router.getAddress(),
remoteRouter,
remoteRouterB32: b32(remoteRouter),
};
}
// Deliver a verified inbound message through the REAL mailbox.process().
// Returns { tx, message } where message is the exact bytes (for replay tests).
async function deliverInbound(stack, mailbox, senderB32, recipientRouterAddr, nonce, recipientB32, amount) {
const body = packBody(recipientB32, amount);
const message = packMessage(nonce, ETH_DOMAIN, senderB32, AERE_DOMAIN, b32(recipientRouterAddr), body);
const messageId = ethers.keccak256(message);
const sig = await validator.signMessage(ethers.getBytes(messageId));
return { tx: mailbox.connect(signers[0]).process(sig, message), message, messageId };
}
// Re-submit an already-built message (byte-identical) for the replay guard.
async function reprocess(mailbox, message) {
return mailbox.connect(signers[0]).process(await validatorSig(message), message);
}
async function validatorSig(message) {
return validator.signMessage(ethers.getBytes(ethers.keccak256(message)));
}
// -------------------------------------------------------------------------
// MAIN FUZZ, parametrized over both token modes.
// -------------------------------------------------------------------------
async function runMode(modeName, mode) {
let steps = 0, mints = 0, burns = 0, replays = 0, foreign = 0, badSender = 0, direct = 0;
for (let c = 0; c < N.CAMPAIGNS; c++) {
const rng = rngFor("main-" + modeName, c);
const stack = await deployStack(mode);
const { mailbox, foreignMailbox, router, token, routerAddr, remoteRouterB32 } = stack;
const users = [signers[1], signers[2], signers[3], signers[4]];
const recips = [signers[1], signers[2], signers[3], signers[4], signers[5]];
// In Lock mode, users hold the pre-existing token and approve the router.
if (mode === MODE.Lock) {
for (const u of users) {
await (await token.mint(u.address, U(100_000_000))).wait();
await (await token.connect(u).approve(routerAddr, MAXU)).wait();
}
}
// JS-side ledger of what the bridge messaging has authorized.
let messagedIn = 0n; // sum of verified inbound amounts (mints / releases)
let messagedOut = 0n; // sum of outbound amounts (burns / locks)
let inboundNonce = 0; // remote-side nonce, kept unique so messageIds differ
const deliveredMsgs = []; // prior valid message bytes, for replay tests
// net synthetic / locked bookkeeping model
const netBacked = () => messagedIn - messagedOut; // MintBurn: supply; Lock: locked
async function assertInvariants(ctx) {
const tOut = await router.totalSentOut();
const tIn = await router.totalReceivedIn();
expect(tOut, `[warp OUT-LEDGER] ${ctx} totalSentOut ${tOut} != messagedOut ${messagedOut}`).to.equal(messagedOut);
expect(tIn, `[warp IN-LEDGER] ${ctx} totalReceivedIn ${tIn} != messagedIn ${messagedIn}`).to.equal(messagedIn);
if (mode === MODE.MintBurn) {
const supply = await token.totalSupply();
// (1) net backing: every synthetic in circulation is backed by a net
// verified inbound message (minted-in minus burned-out).
expect(supply, `[warp BACKING] ${ctx} USDC.e supply ${supply} != messagedIn-messagedOut ${netBacked()}`)
.to.equal(netBacked());
// (2) no free synthetic: supply can never exceed cumulative verified mints.
expect(supply, `[warp NO-OVER-MINT] ${ctx} supply ${supply} > messagedIn ${messagedIn}`).to.be.lte(messagedIn);
expect(netBacked(), `[warp NONNEG-SUPPLY] ${ctx} net supply negative`).to.be.gte(0n);
} else {
const locked = await token.balanceOf(routerAddr);
const lockedView = await router.lockedSupply();
expect(lockedView, `[warp LOCKED-VIEW] ${ctx}`).to.equal(locked);
// (1) collateral conservation: router holds exactly locked-minus-released.
expect(locked, `[warp COLLATERAL] ${ctx} locked ${locked} != messagedOut-messagedIn ${messagedOut - messagedIn}`)
.to.equal(messagedOut - messagedIn);
// (2) never releases more than was locked.
expect(messagedOut - messagedIn, `[warp NO-OVER-RELEASE] ${ctx} released > locked`).to.be.gte(0n);
}
// (3) immutables never move (no config theft).
expect(await router.MAILBOX(), `[warp IMMUT-MAILBOX] ${ctx}`).to.equal(await mailbox.getAddress());
expect(await router.TOKEN(), `[warp IMMUT-TOKEN] ${ctx}`).to.equal(await token.getAddress());
expect(await router.MODE(), `[warp IMMUT-MODE] ${ctx}`).to.equal(BigInt(mode));
expect(await router.routers(ETH_DOMAIN), `[warp REMOTE-ROUTER] ${ctx}`).to.equal(remoteRouterB32);
}
await assertInvariants(`${modeName} c${c} init`);
for (let s = 0; s < N.STEPS; s++) {
// Weight bridge-in early so users have balance to bridge out.
const action = pick(rng, [
"bridgeIn", "bridgeIn", "bridgeIn", "bridgeIn",
"bridgeOut", "bridgeOut", "bridgeOut",
"replay", "foreignMailbox", "badSender", "directHandle", "unauthConfig",
]);
const ctx = `${modeName} c${c} s${s} action=${action}`;
try {
if (action === "bridgeIn") {
// A verified inbound message: mint (MintBurn) or release (Lock).
const recip = pick(rng, recips);
let amount = U(ri(rng, 1, 40_000)) + BigInt(ri(rng, 0, 999999));
if (mode === MODE.Lock) {
// cannot release more than currently locked.
const locked = messagedOut - messagedIn;
if (locked === 0n) { steps++; await assertInvariants(ctx); continue; }
if (amount > locked) amount = locked;
}
const nonce = inboundNonce++;
const supplyBefore = mode === MODE.MintBurn ? await token.totalSupply() : await token.balanceOf(routerAddr);
const recipBefore = await token.balanceOf(recip.address);
const { tx, message } = await deliverInbound(
stack, mailbox, remoteRouterB32, routerAddr, nonce, b32(recip.address), amount
);
await (await tx).wait();
deliveredMsgs.push(message);
messagedIn += amount;
mints++;
const recipAfter = await token.balanceOf(recip.address);
if (mode === MODE.MintBurn) {
expect(recipAfter - recipBefore, `[warp MINT-CREDIT] ${ctx}`).to.equal(amount);
expect((await token.totalSupply()) - supplyBefore, `[warp MINT-SUPPLY] ${ctx}`).to.equal(amount);
} else {
expect(recipAfter - recipBefore, `[warp RELEASE-CREDIT] ${ctx}`).to.equal(amount);
}
} else if (action === "bridgeOut") {
// transferRemote: burn (MintBurn) or lock (Lock) the caller's tokens.
const user = pick(rng, users);
const bal = await token.balanceOf(user.address);
if (bal === 0n) { steps++; await assertInvariants(ctx); continue; }
let amount = bal === 1n ? 1n : (U(ri(rng, 0, 20_000)) % bal) + 1n;
if (amount > bal) amount = bal;
const dest = pick(rng, recips).address;
const recipB32 = b32(dest);
const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipB32, amount);
const overpay = chance(rng, 0.4) ? fee / 5n + 1n : 0n;
const userBalBefore = await token.balanceOf(user.address);
await (
await router.connect(user).transferRemote(ETH_DOMAIN, recipB32, amount, { value: fee + overpay })
).wait();
const userBalAfter = await token.balanceOf(user.address);
expect(userBalBefore - userBalAfter, `[warp OUT-DEBIT] ${ctx} debit != amount`).to.equal(amount);
messagedOut += amount;
burns++;
} else if (action === "replay") {
// Replaying any prior byte-identical message must revert (mailbox
// replay guard) and change nothing.
if (!deliveredMsgs.length) { steps++; await assertInvariants(ctx); continue; }
const msg = pick(rng, deliveredMsgs);
await expect(reprocess(mailbox, msg), `[warp NO-DOUBLE-PROCESS] ${ctx}`).to.be.reverted;
replays++;
} else if (action === "foreignMailbox") {
// A message delivered by a DIFFERENT (non-configured) mailbox must
// not be able to drive the router: router.handle reverts NotMailbox.
const recip = pick(rng, recips);
const amount = U(ri(rng, 1, 5_000));
const nonce = 900000 + inboundNonce++;
const { tx } = await deliverInbound(
stack, foreignMailbox, remoteRouterB32, routerAddr, nonce, b32(recip.address), amount
);
await expect(tx, `[warp FOREIGN-MAILBOX] ${ctx} foreign mailbox minted/released`).to.be.reverted;
foreign++;
} else if (action === "badSender") {
// Valid validator-signed message via the correct mailbox, but the
// sender is NOT the enrolled remote router -> UnknownRouter revert.
const recip = pick(rng, recips);
const amount = U(ri(rng, 1, 5_000));
const nonce = 800000 + inboundNonce++;
const evilSender = b32(ethers.Wallet.createRandom().address);
const { tx } = await deliverInbound(
stack, mailbox, evilSender, routerAddr, nonce, b32(recip.address), amount
);
await expect(tx, `[warp BAD-SENDER] ${ctx} unenrolled sender accepted`).to.be.reverted;
badSender++;
} else if (action === "directHandle") {
// A random EOA calling handle() directly must revert NotMailbox and
// mint/release nothing.
const caller = pick(rng, users);
const amount = U(ri(rng, 1, 5_000));
const body = packBody(b32(caller.address), amount);
await expect(
router.connect(caller).handle(ETH_DOMAIN, remoteRouterB32, body),
`[warp DIRECT-HANDLE] ${ctx} non-mailbox handle accepted`
).to.be.revertedWithCustomError(router, "NotMailbox");
direct++;
} else if (action === "unauthConfig") {
// A non-owner cannot repoint the remote router (config theft).
const attacker = pick(rng, users);
const evil = b32(attacker.address);
await expect(
router.connect(attacker).enrollRemoteRouter(ETH_DOMAIN, evil),
`[warp UNAUTH-ENROLL] ${ctx} non-owner enrolled`
).to.be.revertedWith("Ownable: caller is not the owner");
await expect(
router.connect(attacker).unenrollRemoteRouter(ETH_DOMAIN),
`[warp UNAUTH-UNENROLL] ${ctx} non-owner unenrolled`
).to.be.revertedWith("Ownable: caller is not the owner");
}
} catch (e) {
// Never swallow an invariant break (our tagged assertions).
if (/warp [A-Z]/.test(String(e.message))) throw e;
// Otherwise a legitimate revert on an edge action -> state unchanged;
// invariants are still asserted below.
}
await assertInvariants(ctx);
steps++;
}
// Finale: bridge every remaining user balance back out (MintBurn burns the
// supply to zero; Lock drains the router's collateral to zero), proving the
// net backing collapses to exactly zero with no dust left behind.
if (mode === MODE.MintBurn) {
// Every holder of minted USDC.e (any recipient, not just the outbound
// actors) burns its balance back out, collapsing supply to zero.
const holders = [...new Set(recips.map((r) => r.address))];
for (const addr of holders) {
const holder = signers.find((s) => s.address === addr);
const bal = await token.balanceOf(addr);
if (holder && bal > 0n) {
const fee = await router.quoteTransferRemote(ETH_DOMAIN, b32(addr), bal);
await (await router.connect(holder).transferRemote(ETH_DOMAIN, b32(addr), bal, { value: fee })).wait();
messagedOut += bal;
}
}
}
if (mode === MODE.Lock) {
// Release all remaining locked collateral through verified inbounds.
let locked = messagedOut - messagedIn;
while (locked > 0n) {
const amount = locked > U(30_000) ? U(30_000) : locked;
const nonce = inboundNonce++;
const { tx } = await deliverInbound(
stack, mailbox, remoteRouterB32, routerAddr, nonce, b32(signers[5].address), amount
);
await (await tx).wait();
messagedIn += amount;
locked = messagedOut - messagedIn;
}
}
await assertInvariants(`${modeName} c${c} finale`);
if (mode === MODE.MintBurn) {
expect(await token.totalSupply(), `[warp DRAIN] ${modeName} c${c} supply not drained`).to.equal(0n);
} else {
expect(await token.balanceOf(routerAddr), `[warp DRAIN] ${modeName} c${c} collateral not drained`).to.equal(0n);
}
}
console.log(
` [warp ${modeName}] steps=${steps} bridgeIn=${mints} bridgeOut=${burns} ` +
`replays=${replays} foreignMailbox=${foreign} badSender=${badSender} directHandle=${direct}`
);
expect(steps).to.be.greaterThan(200);
expect(mints).to.be.greaterThan(30);
expect(burns).to.be.greaterThan(15);
expect(replays + foreign + badSender + direct).to.be.greaterThan(20);
}
it("MintBurn: mint only on verified inbound, backing = messagedIn-messagedOut, no double-process, no config theft", async function () {
await runMode("MintBurn", MODE.MintBurn);
});
it("Lock: release only on verified inbound, collateral = messagedOut-messagedIn, no over-release, no config theft", async function () {
await runMode("Lock", MODE.Lock);
});
// -------------------------------------------------------------------------
// TARGETED GUARDS: deterministic checks on the exact security boundaries,
// driven against the REAL mailbox so nothing hinges on a toy relayer.
// -------------------------------------------------------------------------
it("targeted guards: only-mailbox mint, replay-once, unknown-sender, foreign-mailbox, immutables, owner-gated config", async function () {
const [deployer, alice, bob, mallory] = signers;
const stack = await deployStack(MODE.MintBurn);
const { mailbox, foreignMailbox, router, token, routerAddr, remoteRouterB32 } = stack;
// A direct EOA handle() cannot mint.
await expect(
router.connect(mallory).handle(ETH_DOMAIN, remoteRouterB32, packBody(b32(mallory.address), U(1_000_000))),
"[warp G-DIRECT] direct handle minted"
).to.be.revertedWithCustomError(router, "NotMailbox");
expect(await token.totalSupply(), "[warp G-DIRECT-SUPPLY] supply moved").to.equal(0n);
// A verified inbound from the enrolled router DOES mint exactly once.
const { tx, message } = await deliverInbound(
stack, mailbox, remoteRouterB32, routerAddr, 1, b32(alice.address), U(5_000)
);
await (await tx).wait();
expect(await token.balanceOf(alice.address), "[warp G-MINT] alice not credited").to.equal(U(5_000));
expect(await token.totalSupply(), "[warp G-MINT-SUPPLY]").to.equal(U(5_000));
// Replaying that exact message reverts (mailbox replay guard) and mints nothing more.
await expect(reprocess(mailbox, message), "[warp G-REPLAY] replay accepted").to.be.revertedWith(
"AereMailboxV3: already delivered"
);
expect(await token.totalSupply(), "[warp G-REPLAY-SUPPLY] supply moved on replay").to.equal(U(5_000));
// A validator-signed message whose sender is not the enrolled remote router reverts.
const evilSender = b32(mallory.address);
const { tx: badTx } = await deliverInbound(
stack, mailbox, evilSender, routerAddr, 2, b32(bob.address), U(9_999)
);
await expect(badTx, "[warp G-BADSENDER] unenrolled sender minted").to.be.revertedWithCustomError(
router,
"UnknownRouter"
);
expect(await token.totalSupply(), "[warp G-BADSENDER-SUPPLY]").to.equal(U(5_000));
// The same message body delivered by a FOREIGN (non-configured) mailbox cannot mint.
const { tx: foreignTx } = await deliverInbound(
stack, foreignMailbox, remoteRouterB32, routerAddr, 3, b32(bob.address), U(7_777)
);
await expect(foreignTx, "[warp G-FOREIGN] foreign mailbox minted").to.be.revertedWithCustomError(
router,
"NotMailbox"
);
expect(await token.totalSupply(), "[warp G-FOREIGN-SUPPLY]").to.equal(U(5_000));
// Owner-gated config: only the owner can enroll/unenroll.
await expect(
router.connect(mallory).enrollRemoteRouter(ETH_DOMAIN, b32(mallory.address)),
"[warp G-ENROLL] non-owner enrolled"
).to.be.revertedWith("Ownable: caller is not the owner");
await expect(
router.connect(mallory).unenrollRemoteRouter(ETH_DOMAIN),
"[warp G-UNENROLL] non-owner unenrolled"
).to.be.revertedWith("Ownable: caller is not the owner");
// Immutables cannot be repointed by anyone (no setter exists at all).
expect(await router.MAILBOX(), "[warp G-IMMUT-MAILBOX]").to.equal(await mailbox.getAddress());
expect(await router.TOKEN(), "[warp G-IMMUT-TOKEN]").to.equal(await token.getAddress());
expect(await router.MODE(), "[warp G-IMMUT-MODE]").to.equal(1n);
// A user can bridge the minted balance back out (burn), and the supply falls
// by exactly that amount: mint/burn is symmetric and conservative.
const fee = await router.quoteTransferRemote(ETH_DOMAIN, b32(alice.address), U(5_000));
await (await router.connect(alice).transferRemote(ETH_DOMAIN, b32(alice.address), U(5_000), { value: fee })).wait();
expect(await token.totalSupply(), "[warp G-BURN] supply not burned to zero").to.equal(0n);
expect(await router.totalReceivedIn(), "[warp G-IN]").to.equal(U(5_000));
expect(await router.totalSentOut(), "[warp G-OUT]").to.equal(U(5_000));
// A transferRemote to an un-enrolled destination reverts (no silent burn).
await expect(
router.connect(alice).transferRemote(9999, b32(alice.address), U(1), { value: 0 }),
"[warp G-NO-ROUTER] burn to un-enrolled destination"
).to.be.revertedWithCustomError(router, "NoRouterEnrolled");
console.log(" [warp guards] all mint/release security boundaries held against the real mailbox");
});
});