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.
235 lines
10 KiB
JavaScript
235 lines
10 KiB
JavaScript
// End-to-end test of the USDC.e Hyperlane Warp Route:
|
|
// Ethereum (chain 1) → Lock native USDC in AereHypERC20Collateral
|
|
// → MockMailbox dispatch → MockMailbox sister handle
|
|
// → AereHypERC20Synthetic mints USDC.e to recipient on AERE (chain 2800).
|
|
//
|
|
// Round-trip: User on AERE burns USDC.e via transferRemote
|
|
// → reverse-direction message → AereHypERC20Collateral releases native USDC.
|
|
//
|
|
// Mocks the Mailbox via MockHypMailbox (in-tx delivery, no relayer needed).
|
|
//
|
|
// Run: npx hardhat test test/usdce-warp-route.test.js
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ETH_DOMAIN = 1; // Hyperlane mainnet domain id
|
|
const AERE_DOMAIN = 2800; // we use chainId as domain id (matches actual Hyperlane convention for AERE deploy)
|
|
const SIX_DECIMALS = 10n ** 6n;
|
|
|
|
function addrToBytes32(addr) {
|
|
return ethers.zeroPadValue(addr, 32);
|
|
}
|
|
|
|
describe("USDC.e Hyperlane Warp Route — full round trip", function () {
|
|
let deployer, alice, bob;
|
|
let ethMailbox, aereMailbox;
|
|
let usdcNative; // native USDC on Ethereum (mock)
|
|
let collateral; // AereHypERC20Collateral on Ethereum
|
|
let usdce; // USDC.e ERC-20 on AERE
|
|
let synthetic; // AereHypERC20Synthetic on AERE
|
|
|
|
beforeEach(async function () {
|
|
[deployer, alice, bob] = await ethers.getSigners();
|
|
|
|
// 1) Two mock Mailboxes (one per chain).
|
|
const Mailbox = await ethers.getContractFactory("MockHypMailbox");
|
|
ethMailbox = await Mailbox.deploy(ETH_DOMAIN);
|
|
await ethMailbox.waitForDeployment();
|
|
aereMailbox = await Mailbox.deploy(AERE_DOMAIN);
|
|
await aereMailbox.waitForDeployment();
|
|
|
|
// 2) Native USDC mock on Ethereum, with seed balance for Alice.
|
|
usdcNative = await (await ethers.getContractFactory("MockUSDC")).deploy();
|
|
await usdcNative.waitForDeployment();
|
|
await usdcNative.mint(alice.address, 1_000_000n * SIX_DECIMALS);
|
|
// Liquidity for the Ethereum-side collateral lockbox to release on inbound.
|
|
// (In production, the collateral starts empty and accrues with deposits.)
|
|
|
|
// 3) Collateral lockbox on Ethereum.
|
|
collateral = await (await ethers.getContractFactory("AereHypERC20Collateral")).deploy(
|
|
await ethMailbox.getAddress(),
|
|
await usdcNative.getAddress()
|
|
);
|
|
await collateral.waitForDeployment();
|
|
|
|
// 4) USDC.e on AERE — Synthetic deploys AFTER it (synthetic is the BRIDGE).
|
|
// Compute the synthetic's future address so we can pass it into USDC.e's constructor.
|
|
const nonce = await ethers.provider.getTransactionCount(deployer.address);
|
|
const futureSyntheticAddr = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });
|
|
|
|
usdce = await (await ethers.getContractFactory("USDCe")).deploy(futureSyntheticAddr);
|
|
await usdce.waitForDeployment();
|
|
|
|
synthetic = await (await ethers.getContractFactory("AereHypERC20Synthetic")).deploy(
|
|
await aereMailbox.getAddress(),
|
|
await usdce.getAddress(),
|
|
6 // AUDIT FIX (HIGH #17): expectedDecimals (USDC.e has 6)
|
|
);
|
|
await synthetic.waitForDeployment();
|
|
|
|
expect((await synthetic.getAddress()).toLowerCase()).to.equal(futureSyntheticAddr.toLowerCase());
|
|
expect(await usdce.BRIDGE()).to.equal(await synthetic.getAddress());
|
|
|
|
// 5) Enroll trusted routers (each side knows the other's contract address).
|
|
await collateral.enrollRouter(AERE_DOMAIN, addrToBytes32(await synthetic.getAddress()));
|
|
// AUDIT FIX (HIGH #16): synthetic uses 7-day timelock now.
|
|
await synthetic.proposeEnrollRouter(ETH_DOMAIN, addrToBytes32(await collateral.getAddress()));
|
|
await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await synthetic.finalizeEnrollRouter(ETH_DOMAIN);
|
|
|
|
// 6) Pair the mock Mailboxes. Each Mailbox knows its sister AND the
|
|
// local-side application contract that receives inbound .handle()
|
|
// calls. The sister Mailbox is the one that performs the delivery
|
|
// so the recipient sees msg.sender == local Mailbox (matching
|
|
// production Hyperlane semantics).
|
|
await ethMailbox.setSister(await aereMailbox.getAddress(), await collateral.getAddress());
|
|
await aereMailbox.setSister(await ethMailbox.getAddress(), await synthetic.getAddress());
|
|
});
|
|
|
|
it("deposit (Ethereum → AERE): lock USDC, mint USDC.e", async function () {
|
|
const amount = 100n * SIX_DECIMALS; // 100 USDC
|
|
|
|
// Alice approves the collateral and bridges 100 USDC to bob's address on AERE.
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
|
|
const fee = await ethMailbox.fee();
|
|
|
|
const tx = await collateral.connect(alice).transferRemote(
|
|
AERE_DOMAIN,
|
|
addrToBytes32(bob.address),
|
|
amount,
|
|
{ value: fee }
|
|
);
|
|
const receipt = await tx.wait();
|
|
|
|
// Collateral locked.
|
|
expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount);
|
|
expect(await usdcNative.balanceOf(alice.address)).to.equal(1_000_000n * SIX_DECIMALS - amount);
|
|
|
|
// USDC.e minted to bob on AERE.
|
|
expect(await usdce.balanceOf(bob.address)).to.equal(amount);
|
|
expect(await usdce.totalSupply()).to.equal(amount);
|
|
|
|
// ReceivedTransferRemote emitted on AERE side.
|
|
const aereLogs = await aereMailbox.queryFilter(aereMailbox.filters.Handled());
|
|
expect(aereLogs.length).to.equal(1);
|
|
});
|
|
|
|
it("withdrawal (AERE → Ethereum): burn USDC.e, release USDC", async function () {
|
|
// First bridge 200 USDC over so Bob has USDC.e to send back.
|
|
const amount = 200n * SIX_DECIMALS;
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
|
|
const fee = await ethMailbox.fee();
|
|
await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), amount, { value: fee });
|
|
|
|
expect(await usdce.balanceOf(bob.address)).to.equal(amount);
|
|
expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount);
|
|
|
|
// Now Bob bridges 80 USDC.e back to Alice on Ethereum.
|
|
const back = 80n * SIX_DECIMALS;
|
|
const feeBack = await aereMailbox.fee();
|
|
await synthetic.connect(bob).transferRemote(
|
|
ETH_DOMAIN,
|
|
addrToBytes32(alice.address),
|
|
back,
|
|
{ value: feeBack }
|
|
);
|
|
|
|
// Bob's USDC.e burned.
|
|
expect(await usdce.balanceOf(bob.address)).to.equal(amount - back);
|
|
|
|
// Alice received 80 USDC on Ethereum.
|
|
expect(await usdcNative.balanceOf(alice.address)).to.equal(1_000_000n * SIX_DECIMALS - amount + back);
|
|
|
|
// Collateral lockbox decreased by 80 USDC.
|
|
expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount - back);
|
|
});
|
|
|
|
it("reserve invariant: collateral.lockedSupply() == usdce.totalSupply()", async function () {
|
|
const amount = 500n * SIX_DECIMALS;
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
|
|
const fee = await ethMailbox.fee();
|
|
await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), amount, { value: fee });
|
|
expect(await collateral.lockedSupply()).to.equal(await usdce.totalSupply());
|
|
|
|
// Half round-trip:
|
|
const back = 200n * SIX_DECIMALS;
|
|
const feeBack = await aereMailbox.fee();
|
|
await synthetic.connect(bob).transferRemote(ETH_DOMAIN, addrToBytes32(alice.address), back, { value: feeBack });
|
|
expect(await collateral.lockedSupply()).to.equal(await usdce.totalSupply());
|
|
});
|
|
|
|
it("rejects mint from non-bridge caller", async function () {
|
|
await expect(usdce.connect(alice).mint(alice.address, 1n)).to.be.revertedWithCustomError(usdce, "OnlyBridge");
|
|
await expect(usdce.connect(bob).burnFrom(bob.address, 1n)).to.be.revertedWithCustomError(usdce, "OnlyBridge");
|
|
});
|
|
|
|
it("rejects handle() from non-mailbox caller", async function () {
|
|
await expect(
|
|
synthetic.connect(alice).handle(
|
|
ETH_DOMAIN,
|
|
addrToBytes32(await collateral.getAddress()),
|
|
ethers.zeroPadValue("0x00", 64)
|
|
)
|
|
).to.be.revertedWithCustomError(synthetic, "NotMailbox");
|
|
});
|
|
|
|
it("rejects message from unknown router (replay-protection across domains)", async function () {
|
|
// De-enroll the AERE synthetic on collateral, then simulate inbound to collateral
|
|
// from AereMailbox.
|
|
await collateral.unenrollRouter(AERE_DOMAIN);
|
|
// Re-pair the mocks so dispatching from AereMailbox reaches collateral.
|
|
// (Already set in beforeEach; we just call the path.)
|
|
const feeBack = await aereMailbox.fee();
|
|
|
|
// Try a transfer Aere → Eth — collateral has no router enrolled for AERE_DOMAIN, so
|
|
// the inbound handle() on collateral reverts UnknownRouter.
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), 100n * SIX_DECIMALS);
|
|
await collateral.enrollRouter(AERE_DOMAIN, addrToBytes32(await synthetic.getAddress())); // re-enroll for first leg
|
|
await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), 100n * SIX_DECIMALS, { value: feeBack });
|
|
|
|
// De-enroll AGAIN to simulate a router rotation that breaks reverse leg.
|
|
await collateral.unenrollRouter(AERE_DOMAIN);
|
|
|
|
await expect(
|
|
synthetic.connect(bob).transferRemote(ETH_DOMAIN, addrToBytes32(alice.address), 50n * SIX_DECIMALS, { value: feeBack })
|
|
).to.be.revertedWithCustomError(collateral, "UnknownRouter");
|
|
});
|
|
|
|
it("requires sufficient IGP fee on outbound", async function () {
|
|
const amount = 1n * SIX_DECIMALS;
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
|
|
const fee = await ethMailbox.fee();
|
|
await expect(
|
|
collateral.connect(alice).transferRemote(
|
|
AERE_DOMAIN,
|
|
addrToBytes32(bob.address),
|
|
amount,
|
|
{ value: fee - 1n }
|
|
)
|
|
).to.be.revertedWithCustomError(collateral, "InsufficientIgpPayment");
|
|
});
|
|
|
|
it("refunds overpayment of IGP fee", async function () {
|
|
const amount = 50n * SIX_DECIMALS;
|
|
await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
|
|
const fee = await ethMailbox.fee();
|
|
const overpay = fee + 5n * 10n ** 15n; // fee + 0.005 ETH
|
|
|
|
const balBefore = await ethers.provider.getBalance(alice.address);
|
|
const tx = await collateral.connect(alice).transferRemote(
|
|
AERE_DOMAIN,
|
|
addrToBytes32(bob.address),
|
|
amount,
|
|
{ value: overpay }
|
|
);
|
|
const receipt = await tx.wait();
|
|
const gasCost = receipt.gasUsed * receipt.gasPrice;
|
|
const balAfter = await ethers.provider.getBalance(alice.address);
|
|
|
|
// Alice paid only fee + gas, refund of (overpay - fee) returned.
|
|
expect(balBefore - balAfter - gasCost).to.equal(fee);
|
|
});
|
|
});
|