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