// 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");
    });
  });
});
