const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("AereCompliancePool", () => { let foundation, casp, alice, bob, relayer; let token, verifier, pool; const DENOM = ethers.parseUnits("1000", 18); beforeEach(async () => { [foundation, casp, alice, bob, relayer] = await ethers.getSigners(); const T = await ethers.getContractFactory("MockERC20Lending"); token = await T.deploy("USDC.e", "USDCe", 18); await token.waitForDeployment(); const V = await ethers.getContractFactory("MockPrivacyPoolVerifier"); verifier = await V.deploy(); await verifier.waitForDeployment(); const P = await ethers.getContractFactory("AereCompliancePool"); pool = await P.deploy( await token.getAddress(), DENOM, foundation.address, await verifier.getAddress() ); await pool.waitForDeployment(); await token.mint(alice.address, DENOM * 10n); await token.connect(alice).approve(await pool.getAddress(), ethers.MaxUint256); }); function rh(s) { return ethers.keccak256(ethers.toUtf8Bytes(s)); } async function depositOnce(commitTag = "commit-1") { await pool.connect(foundation).setComplianceProvider(casp.address, true); const commit = rh(commitTag); const trh = rh("travel-rule"); await pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr"); return await pool.latestDepositRoot(); } async function publishAssoc(tag = "assoc") { const ar = rh(tag); await pool.connect(foundation).proposeAssociationRoot(ar); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await pool.publishAssociationRoot(ar); return ar; } it("only Foundation registers compliance providers", async () => { await expect( pool.connect(alice).setComplianceProvider(casp.address, true) ).to.be.revertedWithCustomError(pool, "NotFoundation"); await pool.connect(foundation).setComplianceProvider(casp.address, true); expect(await pool.isComplianceProvider(casp.address)).to.equal(true); expect((await pool.complianceProviders()).length).to.equal(1); }); it("deposit pulls DENOM + emits Deposited + advances on-chain Merkle root (audit fix HIGH #28)", async () => { const commit = rh("commit-1"); const trh = rh("travel-rule-hash-1"); await expect( pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr") ).to.be.revertedWithCustomError(pool, "NotAComplianceProvider"); await pool.connect(foundation).setComplianceProvider(casp.address, true); const balBefore = await token.balanceOf(await pool.getAddress()); const rootBefore = await pool.latestDepositRoot(); await pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr"); expect(await token.balanceOf(await pool.getAddress())).to.equal(balBefore + DENOM); expect(await pool.depositCount()).to.equal(1n); const rootAfter = await pool.latestDepositRoot(); expect(rootAfter).to.not.equal(rootBefore); expect(await pool.isKnownRoot(rootAfter)).to.equal(true); }); it("association root 24h challenge window (audit fix MED #28)", async () => { const a1 = rh("assoc-root-1"); await pool.connect(foundation).proposeAssociationRoot(a1); await expect(pool.publishAssociationRoot(a1)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot"); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(pool.publishAssociationRoot(a1)) .to.emit(pool, "AssociationRootPublished").withArgs(a1); expect(await pool.isAssociationRootValid(a1)).to.equal(true); }); it("association root challenge blocks finalisation", async () => { const a1 = rh("assoc-root-challenged"); await pool.connect(foundation).proposeAssociationRoot(a1); await pool.connect(alice).challengeAssociationRoot(a1, "this would let me bypass OFAC"); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(pool.publishAssociationRoot(a1)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot"); await pool.connect(foundation).dismissAssociationRootChallenge(a1); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await pool.publishAssociationRoot(a1); expect(await pool.isAssociationRootValid(a1)).to.equal(true); }); it("withdraw happy path: valid proof, known on-chain deposit root, fresh nullifier", async () => { const dr = await depositOnce("c1"); const ar = await publishAssoc(); const nh = rh("nullifier-1"); const balBefore = await token.balanceOf(bob.address); const relayerFee = ethers.parseUnits("5", 18); await expect( pool.connect(relayer).withdraw("0x1234", dr, ar, nh, bob.address, relayer.address, relayerFee, 0) ).to.emit(pool, "Withdrawn"); expect(await token.balanceOf(bob.address)).to.equal(balBefore + DENOM - relayerFee); expect(await token.balanceOf(relayer.address)).to.equal(relayerFee); expect(await pool.spentNullifiers(nh)).to.equal(true); }); it("withdraw double-spend rejected (same nullifier)", async () => { const dr = await depositOnce("c2"); const ar = await publishAssoc("ar2"); const nh = rh("nullifier-2"); await pool.connect(relayer).withdraw("0x", dr, ar, nh, bob.address, relayer.address, 0, 0); await expect( pool.connect(relayer).withdraw("0x", dr, ar, nh, bob.address, relayer.address, 0, 0) ).to.be.revertedWithCustomError(pool, "AlreadySpent"); }); it("withdraw unknown association root rejected", async () => { const dr = await depositOnce("c3"); await expect( pool.connect(relayer).withdraw("0x", dr, rh("never-published"), rh("n"), bob.address, relayer.address, 0, 0) ).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot"); }); it("withdraw unknown deposit root rejected (audit fix HIGH #28: only on-chain roots accepted)", async () => { const ar = await publishAssoc("ar4"); // depositRoot fabricated; not in on-chain ring buffer → reverts. await expect( pool.connect(relayer).withdraw("0x", rh("fake-dr"), ar, rh("n"), bob.address, relayer.address, 0, 0) ).to.be.revertedWithCustomError(pool, "UnknownDepositRoot"); }); it("withdraw invalid proof rejected by verifier", async () => { const dr = await depositOnce("c5"); const ar = await publishAssoc("ar5"); await verifier.setAccept(false); await expect( pool.connect(relayer).withdraw("0x", dr, ar, rh("n"), bob.address, relayer.address, 0, 0) ).to.be.revertedWithCustomError(pool, "InvalidProof"); }); it("fee + refund > DENOMINATION rejected", async () => { const dr = await depositOnce("c6"); const ar = await publishAssoc("ar6"); await expect( pool.connect(relayer).withdraw("0x", dr, ar, rh("n"), bob.address, relayer.address, DENOM, 1n) ).to.be.revertedWithCustomError(pool, "FeeTooLarge"); }); it("older on-chain root remains valid (root history)", async () => { await pool.connect(foundation).setComplianceProvider(casp.address, true); await pool.connect(alice).deposit(rh("a1"), rh("t"), casp.address, ""); const r1 = await pool.latestDepositRoot(); await pool.connect(alice).deposit(rh("a2"), rh("t"), casp.address, ""); const r2 = await pool.latestDepositRoot(); expect(r1).to.not.equal(r2); expect(await pool.isKnownRoot(r1)).to.equal(true); expect(await pool.isKnownRoot(r2)).to.equal(true); const ar = await publishAssoc("ar7"); // Withdraw against the OLDER root still works. await pool.connect(relayer).withdraw("0x", r1, ar, rh("n"), bob.address, relayer.address, 0, 0); }); it("duplicate commitment rejected", async () => { await pool.connect(foundation).setComplianceProvider(casp.address, true); const c = rh("same"); await pool.connect(alice).deposit(c, rh("t"), casp.address, ""); await expect( pool.connect(alice).deposit(c, rh("t"), casp.address, "") ).to.be.revertedWithCustomError(pool, "CommitmentReused"); }); });