// AUDIT FIX VERIFICATION — 2026-07-10 // Proves the two CONFIRMED bugs (findings 9 & 10 in audit-verify-2026-07.test.js) // are FIXED in the V2 redeploys, by running the ORIGINAL exploit against BOTH: // * V1 -> the exploit STILL reproduces (regression anchor) // * V2 -> the exploit is BLOCKED (the attack call reverts) while legit flows work // // Runs fully local against the REAL V1 + V2 sources (same bytecode as deployed). const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE = 10n ** 18n; describe("AUDIT FIX V2 — findings 9 & 10", function () { this.timeout(300000); // ───────────────────────────────────────────────────────────── // Finding 9 — AereDACommittee removeMember -> threshold 0 -> forge availability // ───────────────────────────────────────────────────────────── describe("F9 AereDACommittee fail-open (removeMember drives threshold to 0)", function () { async function committeeSigs(dac, members, rollupId, epoch, dc) { // members must already be sorted ascending by address const net = await ethers.provider.getNetwork(); const inner = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "uint256", "bytes32"], ["AereDACommittee:attest", net.chainId, await dac.getAddress(), rollupId, epoch, dc] ) ); const sigs = []; for (const m of members) sigs.push(await m.signMessage(ethers.getBytes(inner))); return sigs; } it("V1 CONFIRMED: admin removes all members; attest([]) forges availability with 0 sigs", async () => { const [admin] = await ethers.getSigners(); const members = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()] .sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1)); const DAC = await ethers.getContractFactory("AereDACommittee"); const dac = await DAC.deploy(admin.address, members.map(m => m.address), 2); await dac.waitForDeployment(); for (const m of members) await dac.connect(admin).removeMember(m.address); expect(await dac.memberCount()).to.equal(0n); expect(await dac.threshold()).to.equal(0n); // clamped to 0 const rollupId = ethers.id("rollup:forge"); await dac.attest(rollupId, 42n, ethers.id("forged"), []); // zero sigs accepted expect(await dac.isAvailable(rollupId, 42n)).to.equal(true); const [available, , , sigs] = await dac.getAttestation(rollupId, 42n); expect(available).to.equal(true); expect(sigs).to.equal(0n); // forged with zero committee signatures }); it("V2 BLOCKED: threshold can never reach 0; removeMember reverts; attest([]) reverts; legit M-of-N still works", async () => { const [admin] = await ethers.getSigners(); const members = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()] .sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1)); const DAC = await ethers.getContractFactory("AereDACommitteeV2"); const dac = await DAC.deploy(admin.address, members.map(m => m.address), 2); await dac.waitForDeployment(); // ── the original attack: try to remove all members ── // memberCount 3 -> 2 is fine (>= threshold 2) await dac.connect(admin).removeMember(members[2].address); expect(await dac.memberCount()).to.equal(2n); expect(await dac.threshold()).to.equal(2n); // NOT clamped // memberCount 2 -> 1 would drop below threshold 2 -> REVERT await expect(dac.connect(admin).removeMember(members[1].address)) .to.be.revertedWithCustomError(dac, "WouldBreakThreshold").withArgs(1n, 2n); // admin must lower threshold explicitly first (still can't be 0) await expect(dac.connect(admin).setThreshold(0)).to.be.revertedWithCustomError(dac, "BadThreshold"); await dac.connect(admin).setThreshold(1); await dac.connect(admin).removeMember(members[1].address); // now ok, memberCount 1 expect(await dac.memberCount()).to.equal(1n); expect(await dac.threshold()).to.equal(1n); // the LAST member can never be removed -> threshold can never reach 0 await expect(dac.connect(admin).removeMember(members[0].address)) .to.be.revertedWithCustomError(dac, "WouldBreakThreshold").withArgs(0n, 1n); expect(await dac.threshold()).to.equal(1n); // even so, attest([]) is rejected outright (belt-and-suspenders) await expect(dac.attest(ethers.id("r"), 1n, ethers.id("d"), [])) .to.be.revertedWithCustomError(dac, "EmptySignatureSet"); expect(await dac.isAvailable(ethers.id("r"), 1n)).to.equal(false); // ── legit flows on a fresh 2-of-3 committee ── const m2 = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()] .sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1)); const dac2 = await DAC.deploy(admin.address, m2.map(m => m.address), 2); await dac2.waitForDeployment(); const rollupId = ethers.id("rollup:legit"); const dc = ethers.id("batch-commitment"); const allSigs = await committeeSigs(dac2, m2, rollupId, 7n, dc); // 1 sig reverts ThresholdNotMet(1,2) await expect(dac2.attest(rollupId, 7n, dc, [allSigs[0]])) .to.be.revertedWithCustomError(dac2, "ThresholdNotMet").withArgs(1n, 2n); // 2 distinct ascending sigs -> recorded await dac2.attest(rollupId, 7n, dc, [allSigs[0], allSigs[1]]); expect(await dac2.isAvailable(rollupId, 7n)).to.equal(true); const [avail, , , cnt] = await dac2.getAttestation(rollupId, 7n); expect(avail).to.equal(true); expect(cnt).to.equal(2n); // empty sigs still rejected on this live committee too await expect(dac2.attest(ethers.id("rollup:x"), 1n, dc, [])) .to.be.revertedWithCustomError(dac2, "EmptySignatureSet"); }); }); // ───────────────────────────────────────────────────────────── // Finding 10 — AereSlashingInsurance matured claim griefed via cancel // ───────────────────────────────────────────────────────────── describe("F10 AereSlashingInsurance griefing (cancel a matured payout)", function () { it("V1 CONFIRMED: griefer cancels a matured claim, permanently blocking the payout", async () => { const [foundation, griefer, delegatee] = await ethers.getSigners(); const SI = await ethers.getContractFactory("AereSlashingInsurance"); const si = await SI.deploy(foundation.address, 5000, 10n ** 24n, 0, 100); await si.waitForDeployment(); await si.connect(foundation).donate({ value: 100n * ONE }); await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence")); await ethers.provider.send("evm_increaseTime", [101]); await ethers.provider.send("evm_mine", []); // matured — but cancel has no timelock guard, so griefer deletes it await si.connect(griefer).cancelCoverage(0, "griefing a valid claim"); await expect(si.connect(delegatee).executeCoverage(0)).to.be.revertedWithCustomError(si, "NoClaim"); }); it("V2 BLOCKED: matured claim can NOT be cancelled (grief reverts) and executes; un-matured claim still cancellable", async () => { const [foundation, griefer, delegatee] = await ethers.getSigners(); const SI = await ethers.getContractFactory("AereSlashingInsuranceV2"); const si = await SI.deploy(foundation.address, 5000, 10n ** 24n, 0, 100); await si.waitForDeployment(); await si.connect(foundation).donate({ value: 100n * ONE }); // ── claim 0: matured -> grief blocked, executes ── await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence")); const claim = await si.claims(0); const payout = claim.payout; // 20 * 50% = 10 AERE expect(payout).to.equal(10n * ONE); await ethers.provider.send("evm_increaseTime", [101]); await ethers.provider.send("evm_mine", []); // griefer front-run now REVERTS (ClaimMatured) await expect(si.connect(griefer).cancelCoverage(0, "grief")) .to.be.revertedWithCustomError(si, "ClaimMatured"); // payout executes to the frozen delegator const before = await ethers.provider.getBalance(delegatee.address); await si.connect(griefer).executeCoverage(0); // anyone can trigger; recipient frozen const after = await ethers.provider.getBalance(delegatee.address); expect(after - before).to.equal(payout); // ── claim 1: NOT matured -> community veto still works (permissionless cancel) ── await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence2")); // do NOT advance time past readyAt await si.connect(griefer).cancelCoverage(1, "legit community veto during window"); await expect(si.connect(delegatee).executeCoverage(1)).to.be.revertedWithCustomError(si, "NoClaim"); }); }); });