// ============================================================================= // 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"); }); });