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.
240 lines
12 KiB
JavaScript
240 lines
12 KiB
JavaScript
// AereAccountMigrator tests.
|
|
//
|
|
// Proves the account-layer migration path is real, no-custody, fail-closed, and
|
|
// atomic:
|
|
// - a user moves ERC-20 balances from an EOA to a destination in one tx
|
|
// (both the plain `migrate` and the Falcon-derived `migrateToPqcAccount`);
|
|
// - native AERE forwards to the destination;
|
|
// - a zero destination reverts;
|
|
// - a failed token transfer reverts the WHOLE migration (atomicity);
|
|
// - the migrator can only send to the specified destination (nothing leaks to
|
|
// a third party);
|
|
// - there is no admin function that can drain, and the migrator holds no custody.
|
|
//
|
|
// The destination in the strong path is a REAL AerePQCAccount deployed through the
|
|
// REAL AerePQCAccountFactory (Falcon verify mocked, since a genuine Falcon-512
|
|
// signature cannot be produced inside the hardhat process; the CREATE2 derivation
|
|
// and asset movement are fully real).
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ONE18 = 10n ** 18n;
|
|
|
|
// A valid-shape 897-byte NIST Falcon-512 public key: header 0x09 then zeros.
|
|
// predictAddress / createAccount only check length + header, so this exercises
|
|
// the real CREATE2 path without a real key.
|
|
function falconPubKey() {
|
|
const b = new Uint8Array(897);
|
|
b[0] = 0x09;
|
|
return "0x" + Buffer.from(b).toString("hex");
|
|
}
|
|
|
|
async function deployStack() {
|
|
const [deployer, alice, bob, carol] = await ethers.getSigners();
|
|
|
|
const ERC20 = await ethers.getContractFactory("MockERC20Lending");
|
|
const tokenA = await ERC20.deploy("Token A", "TKA", 18);
|
|
await tokenA.waitForDeployment();
|
|
const tokenB = await ERC20.deploy("Token B", "TKB", 18);
|
|
await tokenB.waitForDeployment();
|
|
|
|
// Real PQC account factory (Falcon verify + entryPoint mocked / placeholder).
|
|
const Verifier = await ethers.getContractFactory("MockFalcon512Verifier");
|
|
const verifier = await Verifier.deploy();
|
|
await verifier.waitForDeployment();
|
|
await verifier.setDefault(true);
|
|
|
|
const entryPoint = deployer.address; // any non-zero address; unused by migration
|
|
|
|
const Factory = await ethers.getContractFactory("AerePQCAccountFactory");
|
|
const factory = await Factory.deploy(entryPoint, await verifier.getAddress());
|
|
await factory.waitForDeployment();
|
|
|
|
const Migrator = await ethers.getContractFactory("AereAccountMigrator");
|
|
const migrator = await Migrator.deploy();
|
|
await migrator.waitForDeployment();
|
|
|
|
return { deployer, alice, bob, carol, tokenA, tokenB, factory, migrator };
|
|
}
|
|
|
|
describe("AereAccountMigrator", function () {
|
|
it("migrates ERC-20 balances from an EOA to a destination in one tx", async function () {
|
|
const env = await deployStack();
|
|
const pk = falconPubKey();
|
|
const salt = 42n;
|
|
|
|
// Deploy the real destination PQC account through the real factory.
|
|
await env.factory.createAccount(pk, salt);
|
|
const dest = await env.factory.predictAddress(pk, salt);
|
|
expect(await ethers.provider.getCode(dest)).to.not.equal("0x");
|
|
|
|
// Alice (old EOA) funds and approves the migrator for both tokens.
|
|
await env.tokenA.mint(env.alice.address, 100n * ONE18);
|
|
await env.tokenB.mint(env.alice.address, 250n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);
|
|
await env.tokenB.connect(env.alice).approve(await env.migrator.getAddress(), 250n * ONE18);
|
|
|
|
const tokens = [await env.tokenA.getAddress(), await env.tokenB.getAddress()];
|
|
const amounts = [100n * ONE18, 250n * ONE18];
|
|
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, tokens, amounts, false))
|
|
.to.emit(env.migrator, "Migrated")
|
|
.withArgs(env.alice.address, dest, tokens, amounts, 0n);
|
|
|
|
// Everything landed on the destination; nothing left on the old EOA.
|
|
expect(await env.tokenA.balanceOf(dest)).to.equal(100n * ONE18);
|
|
expect(await env.tokenB.balanceOf(dest)).to.equal(250n * ONE18);
|
|
expect(await env.tokenA.balanceOf(env.alice.address)).to.equal(0n);
|
|
expect(await env.tokenB.balanceOf(env.alice.address)).to.equal(0n);
|
|
// The migrator took no custody.
|
|
expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(0n);
|
|
expect(await env.tokenB.balanceOf(await env.migrator.getAddress())).to.equal(0n);
|
|
});
|
|
|
|
it("migrateToPqcAccount derives the destination from the Falcon key via the live factory", async function () {
|
|
const env = await deployStack();
|
|
const pk = falconPubKey();
|
|
const salt = 7n;
|
|
const dest = await env.factory.predictAddress(pk, salt);
|
|
|
|
await env.tokenA.mint(env.alice.address, 500n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 500n * ONE18);
|
|
|
|
const tokens = [await env.tokenA.getAddress()];
|
|
const amounts = [500n * ONE18];
|
|
|
|
// expectedDestination guard matches -> ok. Note: destination need NOT be
|
|
// deployed yet; it can hold assets at the counterfactual address.
|
|
await expect(env.migrator.connect(env.alice).migrateToPqcAccount(
|
|
await env.factory.getAddress(), pk, salt, dest, tokens, amounts, false
|
|
)).to.emit(env.migrator, "Migrated").withArgs(env.alice.address, dest, tokens, amounts, 0n);
|
|
|
|
expect(await env.tokenA.balanceOf(dest)).to.equal(500n * ONE18);
|
|
|
|
// A wrong expectedDestination fails closed.
|
|
await env.tokenA.mint(env.alice.address, 1n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
|
|
await expect(env.migrator.connect(env.alice).migrateToPqcAccount(
|
|
await env.factory.getAddress(), pk, salt, env.bob.address, tokens, [1n * ONE18], false
|
|
)).to.be.revertedWithCustomError(env.migrator, "AddressMismatch");
|
|
});
|
|
|
|
it("forwards native AERE to the destination in the same migration", async function () {
|
|
const env = await deployStack();
|
|
const pk = falconPubKey();
|
|
const salt = 9n;
|
|
await env.factory.createAccount(pk, salt); // real account has receive()
|
|
const dest = await env.factory.predictAddress(pk, salt);
|
|
|
|
const before = await ethers.provider.getBalance(dest);
|
|
await env.migrator.connect(env.alice).migrate(dest, [], [], true, { value: 3n * ONE18 });
|
|
const after = await ethers.provider.getBalance(dest);
|
|
expect(after - before).to.equal(3n * ONE18);
|
|
// No native stuck in the migrator.
|
|
expect(await ethers.provider.getBalance(await env.migrator.getAddress())).to.equal(0n);
|
|
});
|
|
|
|
it("reverts on a zero destination", async function () {
|
|
const env = await deployStack();
|
|
await env.tokenA.mint(env.alice.address, 1n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
|
|
await expect(env.migrator.connect(env.alice).migrate(
|
|
ethers.ZeroAddress, [await env.tokenA.getAddress()], [1n * ONE18], false
|
|
)).to.be.revertedWithCustomError(env.migrator, "ZeroDestination");
|
|
});
|
|
|
|
it("is atomic: a failed token transfer reverts the whole migration", async function () {
|
|
const env = await deployStack();
|
|
const pk = falconPubKey();
|
|
const salt = 11n;
|
|
const dest = await env.factory.predictAddress(pk, salt);
|
|
|
|
await env.tokenA.mint(env.alice.address, 100n * ONE18);
|
|
await env.tokenB.mint(env.alice.address, 100n * ONE18);
|
|
// Approve tokenA fully, but UNDER-approve tokenB so its transfer fails.
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);
|
|
await env.tokenB.connect(env.alice).approve(await env.migrator.getAddress(), 10n * ONE18);
|
|
|
|
const tokens = [await env.tokenA.getAddress(), await env.tokenB.getAddress()];
|
|
const amounts = [100n * ONE18, 100n * ONE18];
|
|
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, tokens, amounts, false)).to.be.reverted;
|
|
|
|
// tokenA (the first, "successful" leg) was NOT moved: no partial migration.
|
|
expect(await env.tokenA.balanceOf(dest)).to.equal(0n);
|
|
expect(await env.tokenA.balanceOf(env.alice.address)).to.equal(100n * ONE18);
|
|
expect(await env.tokenB.balanceOf(dest)).to.equal(0n);
|
|
});
|
|
|
|
it("can only send to the specified destination; nothing leaks to a third party", async function () {
|
|
const env = await deployStack();
|
|
const dest = env.carol.address;
|
|
|
|
await env.tokenA.mint(env.alice.address, 100n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);
|
|
|
|
const bobBefore = await env.tokenA.balanceOf(env.bob.address);
|
|
await env.migrator.connect(env.alice).migrate(dest, [await env.tokenA.getAddress()], [100n * ONE18], false);
|
|
|
|
// Only `dest` (carol) received; an unrelated address (bob) got nothing, and
|
|
// the migrator retained nothing.
|
|
expect(await env.tokenA.balanceOf(dest)).to.equal(100n * ONE18);
|
|
expect(await env.tokenA.balanceOf(env.bob.address)).to.equal(bobBefore);
|
|
expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(0n);
|
|
});
|
|
|
|
it("has no admin drain: no owner/withdraw/rescue/sweep, and stray tokens cannot be pulled out", async function () {
|
|
const env = await deployStack();
|
|
const iface = env.migrator.interface;
|
|
// No admin / custody-exit surface exists in the ABI.
|
|
for (const fn of ["owner", "withdraw", "rescue", "rescueERC20", "sweep", "sweepToken", "transferOwnership", "initialize"]) {
|
|
expect(iface.getFunction(fn), `unexpected function ${fn}`).to.equal(null);
|
|
}
|
|
|
|
// Even if tokens are force-sent to the migrator, there is no function to move
|
|
// them out: the balance is stuck-but-safe (no privileged drain).
|
|
await env.tokenA.mint(await env.migrator.getAddress(), 5n * ONE18);
|
|
expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(5n * ONE18);
|
|
// The only asset-moving entrypoints require the CALLER to be the token source;
|
|
// they cannot move the migrator's own balance to anyone.
|
|
// (Confirmed by ABI absence above; nothing further to call.)
|
|
});
|
|
|
|
it("fails closed on native: stray value when not moving native, and moveNative with zero value", async function () {
|
|
const env = await deployStack();
|
|
const dest = env.carol.address;
|
|
await env.tokenA.mint(env.alice.address, 1n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
|
|
|
|
// Sending value while moveNative=false must revert (StrayNative).
|
|
await expect(env.migrator.connect(env.alice).migrate(
|
|
dest, [await env.tokenA.getAddress()], [1n * ONE18], false, { value: 1n }
|
|
)).to.be.revertedWithCustomError(env.migrator, "StrayNative");
|
|
|
|
// moveNative=true with zero value must revert (ZeroNative).
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, [], [], true, { value: 0n }))
|
|
.to.be.revertedWithCustomError(env.migrator, "ZeroNative");
|
|
|
|
// An empty migration (no tokens, no native) reverts (NothingToMigrate).
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, [], [], false))
|
|
.to.be.revertedWithCustomError(env.migrator, "NothingToMigrate");
|
|
});
|
|
|
|
it("rejects malformed asset sets: length mismatch, zero token, zero amount", async function () {
|
|
const env = await deployStack();
|
|
const dest = env.carol.address;
|
|
const a = await env.tokenA.getAddress();
|
|
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, [a], [1n, 2n], false))
|
|
.to.be.revertedWithCustomError(env.migrator, "LengthMismatch");
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, [ethers.ZeroAddress], [1n], false))
|
|
.to.be.revertedWithCustomError(env.migrator, "ZeroToken");
|
|
|
|
await env.tokenA.mint(env.alice.address, 1n * ONE18);
|
|
await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
|
|
await expect(env.migrator.connect(env.alice).migrate(dest, [a], [0n], false))
|
|
.to.be.revertedWithCustomError(env.migrator, "ZeroAmount");
|
|
});
|
|
});
|