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

// ---------------------------------------------------------------------------
// FINDING 1 — governance quorum inflation.
//
// The generic AereGovernor uses GovernorVotesQuorumFraction, so quorum for a
// proposal is token.getPastTotalSupply(snapshot) * pct / 100 with NO floor, NO
// cap and NO time-averaging. AereGovLock.lock() permissionlessly mints govAERE
// 1:1 from the abundant native token, so an attacker can lock a large amount
// just before a proposal's snapshot, stay SILENT (never vote), and permanently
// fix an unreachable quorum. Honest holders voting 100% For then cannot reach
// quorum and the proposal is Defeated. The attacker cannot force-pass anything
// (principal is returned after the lock), but can veto every proposal.
//
// This suite reproduces the EXACT attack against the current AereGovernor and
// proves the corrected governor (AereGovernorV2, with an absolute quorum FLOOR
// and CAP) defeats it, while a genuinely low-turnout proposal still correctly
// fails quorum (no over-correction).
// ---------------------------------------------------------------------------

async function mine(n = 1) {
  for (let i = 0; i < n; i++) await network.provider.send("evm_mine");
}
async function now() {
  const b = await ethers.provider.getBlock("latest");
  return b.timestamp;
}
const DAY = 24 * 60 * 60;
const E = (n) => ethers.parseEther(String(n));

// Give a signer enough native AERE to fund a large lock (default test accounts
// only hold 10,000, far below the whale's stake). This mirrors chain 2800's
// abundant native supply that makes the inflation attack cheap.
async function fund(signer, amount) {
  const bal = amount + E(1000); // stake plus gas headroom
  await network.provider.send("hardhat_setBalance", [signer.address, "0x" + bal.toString(16)]);
}

// Proposal states (OZ Governor enum).
const STATE = { Pending: 0, Active: 1, Canceled: 2, Defeated: 3, Succeeded: 4, Queued: 5, Expired: 6, Executed: 7 };

// Small windows so a whale can lock inside the voting-delay window, right before
// the snapshot block, exactly as the attack describes.
const VOTING_DELAY = 4; // blocks
const VOTING_PERIOD = 12; // blocks
const QUORUM_PCT = 10; // 10% of past total supply

// Absolute band for the corrected governor.
const QUORUM_FLOOR = E(100000); // 100k govAERE minimum quorum
const QUORUM_CAP = E(500000); // 500k govAERE secondary ceiling
// Denominator lookback (blocks): must cover the voting-delay window plus a margin
// so a lock placed after (or front-running) the proposal cannot inflate quorum.
const QUORUM_LOOKBACK = VOTING_DELAY + 2; // 6 blocks

// Attack sizing.
const HONEST = E(1000000); // honest holders lock 1,000,000 and vote 100% For
const WHALE = E(100000000); // whale silently locks 100,000,000 just before snapshot

async function deployGovLock(minLockDuration = 0) {
  const F = await ethers.getContractFactory("AereGovLock");
  const c = await F.deploy(minLockDuration);
  await c.waitForDeployment();
  return c;
}

// Deploy a governor (by contract name) wired to the given vote source, plus a
// throwaway timelock (0 delay) and an owned target.
async function deployGovStack(governorName, gl, deployer, extraArgs = []) {
  const TL = await ethers.getContractFactory("TimelockController");
  const timelock = await TL.deploy(0, [], [], deployer.address);
  await timelock.waitForDeployment();

  const Gov = await ethers.getContractFactory(governorName);
  const gov = await Gov.deploy(
    await gl.getAddress(),
    await timelock.getAddress(),
    VOTING_DELAY,
    VOTING_PERIOD,
    0, // proposalThreshold: 0 so an honest holder can always propose
    QUORUM_PCT,
    ...extraArgs
  );
  await gov.waitForDeployment();

  const PROPOSER = await timelock.PROPOSER_ROLE();
  const EXECUTOR = await timelock.EXECUTOR_ROLE();
  const CANCELLER = await timelock.CANCELLER_ROLE();
  await (await timelock.grantRole(PROPOSER, await gov.getAddress())).wait();
  await (await timelock.grantRole(EXECUTOR, await gov.getAddress())).wait();
  await (await timelock.grantRole(CANCELLER, await gov.getAddress())).wait();

  const Target = await ethers.getContractFactory("GovSelfTestTarget");
  const target = await Target.deploy();
  await target.waitForDeployment();
  await (await target.transferOwnership(await timelock.getAddress())).wait();

  return { gov, timelock, target };
}

// Drive one proposal. The honest signer (already locked + delegated) proposes.
// If whaleAmount > 0, the whale locks that amount SILENTLY inside the voting-
// delay window, before the snapshot block. The honest signer then votes For.
// Returns the final state and the quorum that applied at the snapshot.
async function runProposal(gl, gov, target, honest, whale, whaleAmount) {
  const targets = [await target.getAddress()];
  const values = [0n];
  const calldatas = [target.interface.encodeFunctionData("setValue", [42])];
  const description = `proposal ${Math.random()}`;
  const descHash = ethers.id(description);
  const proposalId = await gov.hashProposal(targets, values, calldatas, descHash);

  await (await gov.connect(honest).propose(targets, values, calldatas, description)).wait();
  const snapshot = await gov.proposalSnapshot(proposalId);
  const deadline = await gov.proposalDeadline(proposalId);

  // Whale inflates the total supply SILENTLY, before the snapshot block.
  if (whaleAmount > 0n) {
    const cur = await ethers.provider.getBlockNumber();
    expect(cur, "whale must lock before snapshot").to.be.lessThan(Number(snapshot));
    await (await gl.connect(whale).lock(whaleAmount, (await now()) + 30 * DAY, { value: whaleAmount })).wait();
    await (await gl.connect(whale).delegate(whale.address)).wait(); // real checkpointed supply, but never votes
  }

  const cur = await ethers.provider.getBlockNumber();
  await mine(Number(snapshot) - cur + 1);
  expect(Number(await gov.state(proposalId))).to.equal(STATE.Active);

  const quorumAtSnapshot = await gov.quorum(snapshot);
  await (await gov.connect(honest).castVote(proposalId, 1)).wait(); // 100% For

  const cur2 = await ethers.provider.getBlockNumber();
  await mine(Number(deadline) - cur2 + 1);

  return { state: Number(await gov.state(proposalId)), quorumAtSnapshot, proposalId };
}

describe("FINDING 1 — governance quorum inflation", function () {
  // -------------------------------------------------------------------------
  // 1. Reproduce the attack against the CURRENT AereGovernor: a silent whale
  //    lock before the snapshot fixes an unreachable quorum, so a unanimous
  //    honest proposal is Defeated.
  // -------------------------------------------------------------------------
  it("current AereGovernor: silent whale lock before snapshot blocks a unanimous honest proposal", async function () {
    const [deployer, honest, whale] = await ethers.getSigners();
    const gl = await deployGovLock(0);
    await fund(honest, HONEST); await fund(whale, WHALE);

    // Honest holder locks and self-delegates BEFORE proposing.
    await (await gl.connect(honest).lock(HONEST, (await now()) + 30 * DAY, { value: HONEST })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    const { gov, target } = await deployGovStack("AereGovernor", gl, deployer);

    const { state, quorumAtSnapshot } = await runProposal(gl, gov, target, honest, whale, WHALE);

    // The whale inflated quorum to 10% of (HONEST + WHALE); honest For == HONEST
    // is far below it, so the proposal is Defeated even at 100% honest turnout.
    expect(quorumAtSnapshot).to.equal(((HONEST + WHALE) * BigInt(QUORUM_PCT)) / 100n);
    expect(quorumAtSnapshot).to.be.greaterThan(HONEST);
    expect(state).to.equal(STATE.Defeated); // attack succeeds: honest proposal vetoed
  });

  // -------------------------------------------------------------------------
  // 2. WITH THE FIX (AereGovernorV2): the SAME attack fails. The quorum
  //    denominator is read at a pre-proposal timepoint, so the whale's reactive
  //    lock is not counted at all. Quorum stays a fraction of the honest
  //    pre-proposal supply, below honest turnout, and the proposal Succeeds.
  // -------------------------------------------------------------------------
  it("AereGovernorV2: pre-proposal denominator defeats the quorum-inflation attack, honest proposal succeeds", async function () {
    const [deployer, honest, whale] = await ethers.getSigners();
    const gl = await deployGovLock(0);
    await fund(honest, HONEST); await fund(whale, WHALE);

    await (await gl.connect(honest).lock(HONEST, (await now()) + 30 * DAY, { value: HONEST })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    const { gov, target } = await deployGovStack("AereGovernorV2", gl, deployer, [QUORUM_FLOOR, QUORUM_CAP, QUORUM_LOOKBACK]);

    const { state, quorumAtSnapshot } = await runProposal(gl, gov, target, honest, whale, WHALE);

    // The whale's 100,000,000 lock lands after proposal creation, so it is outside
    // the measured (pre-proposal) supply of 1,000,000. Quorum is 10% of that
    // 1,000,000 == 100,000 (which coincides with the floor here), far below honest
    // For (1,000,000). Quorum reached -> Succeeded.
    expect(quorumAtSnapshot).to.equal((HONEST * BigInt(QUORUM_PCT)) / 100n); // 100,000
    expect(quorumAtSnapshot).to.be.lessThan(HONEST);
    expect(state).to.equal(STATE.Succeeded); // attack closed
  });

  // -------------------------------------------------------------------------
  // 3. WITH THE FIX: no over-correction. A genuinely low-turnout proposal (For
  //    below the absolute floor) still correctly fails quorum.
  // -------------------------------------------------------------------------
  it("AereGovernorV2: floor still fails a genuinely low-turnout proposal", async function () {
    const [deployer, honest, whale] = await ethers.getSigners();
    const gl = await deployGovLock(0);

    // Total locked supply is tiny (below the floor), and it is 100% For, yet the
    // absolute amount is still below the quorum floor, so it must fail.
    const TINY = E(60000); // < QUORUM_FLOOR (100k)
    await fund(honest, TINY);
    await (await gl.connect(honest).lock(TINY, (await now()) + 30 * DAY, { value: TINY })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    const { gov, target } = await deployGovStack("AereGovernorV2", gl, deployer, [QUORUM_FLOOR, QUORUM_CAP, QUORUM_LOOKBACK]);

    const { state, quorumAtSnapshot } = await runProposal(gl, gov, target, honest, whale, 0n /* no whale */);

    // Fractional (10% of 60k = 6k) is below the floor, so quorum == floor (100k),
    // and honest For (60k) is below it. Correctly Defeated: the floor is a real bar.
    expect(quorumAtSnapshot).to.equal(QUORUM_FLOOR);
    expect(state).to.equal(STATE.Defeated);
  });

  // -------------------------------------------------------------------------
  // 3b. RESIDUAL ATTACK (iteration-1 gap). The cap-only defense only works if the
  //     cap is set at or below expected honest turnout. With a LOOSE cap (the real
  //     deploy stages CAP = 50,000,000, far above realistic turnout) a silent whale
  //     that locks enough govAERE AFTER the proposal is created can still push the
  //     fractional quorum above the cap while the cap itself sits above honest For,
  //     re-fixing an unreachable bar. A robust governor must defeat this WITHOUT
  //     depending on a perfectly chosen cap.
  //
  //     Setup: floor 100k, LOOSE cap 50,000,000. Honest locks 1,000,000 and votes
  //     100% For. Whale silently locks 1,000,000,000 inside the voting-delay window.
  //       - fractional at snapshot = 10% of ~1.001B ≈ 100,100,000, clamped to the
  //         50,000,000 cap. 50,000,000 is still >> honest For (1,000,000), so a
  //         cap-only governor Defeats the unanimous honest proposal (attack works).
  //       - a robust governor measures the quorum denominator at a pre-proposal
  //         timepoint the late lock cannot inflate, so quorum stays 10% of the
  //         honest 1,000,000 = 100,000 (== floor here), below honest For, and the
  //         proposal Succeeds.
  // -------------------------------------------------------------------------
  it("AereGovernorV2: robust denominator defeats a reactive whale lock even with a LOOSE cap", async function () {
    const [deployer, honest, whale] = await ethers.getSigners();
    const gl = await deployGovLock(0);
    const BIG_WHALE = E(1000000000); // 1,000,000,000 locked silently after creation
    const LOOSE_CAP = E(50000000); // 50,000,000: the staged real-deploy cap, far above turnout
    await fund(honest, HONEST); await fund(whale, BIG_WHALE);

    await (await gl.connect(honest).lock(HONEST, (await now()) + 30 * DAY, { value: HONEST })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    const { gov, target } = await deployGovStack("AereGovernorV2", gl, deployer, [QUORUM_FLOOR, LOOSE_CAP, QUORUM_LOOKBACK]);

    const { state, quorumAtSnapshot } = await runProposal(gl, gov, target, honest, whale, BIG_WHALE);

    // A robust governor keeps quorum at 10% of the PRE-proposal honest supply
    // (100,000 == floor), which is below honest For, so the proposal Succeeds.
    // A cap-only governor would clamp to LOOSE_CAP (50,000,000) > honest For and Defeat it.
    expect(quorumAtSnapshot).to.equal(QUORUM_FLOOR); // 10% of 1,000,000 == floor 100,000
    expect(quorumAtSnapshot).to.be.lessThan(HONEST);
    expect(state).to.equal(STATE.Succeeded); // residual gap closed
  });

  // -------------------------------------------------------------------------
  // 4. WITH THE FIX: a healthy proposal inside the band still passes and
  //    executes end to end through the timelock (fix does not break normal use).
  // -------------------------------------------------------------------------
  it("AereGovernorV2: healthy proposal still passes and executes through the timelock", async function () {
    const [deployer, honest] = await ethers.getSigners();
    const gl = await deployGovLock(0);

    // 3,000,000 locked -> fractional 300k, inside [100k, 500k] -> quorum 300k.
    const LOCKED = E(3000000);
    await fund(honest, LOCKED);
    await (await gl.connect(honest).lock(LOCKED, (await now()) + 30 * DAY, { value: LOCKED })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    const { gov, target } = await deployGovStack("AereGovernorV2", gl, deployer, [QUORUM_FLOOR, QUORUM_CAP, QUORUM_LOOKBACK]);

    const targets = [await target.getAddress()];
    const values = [0n];
    const calldatas = [target.interface.encodeFunctionData("setValue", [42])];
    const description = "healthy setValue(42)";
    const descHash = ethers.id(description);
    const proposalId = await gov.hashProposal(targets, values, calldatas, descHash);

    await (await gov.connect(honest).propose(targets, values, calldatas, description)).wait();
    const snapshot = await gov.proposalSnapshot(proposalId);
    const deadline = await gov.proposalDeadline(proposalId);

    let cur = await ethers.provider.getBlockNumber();
    await mine(Number(snapshot) - cur + 1);

    // Query quorum only after the snapshot block exists (getPastTotalSupply
    // reverts on a future lookup). 300k is inside [100k, 500k].
    expect(await gov.quorum(snapshot)).to.equal((LOCKED * BigInt(QUORUM_PCT)) / 100n);

    await (await gov.connect(honest).castVote(proposalId, 1)).wait();
    cur = await ethers.provider.getBlockNumber();
    await mine(Number(deadline) - cur + 1);
    expect(Number(await gov.state(proposalId))).to.equal(STATE.Succeeded);

    await (await gov.queue(targets, values, calldatas, descHash)).wait();
    await mine(1);
    await (await gov.execute(targets, values, calldatas, descHash)).wait();
    expect(Number(await gov.state(proposalId))).to.equal(STATE.Executed);
    expect(await target.value()).to.equal(42n);
  });

  // -------------------------------------------------------------------------
  // 5. EARLY-PROPOSAL CLAMP: when the configured lookback reaches before genesis
  //    (timepoint <= lookback), the denominator point is clamped to 0 so the
  //    lookup never underflows or reverts, and the floor governs. quorum() must
  //    still return exactly the floor (never a silent zero) and must not revert.
  // -------------------------------------------------------------------------
  it("AereGovernorV2: an absurdly large lookback clamps to block 0 and returns the floor without reverting", async function () {
    const [deployer, honest] = await ethers.getSigners();
    const gl = await deployGovLock(0);

    const LOCKED = E(3000000); // 10% == 300k, inside the band, but the clamp forces floor
    await fund(honest, LOCKED);
    await (await gl.connect(honest).lock(LOCKED, (await now()) + 30 * DAY, { value: LOCKED })).wait();
    await (await gl.connect(honest).delegate(honest.address)).wait();
    await mine(2);

    // Lookback far larger than the current block height forces lookbackPoint -> 0,
    // where getPastTotalSupply reads the genesis supply (0). Fractional is then 0,
    // so the floor must govern.
    const HUGE_LOOKBACK = 1000000000; // >> current block number
    const { gov, target } = await deployGovStack(
      "AereGovernorV2", gl, deployer, [QUORUM_FLOOR, QUORUM_CAP, HUGE_LOOKBACK]
    );

    const targets = [await target.getAddress()];
    const values = [0n];
    const calldatas = [target.interface.encodeFunctionData("setValue", [42])];
    const description = "early-proposal clamp";
    const descHash = ethers.id(description);
    const proposalId = await gov.hashProposal(targets, values, calldatas, descHash);

    await (await gov.connect(honest).propose(targets, values, calldatas, description)).wait();
    const snapshot = await gov.proposalSnapshot(proposalId);
    const cur = await ethers.provider.getBlockNumber();
    await mine(Number(snapshot) - cur + 1);

    // Does not revert; supply-at-0 is 0, so quorum falls back to the floor.
    expect(await gov.quorum(snapshot)).to.equal(QUORUM_FLOOR);
  });
});
