const { expect } = require("chai"); const { ethers } = require("hardhat"); // Unit tests for the post-quantum ERC-4337 smart account (roadmap #270). // // The Falcon-512 verification itself is exhaustively KAT-validated in // AereFalcon512Verifier.test.js, and the REAL end-to-end Falcon path (genuine // reference-generated keypair + signature, verified by the LIVE on-chain // verifier) is proven on mainnet by scripts/deploy-pqc-account.js. Here we // isolate the ACCOUNT + FACTORY logic with a settable MockFalcon512Verifier so // both the accept and reject branches of every path are asserted precisely. const PK = "0x09" + "ab".repeat(896); // 897-byte, 0x09-headed Falcon-512 pubkey shape const BAD_PK_LEN = "0x09" + "ab".repeat(10); const BAD_PK_HDR = "0x08" + "ab".repeat(896); const NONCE = "0x" + "11".repeat(40); // 40-byte Falcon salt const COMPSIG = "0x" + "22".repeat(80); // compressed s2 encoding (dummy) const coder = ethers.AbiCoder.defaultAbiCoder(); const envelope = (nonce, compSig) => coder.encode(["bytes", "bytes"], [nonce, compSig]); // EIP-1271 (isValidSignature) verifies the raw 32 bytes of the hash. const msgOf = (hash) => hash; // The fund-moving paths (validateUserOp, executeWithFalcon) are DOMAIN-SEPARATED: // they verify over abi.encodePacked(domain, hash) = 64 bytes. Constants must match // the contract, so a raw-hash EIP-1271 login signature can never authorize a spend. const USEROP_DOMAIN = ethers.id("AerePQCAccount.validateUserOp.v1"); const EXEC_DOMAIN = ethers.id("AerePQCAccount.executeWithFalcon.v1"); const msgDomained = (domain, hash) => ethers.concat([domain, hash]); async function deployFixture() { const [deployer, entryPointEOA, other] = await ethers.getSigners(); const Mock = await ethers.getContractFactory("MockFalcon512Verifier"); const mock = await Mock.deploy(); await mock.waitForDeployment(); const Factory = await ethers.getContractFactory("AerePQCAccountFactory"); const factory = await Factory.deploy(entryPointEOA.address, await mock.getAddress()); await factory.waitForDeployment(); return { deployer, entryPointEOA, other, mock, factory }; } describe("AerePQCAccountFactory", function () { it("predicts deterministically and deploys at the predicted address", async function () { const { factory } = await deployFixture(); const predicted = await factory.predictAddress(PK, 0); await (await factory.createAccount(PK, 0)).wait(); expect(await ethers.provider.getCode(predicted)).to.not.equal("0x"); }); it("is idempotent: a second createAccount returns the same account", async function () { const { factory } = await deployFixture(); const predicted = await factory.predictAddress(PK, 0); await (await factory.createAccount(PK, 0)).wait(); await (await factory.createAccount(PK, 0)).wait(); // no revert, no redeploy const acct = await ethers.getContractAt("AerePQCAccount", predicted); expect((await acct.falconPubKey()).toLowerCase()).to.equal(PK.toLowerCase()); }); it("different salt or different pubkey => different address", async function () { const { factory } = await deployFixture(); const a = await factory.predictAddress(PK, 0); const b = await factory.predictAddress(PK, 1); const c = await factory.predictAddress("0x09" + "cd".repeat(896), 0); expect(a).to.not.equal(b); expect(a).to.not.equal(c); }); it("rejects a malformed Falcon public key", async function () { const { factory } = await deployFixture(); await expect(factory.createAccount(BAD_PK_LEN, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey"); await expect(factory.createAccount(BAD_PK_HDR, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey"); }); it("wires the account's owner key, entryPoint and verifier", async function () { const { factory, entryPointEOA, mock } = await deployFixture(); const predicted = await factory.predictAddress(PK, 7); await (await factory.createAccount(PK, 7)).wait(); const acct = await ethers.getContractAt("AerePQCAccount", predicted); expect(await acct.entryPoint()).to.equal(entryPointEOA.address); expect(await acct.falcon()).to.equal(await mock.getAddress()); expect(await acct.factory()).to.equal(await factory.getAddress()); }); }); describe("AerePQCAccount", function () { async function accountFixture() { const f = await deployFixture(); const predicted = await f.factory.predictAddress(PK, 0); await (await f.factory.createAccount(PK, 0)).wait(); const account = await ethers.getContractAt("AerePQCAccount", predicted); // Fund the account so it can move value in execute tests. await (await f.deployer.sendTransaction({ to: predicted, value: ethers.parseEther("1") })).wait(); return { ...f, account, predicted }; } it("initialize cannot be called twice, and only by the factory", async function () { const { account, entryPointEOA, mock, deployer } = await accountFixture(); await expect( account.connect(deployer).initialize(entryPointEOA.address, await mock.getAddress(), PK) ).to.be.revertedWithCustomError(account, "Unauthorized"); }); it("EIP-1271 returns the magic value for a valid Falcon signature", async function () { const { account, mock } = await accountFixture(); const hash = ethers.id("post-quantum login to dapp.example"); await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait(); expect(await account.isValidSignature(hash, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e"); }); it("EIP-1271 returns failure (0x00000000) for a tampered signature", async function () { const { account, mock } = await accountFixture(); const hash = ethers.id("post-quantum login to dapp.example"); await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait(); // Flip one byte of the compressed signature -> the verifier rejects it. const tampered = "0x" + "22".repeat(79) + "23"; expect(await account.isValidSignature(hash, envelope(NONCE, tampered))).to.equal("0x00000000"); }); it("validateUserOp returns 0 for a valid signature (only from the EntryPoint)", async function () { const { account, entryPointEOA, mock } = await accountFixture(); const userOpHash = ethers.id("userop-1"); await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, true)).wait(); const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)]; const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0); expect(val).to.equal(0n); }); it("validateUserOp returns 1 (SIG_VALIDATION_FAILED) for a bad signature", async function () { const { account, entryPointEOA, mock } = await accountFixture(); const userOpHash = ethers.id("userop-2"); await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, false)).wait(); const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)]; const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0); expect(val).to.equal(1n); }); it("validateUserOp reverts if not called by the EntryPoint", async function () { const { account, other } = await accountFixture(); const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)]; await expect( account.connect(other).validateUserOp(op, ethers.id("x"), 0) ).to.be.revertedWithCustomError(account, "Unauthorized"); }); it("executeWithFalcon moves value when the Falcon signature is valid, and bumps the nonce", async function () { const { account, mock, other } = await accountFixture(); const target = other.address; const value = ethers.parseEther("0.25"); const data = "0x"; const n = await account.execNonce(); const challenge = await account.getExecuteChallenge(target, value, data, n); await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, true)).wait(); const before = await ethers.provider.getBalance(target); await (await account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data)).wait(); const after = await ethers.provider.getBalance(target); expect(after - before).to.equal(value); expect(await account.execNonce()).to.equal(n + 1n); }); it("executeWithFalcon reverts on an invalid Falcon signature", async function () { const { account, mock, other } = await accountFixture(); const target = other.address; const value = ethers.parseEther("0.1"); const n = await account.execNonce(); const challenge = await account.getExecuteChallenge(target, value, "0x", n); await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, false)).wait(); await expect( account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, "0x") ).to.be.revertedWithCustomError(account, "InvalidSignature"); }); // DOMAIN SEPARATION (adversarial-review HIGH, 2026-07-12): a Falcon signature // phished through the raw-hash EIP-1271 login surface must NOT authorize a spend. it("domain separation: an EIP-1271 login signature cannot be replayed as an executeWithFalcon spend", async function () { const { account, mock, other } = await accountFixture(); const target = other.address; const value = ethers.parseEther("0.5"); const data = "0x"; const n = await account.execNonce(); // Attacker computes the exact execute challenge (all inputs are public; execNonce is public). const challenge = await account.getExecuteChallenge(target, value, data, n); // The user is phished into signing `challenge` as a raw "login" hash: the mock // records a VALID Falcon signature for the RAW 32-byte message (the EIP-1271 surface). await (await mock.setResult(PK, msgOf(challenge), NONCE, COMPSIG, true)).wait(); // That login signature IS accepted by the raw-hash isValidSignature surface... expect(await account.isValidSignature(challenge, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e"); // ...but it must NOT authorize a spend: executeWithFalcon verifies over // (EXEC_DOMAIN || challenge), which the phished raw-hash signature does not cover. await expect( account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data) ).to.be.revertedWithCustomError(account, "InvalidSignature"); // No value moved, nonce unchanged. expect(await account.execNonce()).to.equal(n); }); it("executeFromEntryPoint is EntryPoint-gated", async function () { const { account, other } = await accountFixture(); await expect( account.connect(other).executeFromEntryPoint(other.address, 0, "0x") ).to.be.revertedWithCustomError(account, "Unauthorized"); }); it("executeFromEntryPoint dispatches the call when invoked by the EntryPoint", async function () { const { account, entryPointEOA, other } = await accountFixture(); const value = ethers.parseEther("0.3"); const before = await ethers.provider.getBalance(other.address); await (await account.connect(entryPointEOA).executeFromEntryPoint(other.address, value, "0x")).wait(); const after = await ethers.provider.getBalance(other.address); expect(after - before).to.equal(value); }); });