// AereWarpRouteV3 — USDC.e Hyperlane v3 route on the AERE side (chain 2800), // bound to a faithful AereMailboxV3-like mock. Covers both token modes: // MintBurn (synthetic USDC.e, the canonical bridged representation) and // Lock (release an existing token the router does not mint). // // Run: npx hardhat test test/aere-warp-route-v3.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); const ETH_DOMAIN = 1; const AERE_DOMAIN = 2800; const SIX = 10n ** 6n; function addrToBytes32(a) { return ethers.zeroPadValue(a, 32); } function packBody(recipientB32, amount) { return ethers.solidityPacked(["bytes32", "uint256"], [recipientB32, amount]); } describe("AereWarpRouteV3 — USDC.e Hyperlane v3 route (AERE side)", function () { let deployer, alice, bob, carol; let mailbox, igp; beforeEach(async function () { [deployer, alice, bob, carol] = await ethers.getSigners(); mailbox = await (await ethers.getContractFactory("MockWarpMailboxV3")).deploy(AERE_DOMAIN); await mailbox.waitForDeployment(); igp = await (await ethers.getContractFactory("MockWarpIGPV3")).deploy(); await igp.waitForDeployment(); }); // ─────────────────────── MintBurn (synthetic USDC.e) ─────────────────────── describe("MintBurn mode (synthetic USDC.e)", function () { let usdce, router; beforeEach(async function () { // USDC.e's BRIDGE is immutable, so predict the router address (deployer // nonce + 1) and deploy USDC.e pointing at it first — same pattern the // multi-stable-warp deploy script uses. const nonce = await ethers.provider.getTransactionCount(deployer.address); const futureRouter = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 }); usdce = await (await ethers.getContractFactory("USDCe")).deploy(futureRouter); await usdce.waitForDeployment(); router = await (await ethers.getContractFactory("AereWarpRouteV3")).deploy( await mailbox.getAddress(), await igp.getAddress(), await usdce.getAddress(), 1 // Mode.MintBurn ); await router.waitForDeployment(); expect((await router.getAddress()).toLowerCase()).to.equal(futureRouter.toLowerCase()); expect(await usdce.BRIDGE()).to.equal(await router.getAddress()); }); it("wires immutables (mailbox / igp / token / mode)", async function () { expect(await router.MAILBOX()).to.equal(await mailbox.getAddress()); expect(await router.IGP()).to.equal(await igp.getAddress()); expect(await router.TOKEN()).to.equal(await usdce.getAddress()); expect(await router.MODE()).to.equal(1n); }); it("enrollRemoteRouter is owner-only", async function () { const r = addrToBytes32(carol.address); await expect(router.connect(alice).enrollRemoteRouter(ETH_DOMAIN, r)) .to.be.revertedWith("Ownable: caller is not the owner"); await expect(router.enrollRemoteRouter(ETH_DOMAIN, r)) .to.emit(router, "RemoteRouterEnrolled").withArgs(ETH_DOMAIN, r); expect(await router.routers(ETH_DOMAIN)).to.equal(r); }); it("handle from a non-mailbox caller reverts", async function () { await expect( router.connect(alice).handle(ETH_DOMAIN, addrToBytes32(carol.address), "0x" + "00".repeat(64)) ).to.be.revertedWithCustomError(router, "NotMailbox"); }); it("handle from an unenrolled remote router reverts", async function () { const body = packBody(addrToBytes32(bob.address), 5n * SIX); await expect( mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, addrToBytes32(carol.address), body) ).to.be.revertedWithCustomError(router, "UnknownRouter"); }); it("enrolled inbound mints exactly the amount to the recipient", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); const amount = 123n * SIX; const body = packBody(addrToBytes32(bob.address), amount); await expect(mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, remote, body)) .to.emit(router, "ReceivedTransferRemote").withArgs(ETH_DOMAIN, bob.address, amount); expect(await usdce.balanceOf(bob.address)).to.equal(amount); expect(await usdce.totalSupply()).to.equal(amount); expect(await router.totalReceivedIn()).to.equal(amount); }); it("transferRemote burns caller tokens, quotes, dispatches with msg.value==quote, emits", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); // Seed bob with USDC.e via a verified inbound. const seed = 100n * SIX; await mailbox.mockDeliver( await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), seed) ); const amount = 40n * SIX; const recipient = addrToBytes32(alice.address); const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount); expect(fee).to.equal(await mailbox.feeAmount()); await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: fee })) .to.emit(router, "SentTransferRemote"); // Caller's tokens burned. expect(await usdce.balanceOf(bob.address)).to.equal(seed - amount); // Dispatch received exactly the quoted fee, with the right payload. expect(await mailbox.lastFeePaid()).to.equal(fee); expect(await mailbox.lastDestination()).to.equal(ETH_DOMAIN); expect(await mailbox.lastRecipient()).to.equal(remote); expect(await mailbox.lastBody()).to.equal(packBody(recipient, amount)); // No dust left in the router; mailbox holds exactly the fee. expect(await ethers.provider.getBalance(await router.getAddress())).to.equal(0n); expect(await ethers.provider.getBalance(await mailbox.getAddress())).to.equal(fee); expect(await router.totalSentOut()).to.equal(amount); }); it("transferRemote refunds overpayment above the quote", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); await mailbox.mockDeliver( await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), 10n * SIX) ); const amount = 3n * SIX; const recipient = addrToBytes32(alice.address); const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount); const overpay = fee + ethers.parseEther("0.05"); const balBefore = await ethers.provider.getBalance(bob.address); const tx = await router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: overpay }); const rc = await tx.wait(); const gas = rc.gasUsed * rc.gasPrice; const balAfter = await ethers.provider.getBalance(bob.address); expect(balBefore - balAfter - gas).to.equal(fee); expect(await ethers.provider.getBalance(await router.getAddress())).to.equal(0n); }); it("transferRemote reverts when msg.value < quote", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); await mailbox.mockDeliver( await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), 5n * SIX) ); const recipient = addrToBytes32(alice.address); const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, 1n * SIX); await expect( router.connect(bob).transferRemote(ETH_DOMAIN, recipient, 1n * SIX, { value: fee - 1n }) ).to.be.revertedWithCustomError(router, "InsufficientGasPayment"); }); it("transferRemote input guards: no router / zero amount / zero recipient", async function () { const recipient = addrToBytes32(alice.address); const fee = ethers.parseEther("0.001"); await expect(router.connect(bob).transferRemote(999, recipient, 1n, { value: fee })) .to.be.revertedWithCustomError(router, "NoRouterEnrolled"); await router.enrollRemoteRouter(ETH_DOMAIN, addrToBytes32(carol.address)); await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, 0n, { value: fee })) .to.be.revertedWithCustomError(router, "ZeroAmount"); await expect(router.connect(bob).transferRemote(ETH_DOMAIN, ethers.ZeroHash, 1n, { value: fee })) .to.be.revertedWithCustomError(router, "ZeroRecipient"); }); it("quoteGasPayment view reads the IGP", async function () { expect(await router.quoteGasPayment(ETH_DOMAIN, 50000n)).to.equal(50000n * 10n ** 9n); }); }); // ─────────────────────── Lock (release existing token) ─────────────────────── describe("Lock mode (release existing token)", function () { let token, router; beforeEach(async function () { token = await (await ethers.getContractFactory("MockUSDC")).deploy(); await token.waitForDeployment(); router = await (await ethers.getContractFactory("AereWarpRouteV3")).deploy( await mailbox.getAddress(), await igp.getAddress(), await token.getAddress(), 0 // Mode.Lock ); await router.waitForDeployment(); }); it("transferRemote locks tokens into the router and dispatches", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); const amount = 250n * SIX; await token.mint(bob.address, amount); await token.connect(bob).approve(await router.getAddress(), amount); const recipient = addrToBytes32(alice.address); const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount); await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: fee })) .to.emit(router, "SentTransferRemote"); expect(await token.balanceOf(await router.getAddress())).to.equal(amount); expect(await token.balanceOf(bob.address)).to.equal(0n); expect(await router.lockedSupply()).to.equal(amount); }); it("enrolled inbound releases exactly the amount to the recipient", async function () { const remote = addrToBytes32(carol.address); await router.enrollRemoteRouter(ETH_DOMAIN, remote); await token.mint(await router.getAddress(), 500n * SIX); const amount = 175n * SIX; const body = packBody(addrToBytes32(bob.address), amount); await expect(mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, remote, body)) .to.emit(router, "ReceivedTransferRemote").withArgs(ETH_DOMAIN, bob.address, amount); expect(await token.balanceOf(bob.address)).to.equal(amount); expect(await router.lockedSupply()).to.equal(500n * SIX - amount); }); it("handle from an unenrolled remote router reverts", async function () { await token.mint(await router.getAddress(), 100n * SIX); const body = packBody(addrToBytes32(bob.address), 1n * SIX); await expect( mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, addrToBytes32(carol.address), body) ).to.be.revertedWithCustomError(router, "UnknownRouter"); }); }); });