// AUDIT FIX VERIFICATION — AereStakingV2 — 2026-07-10
//
// Proves the five confirmed AereStaking (V1) bugs are FIXED in AereStakingV2.
// For each finding: V1 REPRODUCES the bug, V2 BLOCKS/CORRECTS it. Runs fully local
// against the REAL contract sources (same source that gets deployed to chain 2800).
//
//   F2  reward drain on top-up      — V2 settles before top-up (0 retroactive reward)
//   F3  locked validator self-stake — V2 deregisterValidator + claimUnbonded recovers it
//   F4  re-register bricks unbond   — V2 preserves totalDelegated + no validatorList dup
//   F5  APY ~5x underpay            — V2 timestamp accrual hits 8%; rewardRateBps settable
//   LOW commission silently kept    — V2 credits + pays validator commission
//
// AereStakingV2 constructor takes initialOwner; tests deploy with owner = signer[0]
// so owner-only slash / setRewardRateBps can be exercised.

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

const ONE = 10n ** 18n;
const BPS_DENOM = 10000n;
const YEAR = 31536000n;              // 365 days, V2 timestamp accrual base
const V1_BLOCKS_PER_YEAR = 315360000n; // V1's hardcoded (0.1s-block) constant

// V2 gross reward for `amount` over `secs` seconds at `rateBps` (timestamp accrual)
function grossV2(amount, secs, rateBps = 800n) {
  return (amount * rateBps * secs) / (BPS_DENOM * YEAR);
}

async function deployV1() {
  const S = await ethers.getContractFactory("AereStaking");
  const st = await S.deploy();
  await st.waitForDeployment();
  return st;
}
async function deployV2(ownerAddr) {
  const S = await ethers.getContractFactory("AereStakingV2");
  const st = await S.deploy(ownerAddr);
  await st.waitForDeployment();
  return st;
}
async function ts() {
  return BigInt((await ethers.provider.getBlock("latest")).timestamp);
}

describe("AereStakingV2 — audit fixes (V1 reproduces, V2 corrects)", function () {
  this.timeout(300000);

  // ───────────────────────── F2 ─────────────────────────
  describe("F2 top-up retroactive reward drain", function () {
    it("V1 REPRODUCES: top-up makes the whole balance earn over the full past window", async () => {
      const [v, d] = await ethers.getSigners();
      const st = await deployV1();
      await st.connect(v).registerValidator(0, { value: 500n * ONE });

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

      await ethers.provider.send("hardhat_mine", ["0x3E8"]); // 1000 blocks

      const Y = 900n * ONE;
      await st.connect(d).delegate(v.address, { value: Y }); // no settle in V1
      const del1 = await st.delegations(v.address, d.address);
      expect(del1.lastClaimBlock).to.equal(firstClaimBlock); // NOT reset (the bug)
      expect(del1.amount).to.equal(X + Y);

      const cur = BigInt(await ethers.provider.getBlockNumber());
      const elapsed = cur - firstClaimBlock;
      const buggy = ((X + Y) * 800n * elapsed) / (V1_BLOCKS_PER_YEAR * 10000n);
      const fairOnX = (X * 800n * elapsed) / (V1_BLOCKS_PER_YEAR * 10000n);
      const pAfter = await st.pendingRewards(v.address, d.address);
      expect(pAfter).to.equal(buggy);       // pays on X+Y over the whole window
      expect(pAfter).to.be.gt(fairOnX * 9n); // ~10x the fair value (Y=9X earns full retro)
    });

    it("V2 CORRECTS: top-up settles first, so the added stake earns 0 retroactive reward", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      await st.connect(v).registerValidator(0, { value: 500n * ONE });

      const X = 100n * ONE;
      await st.connect(d).delegate(v.address, { value: X });
      const L0 = (await st.delegations(v.address, d.address)).lastClaimTime;

      // Advance EXACTLY 1000s, then top up in that same block so the read (a view at
      // the top-up block) sees 0 elapsed since the settle.
      const T = 1000n;
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(L0 + T)]);
      const Y = 900n * ONE;
      await st.connect(d).delegate(v.address, { value: Y }); // settles X over 1000s, then adds Y

      const del1 = await st.delegations(v.address, d.address);
      expect(del1.lastClaimTime).to.equal(L0 + T);  // anchor advanced (the fix)
      expect(del1.amount).to.equal(X + Y);

      const expectedNet = grossV2(X, T);                 // only X earned, only for 1000s
      const buggyWouldBe = grossV2(X + Y, T);            // what V1 would have paid
      const pAfter = await st.pendingRewards(v.address, d.address);
      expect(pAfter).to.equal(expectedNet);              // exact: Y contributes nothing
      expect(pAfter * 9n).to.be.lt(buggyWouldBe * 2n);   // pAfter << buggy (Y did not earn retro)
      // rewardAccrued holds exactly the settled net for X.
      expect(del1.rewardAccrued).to.equal(expectedNet);
    });
  });

  // ───────────────────────── F3 ─────────────────────────
  describe("F3 validator self-stake recovery", function () {
    it("V1 REPRODUCES: no path returns self-stake; it is locked forever", async () => {
      const [v] = await ethers.getSigners();
      const st = await deployV1();
      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE);

      const names = st.interface.fragments.filter((f) => f.type === "function").map((f) => f.name);
      for (const n of ["deregisterValidator", "withdrawSelfStake", "exitValidator"]) {
        expect(names).to.not.include(n);
      }
      await expect(st.connect(v).unbond(v.address, 500n * ONE)).to.be.revertedWith(
        "AereStaking: insufficient delegation"
      );
      await expect(st.connect(v).claimUnbonded()).to.be.revertedWith("AereStaking: nothing to claim");
      expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE); // stuck
    });

    it("V2 CORRECTS: deregisterValidator + claimUnbonded returns the self-stake after the delay", async () => {
      const [owner, v] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      const addr = await st.getAddress();
      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      expect(await st.totalStaked()).to.equal(500n * ONE);

      await st.connect(v).deregisterValidator();
      expect((await st.validators(v.address)).selfStake).to.equal(0n);
      expect((await st.validators(v.address)).active).to.equal(false);
      expect(await st.totalStaked()).to.equal(0n);
      expect(await st.unbondQueueLength(v.address)).to.equal(1n);

      // Not matured yet.
      await expect(st.connect(v).claimUnbonded()).to.be.revertedWith("AereStakingV2: nothing to claim");

      // Mature the 7-day unbonding.
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]);
      await ethers.provider.send("evm_mine", []);

      const before = await ethers.provider.getBalance(v.address);
      const rc = await (await st.connect(v).claimUnbonded()).wait();
      const gas = rc.gasUsed * rc.gasPrice;
      const after = await ethers.provider.getBalance(v.address);
      expect(after + gas - before).to.equal(500n * ONE);         // full self-stake recovered
      expect(await ethers.provider.getBalance(addr)).to.equal(0n);
    });

    it("V2 CORRECTS: a slashed-out validator recovers only the POST-slash self-stake; slashed value stays in the pool (accounted, not orphaned)", async () => {
      const [owner, v] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      const addr = await st.getAddress();
      await st.connect(v).registerValidator(0, { value: 500n * ONE });

      await st.connect(owner).slash(v.address, "double-sign"); // 5% -> 475 left, deactivated
      const residual = (await st.validators(v.address)).selfStake;
      expect(residual).to.equal(475n * ONE);
      expect((await st.validators(v.address)).active).to.equal(false);

      // deregister works even though already deactivated.
      await st.connect(v).deregisterValidator();
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]);
      await ethers.provider.send("evm_mine", []);

      const before = await ethers.provider.getBalance(v.address);
      const rc = await (await st.connect(v).claimUnbonded()).wait();
      const gas = rc.gasUsed * rc.gasPrice;
      const after = await ethers.provider.getBalance(v.address);
      expect(after + gas - before).to.equal(475n * ONE);       // only post-slash amount back
      // The slashed 25 AERE remains in the pool (surplus), not orphaned to the validator.
      expect(await ethers.provider.getBalance(addr)).to.equal(25n * ONE);
      expect(await st.totalStaked()).to.equal(0n);
    });
  });

  // ───────────────────────── F4 ─────────────────────────
  describe("F4 re-registration after slash", function () {
    it("V1 REPRODUCES: re-register zeroes totalDelegated (unbond underflow) and duplicates validatorList", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const st = await deployV1();
      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);
      expect(await st.validatorCount()).to.equal(1n);

      await st.connect(owner).slash(v.address, "double-sign");
      expect((await st.validators(v.address)).active).to.equal(false);

      await st.connect(v).registerValidator(0, { value: 500n * ONE }); // overwrites struct
      expect((await st.validators(v.address)).totalDelegated).to.equal(0n); // BUG: zeroed
      expect(await st.validatorCount()).to.equal(2n);                       // BUG: duplicate

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

    it("V2 CORRECTS: re-register preserves totalDelegated + delegations, no duplicate; delegator can still unbond", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      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);
      expect(await st.validatorCount()).to.equal(1n);

      await st.connect(owner).slash(v.address, "double-sign");
      expect((await st.validators(v.address)).active).to.equal(false);

      await st.connect(v).registerValidator(0, { value: 500n * ONE }); // re-activate
      expect((await st.validators(v.address)).active).to.equal(true);
      expect((await st.validators(v.address)).totalDelegated).to.equal(D); // PRESERVED
      expect(await st.validatorCount()).to.equal(1n);                      // no duplicate
      // self-stake added to residual (475 + 500 = 975)
      expect((await st.validators(v.address)).selfStake).to.equal(975n * ONE);

      await st.connect(d).unbond(v.address, D); // no underflow
      expect((await st.validators(v.address)).totalDelegated).to.equal(0n);
      expect((await st.delegations(v.address, d.address)).amount).to.equal(0n);
    });
  });

  // ───────────────────────── F5 ─────────────────────────
  describe("F5 reward rate / block-time (APY 8%)", function () {
    it("V1 REPRODUCES: constant is 315,360,000 (0.1s blocks) -> realized ~1.6% at 0.5s (5x under)", async () => {
      const [v, d] = await ethers.getSigners();
      const st = await deployV1();
      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);

      await ethers.provider.send("hardhat_mine", ["0x" + (500000n).toString(16)]);
      const cur = BigInt(await ethers.provider.getBlockNumber());
      const elapsed = cur - del.lastClaimBlock;
      const pending = await st.pendingRewards(v.address, d.address);
      expect(pending).to.equal((stake * 800n * elapsed) / (V1_BLOCKS_PER_YEAR * 10000n));

      const BLOCKS_PER_YEAR_HALF_SEC = 63072000n; // 31,536,000 / 0.5
      const realizedBps = (800n * BLOCKS_PER_YEAR_HALF_SEC) / V1_BLOCKS_PER_YEAR;
      expect(realizedBps).to.equal(160n);   // 1.60% realized vs 800 (8%) intended
      expect(800n / realizedBps).to.equal(5n);
    });

    it("V2 CORRECTS: timestamp accrual -> 1000 AERE for 1 year accrues exactly 80 AERE (8%)", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      expect(await st.rewardRateBps()).to.equal(800n);
      await st.connect(v).registerValidator(0, { value: 500n * ONE });

      const stake = 1000n * ONE;
      await st.connect(d).delegate(v.address, { value: stake });
      const L0 = (await st.delegations(v.address, d.address)).lastClaimTime;

      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(L0 + YEAR)]);
      await ethers.provider.send("evm_mine", []);

      const pending = await st.pendingRewards(v.address, d.address);
      expect(pending).to.equal(80n * ONE);                 // 8% of 1000 AERE, exactly
      expect(pending).to.equal(grossV2(stake, YEAR));
    });

    it("V2: rewardRateBps is owner-settable (guards + non-owner reject), and changes the accrual", async () => {
      const [owner, v, d, stranger] = await ethers.getSigners();
      const st = await deployV2(owner.address);

      await expect(st.connect(stranger).setRewardRateBps(1600)).to.be.revertedWith(
        "Ownable: caller is not the owner"
      );
      await expect(st.connect(owner).setRewardRateBps(0)).to.be.revertedWith(
        "AereStakingV2: rate out of range"
      );
      await expect(st.connect(owner).setRewardRateBps(10001)).to.be.revertedWith(
        "AereStakingV2: rate out of range"
      );

      await st.connect(owner).setRewardRateBps(1600); // 16% APY
      expect(await st.rewardRateBps()).to.equal(1600n);

      await st.connect(v).registerValidator(0, { value: 500n * ONE });
      const stake = 1000n * ONE;
      await st.connect(d).delegate(v.address, { value: stake });
      const L0 = (await st.delegations(v.address, d.address)).lastClaimTime;
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(L0 + YEAR)]);
      await ethers.provider.send("evm_mine", []);
      expect(await st.pendingRewards(v.address, d.address)).to.equal(160n * ONE); // 16% of 1000
    });
  });

  // ───────────────────────── LOW: commission ─────────────────────────
  describe("LOW validator commission is credited (not silently kept)", function () {
    it("V1 REPRODUCES: commission is deducted from the delegator but credited nowhere (no validator claim)", async () => {
      const st = await deployV1();
      const names = st.interface.fragments.filter((f) => f.type === "function").map((f) => f.name);
      expect(names).to.not.include("claimCommission");
      // Validator struct has no commission-claimable field either.
      const [v, d] = await ethers.getSigners();
      await st.connect(v).registerValidator(1000, { value: 500n * ONE }); // 10% commission
      await st.connect(d).delegate(v.address, { value: 1000n * ONE });
      await ethers.provider.send("hardhat_mine", ["0x186A0"]); // 100000 blocks
      const gross = ((1000n * ONE) * 800n * (BigInt(await ethers.provider.getBlockNumber()) -
        (await st.delegations(v.address, d.address)).lastClaimBlock)) / (V1_BLOCKS_PER_YEAR * 10000n);
      const net = await st.pendingRewards(v.address, d.address);
      // delegator sees gross - 10%; the 10% commission exists but has no home in V1.
      expect(net).to.equal(gross - gross / 10n);
    });

    it("V2 CORRECTS: commission accrues to the validator on settle and is withdrawable via claimCommission", async () => {
      const [owner, v, d] = await ethers.getSigners();
      const st = await deployV2(owner.address);
      const addr = await st.getAddress();
      await st.connect(v).registerValidator(1000, { value: 500n * ONE }); // 10% commission
      await st.connect(d).delegate(v.address, { value: 1000n * ONE });
      const L0 = (await st.delegations(v.address, d.address)).lastClaimTime;

      // Seed the reward pool so claims can pay out.
      await owner.sendTransaction({ to: addr, value: 100n * ONE });

      // Lazy: before any settle, nothing is credited to the validator.
      expect((await st.validators(v.address)).commissionAccrued).to.equal(0n);

      // Delegator claims after 1 year -> settle splits gross 80 into 72 net + 8 commission.
      await ethers.provider.send("evm_setNextBlockTimestamp", [Number(L0 + YEAR)]);
      const dBefore = await ethers.provider.getBalance(d.address);
      const rc = await (await st.connect(d).claimRewards(v.address)).wait();
      const gas = rc.gasUsed * rc.gasPrice;
      const dAfter = await ethers.provider.getBalance(d.address);
      expect(dAfter + gas - dBefore).to.equal(72n * ONE);                 // 90% of 80
      expect((await st.validators(v.address)).commissionAccrued).to.equal(8n * ONE); // 10% credited

      // Validator withdraws the commission.
      const vBefore = await ethers.provider.getBalance(v.address);
      const rc2 = await (await st.connect(v).claimCommission()).wait();
      const gas2 = rc2.gasUsed * rc2.gasPrice;
      const vAfter = await ethers.provider.getBalance(v.address);
      expect(vAfter + gas2 - vBefore).to.equal(8n * ONE);                 // commission paid out
      expect((await st.validators(v.address)).commissionAccrued).to.equal(0n);
    });
  });
});
