// =============================================================================
// SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for RWATransferAdapter
// (contracts/lending/RWATransferAdapter.sol) — the permissionless redeem of a
// registered permissioned RWA (BUIDL.e / USDY.e / OUSG.e) to PAYOUT_TOKEN
// (USDC.e) at oracle price minus an immutable per-asset haircut, plus the
// Foundation-only sweepRWA path.
//
// STATUS: the adapter is NOT yet live (sdk-js/src/addresses.ts lists it under
// "PENDING ... RWATransferAdapter — needs Foundation NAV aggregators"), so it is
// stood up locally against the simple settable MockPriceOracle (which implements
// the exact IPriceOracle.getPrice(address)->1e18 interface the adapter calls) and
// MockERC20Lending (arbitrary-decimals ERC-20). No live node is touched.
//
// TOOLCHAIN: hardhat-based randomized invariant fuzzing (forge is NOT installed;
// the repo builds through hardhat + 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/rwa-transfer-adapter-invariant-property.test.js
// One new test file only, no contract-logic change, deploy nothing to any node.
//
// INVARIANTS FUZZED (exactly the RWATransferAdapter brief):
//   * NO OVER-REDEMPTION / EXACT PRICE-MINUS-HAIRCUT: a redeem pays out EXACTLY
//     _scaleDown( (rwaValueUsd*(1-haircut)) * 1e18 / payoutPrice ) and never a
//     wei more. Verified by a full BigInt differential re-implementation of the
//     on-chain floor math, asserted equal to the actor's USDC.e balance delta.
//   * NEVER-MORE-THAN-WORTH: the USD value of the payout received, re-priced at
//     the SAME oracle, never exceeds the post-haircut USD value of the RWA input
//     (and a fortiori never exceeds the pre-haircut value). Rounding ALWAYS
//     favors the adapter — the adapter keeps every wei of rounding dust.
//   * CANNOT REDEEM MORE THAN HELD: no redeem ever pays more USDC.e than the
//     adapter's balance; conservation balance == funded - sum(payouts) is exact.
//   * SWEEP ONLY VIA REDEEM: totalAccumulated grows ONLY through redeem and the
//     Foundation sweep can remove at most what redemptions deposited
//     (cumulativeSwept <= cumulativeRedeemedRWA); the sweep can NEVER touch the
//     USDC.e treasury (there is no such code path and the balance is invariant to
//     it). The adapter's RWA token balance always equals states.totalAccumulated.
//   * RATE LIMIT: after every successful redeem the recorded periodRedeemed never
//     exceeds the immutable rateLimitPerPeriod, and a dedicated leaky-bucket
//     campaign proves the cap genuinely bounds a burst.
//
// 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) : 0x5A11ADAF; // "salladaf"
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 ONE18 = 10n ** 18n;
const TENK = 10_000n;
const HUGE_CAP = 10n ** 40n; // effectively-unbounded rate limit for the value campaigns

function pow10(n) {
  return 10n ** BigInt(n);
}

// Exact BigInt mirror of RWATransferAdapter._scaleUp (all divisions floor).
function scaleUp(amount, dec) {
  if (dec === 18) return amount;
  if (dec < 18) return amount * pow10(18 - dec);
  return amount / pow10(dec - 18);
}
// Exact BigInt mirror of RWATransferAdapter._scaleDown.
function scaleDown(amount, dec) {
  if (dec === 18) return amount;
  if (dec < 18) return amount / pow10(18 - dec);
  return amount * pow10(dec - 18);
}

// Exact BigInt mirror of the redeem payout math (contract lines 168-172).
function predictPayout(rwaAmount, assetDec, haircutBps, rwaPrice, payoutPrice, payoutDec) {
  let valueUsd = (scaleUp(rwaAmount, assetDec) * rwaPrice) / ONE18;
  valueUsd = (valueUsd * (TENK - BigInt(haircutBps))) / TENK;
  const payoutOut = scaleDown((valueUsd * ONE18) / payoutPrice, payoutDec);
  return { payoutOut, haircutValueUsd: valueUsd };
}

// Iteration counts (env-overridable so a quick smoke can shrink them).
const N = {
  VALUE_CAMPAIGNS: Number(process.env.RWA_VALUE_CAMPAIGNS || 8),
  VALUE_STEPS: Number(process.env.RWA_VALUE_STEPS || 60),
  RL_CAMPAIGNS: Number(process.env.RWA_RL_CAMPAIGNS || 4),
  RL_STEPS: Number(process.env.RWA_RL_STEPS || 45),
};

const ASSET_DECS = [6, 8, 18, 27];       // RWA token decimals seen in the wild
const PAYOUT_DECS = [6, 8, 18];          // USDC.e is 6; fuzz others for generality
const HAIRCUTS = [0, 1, 25, 100, 250, 500, 1234, 4999, 5000];

// price samples in 1e18-normalised USD (RWAs hover near $1 but can drift; USDC
// can depeg either way).
function rndPrice(rng) {
  const k = rng();
  if (k < 0.5) return ONE18; // exactly $1
  if (k < 0.8) return (ONE18 * BigInt(ri(rng, 80, 120))) / 100n; // 0.80 .. 1.20
  return (ONE18 * BigInt(ri(rng, 5, 500))) / 100n; // 0.05 .. 5.00 (stress)
}
function rndPayoutPrice(rng) {
  const k = rng();
  if (k < 0.7) return ONE18;
  return (ONE18 * BigInt(ri(rng, 90, 110))) / 100n; // mild depeg, always > 0
}

// random RWA input: dust (forces payout->0 rounding reverts), small, and large.
function rndRwaAmount(rng, dec) {
  const unit = pow10(dec);
  const k = rng();
  if (k < 0.2) return BigInt(ri(rng, 1, 1000)); // raw dust in smallest units
  if (k < 0.65) return (unit * BigInt(ri(rng, 1, 500))) / BigInt(ri(rng, 1, 997)); // fractional
  return unit * BigInt(ri(rng, 1, 5000)); // whole tokens
}

// =============================================================================
describe("INVARIANT: RWATransferAdapter (permissionless RWA->USDC.e redeem + sweep)", function () {
  this.timeout(0);

  let signers, erc20F, oracleF, adapterF;

  before(async function () {
    signers = await ethers.getSigners();
    erc20F = await ethers.getContractFactory("MockERC20Lending");
    oracleF = await ethers.getContractFactory("MockPriceOracle");
    adapterF = await ethers.getContractFactory("RWATransferAdapter");
    console.log(
      `      [rwa] base seed 0x${BASE_SEED.toString(16)} | ` +
        `${N.VALUE_CAMPAIGNS}x${N.VALUE_STEPS} value + ${N.RL_CAMPAIGNS}x${N.RL_STEPS} rate-limit`
    );
  });

  // Deploy a fresh adapter with `numAssets` registered RWAs, a chosen payout-token
  // decimal count, and either huge (value campaign) or tight (rate-limit) caps.
  async function deployStack(rng, { numAssets, payoutDec, tightCaps }) {
    const foundation = signers[1];
    const usdce = await erc20F.deploy("USDC.e", "USDC.e", payoutDec);
    await usdce.waitForDeployment();
    const oracle = await oracleF.deploy();
    await oracle.waitForDeployment();

    const adapter = await adapterF.deploy(
      await usdce.getAddress(),
      await oracle.getAddress(),
      foundation.address,
      payoutDec
    );
    await adapter.waitForDeployment();
    const adapterAddr = await adapter.getAddress();

    // payout-token oracle price (never zero -> no div-by-zero).
    const payoutPrice = rndPayoutPrice(rng);
    await (await oracle.setPrice(await usdce.getAddress(), payoutPrice)).wait();

    const actors = [signers[2], signers[3], signers[4], signers[5]];

    const assets = [];
    const names = ["BUIDL.e", "USDY.e", "OUSG.e", "STBT.e", "USTB.e"];
    for (let i = 0; i < numAssets; i++) {
      const dec = pick(rng, ASSET_DECS);
      const tok = await erc20F.deploy(names[i], names[i], dec);
      await tok.waitForDeployment();
      const tokAddr = await tok.getAddress();
      const haircut = pick(rng, HAIRCUTS);
      const cap = tightCaps ? BigInt(ri(rng, 5, 200)) * pow10(payoutDec) : HUGE_CAP;
      const periodSeconds = tightCaps ? ri(rng, 30, 3600) : ri(rng, 1, 86400);
      const price = rndPrice(rng);
      await (await oracle.setPrice(tokAddr, price)).wait();
      await (
        await adapter.connect(foundation).registerAsset(tokAddr, haircut, cap, periodSeconds, dec)
      ).wait();

      // mint each actor a deep RWA balance and approve the adapter max.
      for (const a of actors) {
        await (await tok.mint(a.address, pow10(dec) * 10n ** 9n)).wait();
        await (await tok.connect(a).approve(adapterAddr, ethers.MaxUint256)).wait();
      }
      assets.push({ tok, tokAddr, dec, haircut, cap, periodSeconds, price });
    }

    return { foundation, usdce, usdceAddr: await usdce.getAddress(), oracle, adapter, adapterAddr, actors, assets, payoutPrice, payoutDec };
  }

  // The full non-per-redeem invariant battery, run after EVERY step.
  async function assertBattery(ctx, env, st) {
    const { adapter, adapterAddr, usdce, assets } = env;

    // ---- CONSERVATION: USDC.e balance == funded - sum(payouts), exactly. The
    //      ONLY exit for the payout treasury is a redeem payout. ----
    const bal = await usdce.balanceOf(adapterAddr);
    expect(bal, `[rwa CONSERVE] usdce balance != funded-payouts ${ctx} funded=${st.funded} paid=${st.paid}`)
      .to.equal(st.funded - st.paid);
    expect(st.paid, `[rwa NO-OVERPAY] cumulative payouts exceeded funding ${ctx}`).to.be.lte(st.funded);

    for (const asset of assets) {
      const s = await adapter.states(asset.tokAddr);
      const c = await adapter.configs(asset.tokAddr);
      const acc = st.rwaIn[asset.tokAddr] - st.swept[asset.tokAddr];

      // ---- SWEEP-ONLY-VIA-REDEEM: totalAccumulated == in(redeem) - out(sweep) ----
      expect(s.totalAccumulated, `[rwa ACC] totalAccumulated != rwaIn-swept ${ctx} asset=${asset.tokAddr.slice(0, 10)}`)
        .to.equal(acc);
      // the adapter's actual RWA balance equals the accounted accumulation (no
      // phantom RWA, and the sweep ceiling is real).
      const rwaBal = await asset.tok.balanceOf(adapterAddr);
      expect(rwaBal, `[rwa RWABAL] rwa token balance != totalAccumulated ${ctx} asset=${asset.tokAddr.slice(0, 10)}`)
        .to.equal(acc);
      // ---- sweep can never remove more than redemptions deposited ----
      expect(st.swept[asset.tokAddr], `[rwa SWEEP-BOUND] swept>redeemed-in ${ctx} asset=${asset.tokAddr.slice(0, 10)}`)
        .to.be.lte(st.rwaIn[asset.tokAddr]);
      // ---- RATE LIMIT: recorded periodRedeemed never exceeds the immutable cap ----
      expect(s.periodRedeemed, `[rwa RL-STATE] periodRedeemed>cap ${ctx} asset=${asset.tokAddr.slice(0, 10)}`)
        .to.be.lte(c.rateLimitPerPeriod);
    }
  }

  // ---- VALUE campaigns: huge caps, so redeem success is fully predictable and
  //      every successful redeem is diff-checked against the exact BigInt math. --
  it("value math: exact payout, never-more-than-worth, rounding favors adapter, conservation, sweep bound", async function () {
    let steps = 0, redeems = 0, zeroReverts = 0, liqReverts = 0, sweeps = 0, fundings = 0, priceMoves = 0, pauses = 0;

    for (let c = 0; c < N.VALUE_CAMPAIGNS; c++) {
      const rng = rngFor("value", c);
      const payoutDec = pick(rng, PAYOUT_DECS);
      const env = await deployStack(rng, { numAssets: ri(rng, 1, 3), payoutDec, tightCaps: false });
      const { foundation, usdce, oracle, adapter, adapterAddr, actors, assets } = env;

      const st = {
        funded: 0n,
        paid: 0n,
        rwaIn: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        swept: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        // live per-asset price mirror (starts at what deployStack set).
        price: Object.fromEntries(assets.map((a) => [a.tokAddr, a.price])),
        payoutPrice: env.payoutPrice,
      };

      // seed the treasury.
      const seed = BigInt(ri(rng, 1, 5_000_000)) * pow10(payoutDec);
      await (await usdce.mint(adapterAddr, seed)).wait();
      st.funded += seed;

      await assertBattery(`c${c} init`, env, st);

      for (let s = 0; s < N.VALUE_STEPS; s++) {
        const action = pick(rng, [
          "redeem", "redeem", "redeem", "redeem", "fund", "sweep",
          "priceMove", "payoutPriceMove", "pause", "advance",
        ]);
        const actor = pick(rng, actors);
        const asset = pick(rng, assets);
        const ctx = `c${c} s${s} actor=${actor.address.slice(0, 8)} action=${action} asset=${asset.tokAddr.slice(0, 8)}`;

        if (action === "redeem") {
          const cfg = await adapter.configs(asset.tokAddr);
          const paused = cfg.paused;
          const amount = rndRwaAmount(rng, asset.dec);
          const { payoutOut, haircutValueUsd } = predictPayout(
            amount, asset.dec, asset.haircut, st.price[asset.tokAddr], st.payoutPrice, env.payoutDec
          );
          const balBefore = await usdce.balanceOf(adapterAddr);
          const actorBefore = await usdce.balanceOf(actor.address);

          // classify the EXPECTED outcome up front (huge cap => rate limit never binds).
          if (paused) {
            await expect(
              adapter.connect(actor).redeem(asset.tokAddr, amount),
              `[rwa PRED-PAUSE] expected AssetIsPaused ${ctx}`
            ).to.be.revertedWithCustomError(adapter, "AssetIsPaused");
          } else if (amount === 0n) {
            // rndRwaAmount never returns 0, but guard anyway.
            continue;
          } else if (payoutOut === 0n) {
            await expect(
              adapter.connect(actor).redeem(asset.tokAddr, amount),
              `[rwa PRED-ZERO] expected ZeroAmount for dust ${ctx} amt=${amount}`
            ).to.be.revertedWithCustomError(adapter, "ZeroAmount");
            zeroReverts++;
          } else if (balBefore < payoutOut) {
            await expect(
              adapter.connect(actor).redeem(asset.tokAddr, amount),
              `[rwa PRED-LIQ] expected InsufficientPayoutLiquidity ${ctx} need=${payoutOut} have=${balBefore}`
            ).to.be.revertedWithCustomError(adapter, "InsufficientPayoutLiquidity");
            liqReverts++;
          } else {
            // must SUCCEED and pay EXACTLY the predicted amount.
            await (await adapter.connect(actor).redeem(asset.tokAddr, amount)).wait();
            const delta = (await usdce.balanceOf(actor.address)) - actorBefore;

            // ---- NO OVER-REDEMPTION: exact price-minus-haircut, to the wei ----
            expect(delta, `[rwa EXACT] payout != predicted ${ctx} amt=${amount} got=${delta} want=${payoutOut}`)
              .to.equal(payoutOut);

            // ---- NEVER-MORE-THAN-WORTH + ROUNDING-FAVORS-ADAPTER: re-price the
            //      payout at the SAME oracle; must be <= the post-haircut RWA value
            //      (and hence <= the pre-haircut value). ----
            const payoutValueUsd = (scaleUp(delta, env.payoutDec) * st.payoutPrice) / ONE18;
            expect(payoutValueUsd, `[rwa WORTH] payout worth ${payoutValueUsd} > haircut value ${haircutValueUsd} ${ctx}`)
              .to.be.lte(haircutValueUsd);
            const rwaValueUsd = (scaleUp(amount, asset.dec) * st.price[asset.tokAddr]) / ONE18;
            expect(payoutValueUsd, `[rwa WORTH-GROSS] payout worth ${payoutValueUsd} > gross RWA value ${rwaValueUsd} ${ctx}`)
              .to.be.lte(rwaValueUsd);

            // ---- CANNOT REDEEM MORE THAN HELD ----
            expect(delta, `[rwa HELD] payout ${delta} exceeded balance ${balBefore} ${ctx}`).to.be.lte(balBefore);

            st.paid += delta;
            st.rwaIn[asset.tokAddr] += amount;
            redeems++;
          }
        } else if (action === "fund") {
          const amt = BigInt(ri(rng, 1, 2_000_000)) * pow10(env.payoutDec);
          await (await usdce.mint(adapterAddr, amt)).wait();
          st.funded += amt;
          fundings++;
        } else if (action === "sweep") {
          const s2 = await adapter.states(asset.tokAddr);
          const acc = s2.totalAccumulated;
          if (acc === 0n) {
            // sweeping nothing must revert ZeroAmount or InsufficientSweepBalance.
            await expect(
              adapter.connect(foundation).sweepRWA(asset.tokAddr, 1n, signers[8].address),
              `[rwa SWEEP-EMPTY] ${ctx}`
            ).to.be.revertedWithCustomError(adapter, "InsufficientSweepBalance");
          } else {
            const amt = chance(rng, 0.4) ? acc : (acc * BigInt(ri(rng, 1, 100))) / 100n;
            const amtF = amt === 0n ? 1n : amt;
            // over-sweep must revert; exact/under must succeed.
            if (amtF > acc) {
              await expect(
                adapter.connect(foundation).sweepRWA(asset.tokAddr, amtF, signers[8].address),
                `[rwa SWEEP-OVER] ${ctx}`
              ).to.be.revertedWithCustomError(adapter, "InsufficientSweepBalance");
            } else {
              await (await adapter.connect(foundation).sweepRWA(asset.tokAddr, amtF, signers[8].address)).wait();
              st.swept[asset.tokAddr] += amtF;
              sweeps++;
            }
          }
        } else if (action === "priceMove") {
          const p = rndPrice(rng);
          await (await oracle.setPrice(asset.tokAddr, p)).wait();
          st.price[asset.tokAddr] = p;
          priceMoves++;
        } else if (action === "payoutPriceMove") {
          const p = rndPayoutPrice(rng);
          await (await oracle.setPrice(env.usdceAddr, p)).wait();
          st.payoutPrice = p;
          priceMoves++;
        } else if (action === "pause") {
          const cfg = await adapter.configs(asset.tokAddr);
          await (await adapter.connect(foundation).setAssetPaused(asset.tokAddr, !cfg.paused)).wait();
          pauses++;
        } else if (action === "advance") {
          await time.increase(ri(rng, 1, 2 * 86400));
          await network.provider.send("evm_mine");
        }

        await assertBattery(ctx, env, st);
        steps++;
      }

      // finale: Foundation attempts to drain everything it legitimately can, then
      // we re-check that the USDC.e treasury was never touched by the sweep path.
      const balBeforeSweeps = await usdce.balanceOf(adapterAddr);
      for (const asset of assets) {
        const s2 = await adapter.states(asset.tokAddr);
        if (s2.totalAccumulated > 0n) {
          await (await adapter.connect(foundation).sweepRWA(asset.tokAddr, s2.totalAccumulated, signers[8].address)).wait();
          st.swept[asset.tokAddr] += s2.totalAccumulated;
        }
      }
      expect(await usdce.balanceOf(adapterAddr), `[rwa SWEEP-NO-TOUCH-USDC] c${c}`).to.equal(balBeforeSweeps);
      await assertBattery(`c${c} finale`, env, st);
    }

    console.log(
      `      [rwa value] steps=${steps} redeems=${redeems} zeroRev=${zeroReverts} liqRev=${liqReverts} ` +
        `sweeps=${sweeps} fund=${fundings} priceMoves=${priceMoves} pauses=${pauses}`
    );
    expect(steps).to.be.greaterThan(300);
    expect(redeems).to.be.greaterThan(60);
    expect(sweeps).to.be.greaterThan(0);
  });

  // ---- RATE-LIMIT campaign: tight caps + time travel. We do NOT predict the
  //      exact accept/reject (block.timestamp is set at mine time), but we prove
  //      the safety property: recorded periodRedeemed never exceeds the cap, a
  //      successful redeem always leaves outstanding <= cap, and a same-timestamp
  //      burst can never exceed the cap in one period. ----
  it("rate limit: leaky-bucket cap is never exceeded across bursts and time travel", async function () {
    let steps = 0, ok = 0, rejected = 0, burstTests = 0;

    for (let c = 0; c < N.RL_CAMPAIGNS; c++) {
      const rng = rngFor("rl", c);
      const payoutDec = pick(rng, PAYOUT_DECS);
      const env = await deployStack(rng, { numAssets: ri(rng, 1, 2), payoutDec, tightCaps: true });
      const { foundation, usdce, oracle, adapter, adapterAddr, actors, assets } = env;

      const st = {
        funded: 0n,
        paid: 0n,
        rwaIn: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        swept: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        price: Object.fromEntries(assets.map((a) => [a.tokAddr, a.price])),
        payoutPrice: env.payoutPrice,
      };
      // deep treasury so liquidity never masks the rate limit.
      const seed = 10n ** 12n * pow10(payoutDec);
      await (await usdce.mint(adapterAddr, seed)).wait();
      st.funded += seed;
      await assertBattery(`rl${c} init`, env, st);

      for (let s = 0; s < N.RL_STEPS; s++) {
        const action = pick(rng, ["redeem", "redeem", "redeem", "advance", "burst"]);
        const actor = pick(rng, actors);
        const asset = pick(rng, assets);
        const ctx = `rl${c} s${s} actor=${actor.address.slice(0, 8)} asset=${asset.tokAddr.slice(0, 8)} action=${action}`;
        const cfg = await adapter.configs(asset.tokAddr);
        const cap = cfg.rateLimitPerPeriod;

        if (action === "advance") {
          await time.increase(ri(rng, 1, 2 * Number(asset.periodSeconds)));
          await network.provider.send("evm_mine");
        } else if (action === "burst") {
          // Fire several redeems in immediate succession (same-ish timestamp) and
          // confirm the recorded outstanding never crosses the cap. We size each
          // redeem to roughly a third of the cap so a few fit and the rest revert.
          burstTests++;
          for (let b = 0; b < 6; b++) {
            // choose an RWA amount whose payout is ~ cap/3.
            const amount = pickAmountForPayout(cap / 3n, asset, st, env);
            if (amount === 0n) break;
            const actorBefore = await usdce.balanceOf(actor.address);
            try {
              await (await adapter.connect(actor).redeem(asset.tokAddr, amount)).wait();
              const delta = (await usdce.balanceOf(actor.address)) - actorBefore;
              st.paid += delta;
              st.rwaIn[asset.tokAddr] += amount;
              ok++;
            } catch (e) {
              if (!/RateLimitExceeded|InsufficientPayoutLiquidity|ZeroAmount|AssetIsPaused/.test(String(e.message))) {
                throw e;
              }
              rejected++;
            }
            const sSt = await adapter.states(asset.tokAddr);
            expect(sSt.periodRedeemed, `[rwa RL-BURST] periodRedeemed>cap ${ctx} b=${b}`).to.be.lte(cap);
          }
        } else {
          // ordinary randomly-sized redeem.
          const amount = rndRwaAmount(rng, asset.dec);
          const { payoutOut } = predictPayout(amount, asset.dec, asset.haircut, st.price[asset.tokAddr], st.payoutPrice, env.payoutDec);
          const balBefore = await usdce.balanceOf(adapterAddr);
          const actorBefore = await usdce.balanceOf(actor.address);
          try {
            await (await adapter.connect(actor).redeem(asset.tokAddr, amount)).wait();
            const delta = (await usdce.balanceOf(actor.address)) - actorBefore;
            // even under rate limiting, a SUCCESS still pays exactly the formula.
            expect(delta, `[rwa RL-EXACT] payout != predicted ${ctx} got=${delta} want=${payoutOut}`).to.equal(payoutOut);
            expect(delta, `[rwa RL-HELD] payout>balance ${ctx}`).to.be.lte(balBefore);
            st.paid += delta;
            st.rwaIn[asset.tokAddr] += amount;
            ok++;
          } catch (e) {
            if (!/RateLimitExceeded|InsufficientPayoutLiquidity|ZeroAmount|AssetIsPaused/.test(String(e.message))) {
              throw e;
            }
            rejected++;
          }
          const sSt = await adapter.states(asset.tokAddr);
          expect(sSt.periodRedeemed, `[rwa RL-CAP] periodRedeemed>cap ${ctx}`).to.be.lte(cap);
        }

        await assertBattery(ctx, env, st);
        steps++;
      }
    }

    console.log(`      [rwa rate-limit] steps=${steps} ok=${ok} rejected=${rejected} burstTests=${burstTests}`);
    expect(steps).to.be.greaterThan(120);
    expect(ok).to.be.greaterThan(20);
    expect(rejected).to.be.greaterThan(0); // the cap genuinely bit at least once
  });

  // ---- LIQUIDITY campaign: deliberately THIN treasury so the payout guard is
  //      exercised for real. Proves "a caller can never redeem more than the
  //      adapter holds": every redeem whose predicted payout exceeds the current
  //      balance MUST revert InsufficientPayoutLiquidity and leave the treasury
  //      untouched; every success pays exactly the formula and cannot underflow. --
  it("liquidity guard: redeem never pays more than the adapter holds (thin treasury)", async function () {
    let steps = 0, ok = 0, liqReverts = 0, refills = 0;

    for (let c = 0; c < 3; c++) {
      const rng = rngFor("liq", c);
      const payoutDec = pick(rng, PAYOUT_DECS);
      const env = await deployStack(rng, { numAssets: ri(rng, 1, 2), payoutDec, tightCaps: false });
      const { usdce, oracle, adapter, adapterAddr, actors, assets } = env;

      const st = {
        funded: 0n, paid: 0n,
        rwaIn: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        swept: Object.fromEntries(assets.map((a) => [a.tokAddr, 0n])),
        price: Object.fromEntries(assets.map((a) => [a.tokAddr, a.price])),
        payoutPrice: env.payoutPrice,
      };
      // seed only a small treasury so redeems routinely outrun it.
      const seed = BigInt(ri(rng, 1, 50)) * pow10(payoutDec);
      await (await usdce.mint(adapterAddr, seed)).wait();
      st.funded += seed;
      await assertBattery(`liq${c} init`, env, st);

      for (let s = 0; s < 50; s++) {
        const action = pick(rng, ["redeem", "redeem", "redeem", "redeem", "refill", "priceMove"]);
        const actor = pick(rng, actors);
        const asset = pick(rng, assets);
        const ctx = `liq${c} s${s} actor=${actor.address.slice(0, 8)} asset=${asset.tokAddr.slice(0, 8)} action=${action}`;

        if (action === "refill") {
          const amt = BigInt(ri(rng, 1, 100)) * pow10(env.payoutDec);
          await (await usdce.mint(adapterAddr, amt)).wait();
          st.funded += amt;
          refills++;
        } else if (action === "priceMove") {
          const p = rndPrice(rng);
          await (await oracle.setPrice(asset.tokAddr, p)).wait();
          st.price[asset.tokAddr] = p;
        } else {
          // bias amounts LARGE so payout often exceeds the thin balance.
          const amount = pow10(asset.dec) * BigInt(ri(rng, 1, 100));
          const { payoutOut } = predictPayout(amount, asset.dec, asset.haircut, st.price[asset.tokAddr], st.payoutPrice, env.payoutDec);
          const balBefore = await usdce.balanceOf(adapterAddr);
          const actorBefore = await usdce.balanceOf(actor.address);
          if (payoutOut === 0n) {
            await expect(adapter.connect(actor).redeem(asset.tokAddr, amount), `[rwa LIQ-ZERO] ${ctx}`)
              .to.be.revertedWithCustomError(adapter, "ZeroAmount");
          } else if (balBefore < payoutOut) {
            await expect(adapter.connect(actor).redeem(asset.tokAddr, amount), `[rwa LIQ-GUARD] need=${payoutOut} have=${balBefore} ${ctx}`)
              .to.be.revertedWithCustomError(adapter, "InsufficientPayoutLiquidity");
            // treasury must be completely untouched by the reverted redeem.
            expect(await usdce.balanceOf(adapterAddr), `[rwa LIQ-UNTOUCHED] ${ctx}`).to.equal(balBefore);
            liqReverts++;
          } else {
            await (await adapter.connect(actor).redeem(asset.tokAddr, amount)).wait();
            const delta = (await usdce.balanceOf(actor.address)) - actorBefore;
            expect(delta, `[rwa LIQ-EXACT] ${ctx} got=${delta} want=${payoutOut}`).to.equal(payoutOut);
            expect(delta, `[rwa LIQ-HELD] payout>balance ${ctx}`).to.be.lte(balBefore);
            st.paid += delta;
            st.rwaIn[asset.tokAddr] += amount;
            ok++;
          }
        }

        await assertBattery(ctx, env, st);
        steps++;
      }
    }

    console.log(`      [rwa liquidity] steps=${steps} ok=${ok} liqReverts=${liqReverts} refills=${refills}`);
    expect(steps).to.be.greaterThan(120);
    expect(liqReverts).to.be.greaterThan(0); // the payout guard genuinely fired
    expect(ok).to.be.greaterThan(0);
  });

  // helper: pick an RWA input amount whose predicted payout is close to `target`.
  function pickAmountForPayout(target, asset, st, env) {
    if (target <= 0n) return 0n;
    // invert the formula roughly: payout ~= scaleDown( rwaValue*(1-h)*1e18/pp )
    // rwaValue = scaleUp(amount,dec)*price/1e18. Solve for amount (approximate,
    // exactness is not needed; the differential + cap checks do the verifying).
    const pp = st.payoutPrice;
    const price = st.price[asset.tokAddr];
    const h = TENK - BigInt(asset.haircut);
    // target (payoutDec) -> target*1e18/... invert scaleDown:
    let payoutRaw18 = scaleUpForInvert(target, env.payoutDec); // to 18-dec
    // payoutRaw18 = rwaValue*(1-h)*1e18/pp / 1e18-ish ... reconstruct rwaValue:
    let rwaValue = (payoutRaw18 * pp) / ONE18;
    rwaValue = (rwaValue * TENK) / h;
    // rwaValue = scaleUp(amount,dec)*price/1e18 -> scaleUp(amount,dec)=rwaValue*1e18/price
    const scaledAmt = (rwaValue * ONE18) / price;
    // invert scaleUp to raw asset units.
    let amount;
    if (asset.dec === 18) amount = scaledAmt;
    else if (asset.dec < 18) amount = scaledAmt / pow10(18 - asset.dec);
    else amount = scaledAmt * pow10(asset.dec - 18);
    return amount > 0n ? amount : 1n;
  }
  function scaleUpForInvert(amount, dec) {
    // map a payoutDec-denominated amount back to an 18-dec magnitude.
    if (dec === 18) return amount;
    if (dec < 18) return amount * pow10(18 - dec);
    return amount / pow10(dec - 18);
  }
});
