// ============================================================================= // SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for the LIVE // AereInsuranceFund (canonical mainnet 0x5Ab95C549c2A2b07913Df7edD4a16fd108B7CAAC // per sdk-js/src/addresses.ts, "7-day cooldown"). Source under test: // contracts/lending/AereInsuranceFund.sol // exercised against the REAL AereLendingMarket (contracts/lending/ // AereLendingMarket.sol), which is the production counterpart whose // repayOnBehalfOf / debtBalance / DEBT_TOKEN the fund actually calls. Genuine // BadDebt is manufactured via an insolvent liquidation, exactly the situation // the fund exists to absorb. Market interest is frozen (setBorrowRate(0)) so // debt accounting is exact and every invariant is crisp. // // 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. Existing repo mocks are reused (MockERC20Lending, // MockAggregator); no new contract is added, nothing is deployed to any node. // // SCOPE: run this file in ISOLATION (the full suite OOM/segfaults on the // unrelated ML-DSA PQC KAT tests): // npx hardhat test test/insurance-fund-invariant-property.test.js // // INVARIANTS FUZZED (exactly the AereInsuranceFund brief): // G1 CUSTODY-BALANCE: for every token, real ERC-20 balanceOf(fund) equals the // fund's tracked balances[token] plus any untracked stray transfers. The // tracked mapping is NEVER greater than the real balance, so the fund can // never be asked to pay out more than it truly holds. // G2 ACCOUNTING-CONSERVATION: balances[token] == totalDonated - totalCovered. // G3 CAP: for every registered market, covered <= lifetimeCap, ALWAYS. Cap can // never be exceeded; the cap check applies to the CLAMPED payout amount // (min(request, owed)), so a request over the raw cap that clamps under it // still succeeds, while a clamped amount over the cap reverts CapExceeded. // G4 NO-VALUE-CREATION: cumulative coverage paid in a token never exceeds // cumulative donations of that token (can't pay out value that never came in). // G5 COOLDOWN: two successful coverages on the same market are always at least // COOLDOWN_SECONDS apart; a second cover inside the window reverts. // G6 DEBT-ONLY-DECREASES: fund actions never mint or increase any borrower's // debt; repayOnBehalfOf can only REDUCE it, by exactly the tokens paid. // G7 COVER-NEEDS-REAL-DEBT: coverage is only ever paid against a borrower who // actually owes, and the paid amount is clamped to the outstanding debt; // covering a no-debt account reverts NoDebtToCover. // G8 NO-ESCAPE-HATCH: the ONLY state-mutating externals are // {proposeMarket, registerMarket, cancelPendingMarket, donate, coverBadDebt} // and coverBadDebt is the ONLY path that removes tokens -- there is no // withdraw / rescue / sweep / emergency / owner-drain function, and only // FOUNDATION can call the payout path or propose a market. // // 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) : 0x1F5EED; 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 ONE6 = 10n ** 6n; // USDC.e style debt token const ONE18 = 10n ** 18n; // collateral const REG_DELAY = 7 * 86400 + 5; const N = { CAMPAIGNS: Number(process.env.IF_CAMPAIGNS || 3), STEPS: Number(process.env.IF_STEPS || 60), MARKETS: Number(process.env.IF_MARKETS || 2), BORROWERS: Number(process.env.IF_BORROWERS || 2), }; // ============================================================================= describe("INVARIANT: AereInsuranceFund (bad-debt coverage, custody + bounded payout)", function () { this.timeout(0); let signers; let ERC20F, AggF, OracleF, WAEREF, BurnF, SinkF, MarketF, FundF; before(async function () { signers = await ethers.getSigners(); ERC20F = await ethers.getContractFactory("MockERC20Lending"); AggF = await ethers.getContractFactory("MockAggregator"); OracleF = await ethers.getContractFactory("AereLendingOracle"); WAEREF = await ethers.getContractFactory("contracts/WAERE.sol:WAERE"); BurnF = await ethers.getContractFactory("AereFeeBurnVault"); SinkF = await ethers.getContractFactory("AereSink"); MarketF = await ethers.getContractFactory("AereLendingMarket"); FundF = await ethers.getContractFactory("AereInsuranceFund"); console.log( ` [ifund] base seed 0x${BASE_SEED.toString(16)} | ` + `${N.CAMPAIGNS} campaigns x ${N.STEPS} steps, ${N.MARKETS} markets/${N.BORROWERS} borrowers each` ); }); // Shared infra (oracle + sink) for a campaign. Debt token is shared across // all markets in a campaign so the balances/donation/coverage accounting is a // single, aggregated ledger (the hardest case to keep consistent). async function deployInfra(deployer) { const debt = await ERC20F.deploy("USDC.e", "USDC.e", 6); await debt.waitForDeployment(); const aggD = await AggF.deploy(1_00000000); // $1.00, 8 decimals await aggD.waitForDeployment(); const oracle = await OracleF.deploy(); await oracle.waitForDeployment(); await oracle.configureFeed(await debt.getAddress(), 0, await aggD.getAddress(), ethers.ZeroHash, 30 * 86400, 0, 8); const waere = await WAEREF.deploy(); await waere.waitForDeployment(); const burn = await BurnF.deploy(); await burn.waitForDeployment(); const sink = await SinkF.deploy( await waere.getAddress(), await burn.getAddress(), await waere.getAddress(), await waere.getAddress(), 1500, 4000, 4500, 200, ethers.ZeroAddress ); await sink.waitForDeployment(); return { debt, aggD, oracle, sink }; } // Build one market on shared infra + manufacture real BadDebt on `borrowers` // via an insolvent liquidation. Interest is frozen so residual debt is static. async function buildMarketWithBadDebt(infra, deployer, supplier, liquidator, borrowers) { const collateral = await ERC20F.deploy("BUIDL.e", "BUIDL.e", 18); await collateral.waitForDeployment(); const aggC = await AggF.deploy(1_00000000); // $1.00 to start await aggC.waitForDeployment(); await infra.oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 30 * 86400, 0, 8); const cfg = { collateral: await collateral.getAddress(), debtToken: await infra.debt.getAddress(), sink: await infra.sink.getAddress(), oracle: await infra.oracle.getAddress(), ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, borrowFeeBps: 100, marketDebtCap: 1_000_000_000n * ONE6, collateralDecimals: 18, debtDecimals: 6, }; const market = await MarketF.deploy(cfg); await market.waitForDeployment(); const marketAddr = await market.getAddress(); // Freeze interest so debt accounting stays exact across time warps. await (await market.connect(deployer).setBorrowRate(0)).wait(); // Supplier provides borrowable liquidity. await (await infra.debt.mint(supplier.address, 50_000_000n * ONE6)).wait(); await (await infra.debt.connect(supplier).approve(marketAddr, ethers.MaxUint256)).wait(); await (await market.connect(supplier).supply(20_000_000n * ONE6)).wait(); // Each borrower posts 1000 collateral @ $1, borrows 690 (< 70% LTV). for (const b of borrowers) { await (await collateral.mint(b.address, 1000n * ONE18)).wait(); await (await collateral.connect(b).approve(marketAddr, ethers.MaxUint256)).wait(); await (await market.connect(b).depositCollateral(1000n * ONE18)).wait(); await (await market.connect(b).borrow(690n * ONE6)).wait(); } // Crash collateral to $0.05 -> deeply insolvent, then liquidate fully. await (await aggC.set(5_000_000)).wait(); await (await market.accrueInterest()).wait(); await (await infra.debt.mint(liquidator.address, BigInt(borrowers.length) * 1000n * ONE6)).wait(); await (await infra.debt.connect(liquidator).approve(marketAddr, ethers.MaxUint256)).wait(); for (const b of borrowers) { await (await market.connect(liquidator).liquidate(b.address, 690n * ONE6)).wait(); const resid = await market.debtBalance(b.address); expect(resid, `bad-debt setup produced no residual for ${b.address}`).to.be.gt(0n); } return { market, marketAddr, collateral, aggC }; } // Structural proof: the fund has no escape hatch and only Foundation controls // the privileged paths. Run once per campaign against the deployed instance. function assertNoEscapeHatch(fund) { const allowedMutators = new Set([ "proposeMarket", "registerMarket", "cancelPendingMarket", "donate", "coverBadDebt", ]); const banned = /withdraw|rescue|sweep|emergency|drain|transferOwnership|renounce|setOwner|setFoundation|migrate|skim|recover/i; for (const f of fund.interface.fragments) { if (f.type !== "function") continue; if (f.stateMutability === "view" || f.stateMutability === "pure") continue; expect(banned.test(f.name), `[ifund G8] fund exposes suspicious mutator "${f.name}"`).to.equal(false); expect(allowedMutators.has(f.name), `[ifund G8] unexpected state-mutating external "${f.name}"`).to.equal(true); } } // The global invariant battery -- asserted after EVERY step. async function assertInvariants(ctx, fund, debt, ledger) { const debtAddr = await debt.getAddress(); const tracked = await fund.balances(debtAddr); const real = await debt.balanceOf(await fund.getAddress()); // G1 custody-balance: real == tracked + stray, and tracked <= real always. expect(real, `[ifund G1] real balance != tracked+stray ${ctx} tracked=${tracked} stray=${ledger.stray} real=${real}`) .to.equal(tracked + ledger.stray); expect(tracked, `[ifund G1b] tracked mapping exceeds real balance ${ctx}`).to.be.lte(real); // G2 accounting-conservation. expect(tracked, `[ifund G2] balances != donated-covered ${ctx} donated=${ledger.donated} covered=${ledger.covered}`) .to.equal(ledger.donated - ledger.covered); // G4 no-value-creation. expect(ledger.covered, `[ifund G4] covered ${ledger.covered} > donated ${ledger.donated} ${ctx}`) .to.be.lte(ledger.donated); // G3 cap per market + cooldown-spacing bookkeeping consistency. for (const m of ledger.markets) { const info = await fund.markets(m.addr); // info = [registered, lifetimeCap, covered, lastCoverageAt] expect(info[2], `[ifund G3] market covered ${info[2]} > cap ${info[1]} ${ctx} market=${m.addr.slice(0, 8)}`) .to.be.lte(info[1]); // on-chain covered must match our independently-tracked per-market total. expect(info[2], `[ifund G3b] per-market covered drift ${ctx} market=${m.addr.slice(0, 8)}`).to.equal(m.covered); } // G6 debt-only-decreases: no borrower debt ever exceeds its recorded high-water. for (const b of ledger.borrowers) { const d = await b.market.debtBalance(b.addr); expect(d, `[ifund G6] borrower debt rose ${ctx} borrower=${b.addr.slice(0, 8)} was<=${b.debt} now=${d}`) .to.be.lte(b.debt); b.debt = d; // ratchet down } } // ------------------------------------------------------------------------- it("multi-market / multi-actor coverage: custody, bounded payout, cap+cooldown, conservation", async function () { const deployer = signers[0]; let totalSteps = 0, covers = 0, capReverts = 0, cooldownReverts = 0, noDebtReverts = 0, foundationReverts = 0, donations = 0, strays = 0, insufReverts = 0; for (let c = 0; c < N.CAMPAIGNS; c++) { const rng = rngFor("main", c); const foundation = signers[1]; const supplier = signers[2]; const liquidator = signers[3]; const donors = [signers[4], signers[5], signers[6]]; const attackers = [signers[7], signers[8]]; // borrower pool: MARKETS * BORROWERS distinct addresses from a stable base. const borrowerSigners = signers.slice(9, 9 + N.MARKETS * N.BORROWERS); const infra = await deployInfra(deployer); const debt = infra.debt; const debtAddr = await debt.getAddress(); const fund = await FundF.deploy(foundation.address, ri(rng, 3600, 2 * 86400)); // 1h..2d cooldown await fund.waitForDeployment(); const fundAddr = await fund.getAddress(); const cooldown = await fund.COOLDOWN_SECONDS(); assertNoEscapeHatch(fund); // Access-control probes: attackers can neither propose nor cover. await expect( fund.connect(attackers[0]).proposeMarket(borrowerSigners[0].address, 1n), "[ifund G8] attacker proposeMarket not rejected" ).to.be.revertedWithCustomError(fund, "NotFoundation"); const ledger = { donated: 0n, covered: 0n, stray: 0n, markets: [], borrowers: [], }; // Build markets + bad debt, register each with a random lifetime cap. let bi = 0; for (let mi = 0; mi < N.MARKETS; mi++) { const brs = borrowerSigners.slice(bi, bi + N.BORROWERS); bi += N.BORROWERS; const mk = await buildMarketWithBadDebt(infra, deployer, supplier, liquidator, brs); // Cap chosen to sometimes be tight (forces CapExceeded) and sometimes loose. const cap = BigInt(ri(rng, 200, 4000)) * ONE6; await (await fund.connect(foundation).proposeMarket(mk.marketAddr, cap)).wait(); ledger.markets.push({ addr: mk.marketAddr, market: mk.market, cap, covered: 0n }); for (const b of brs) { ledger.borrowers.push({ addr: b.address, market: mk.market, debt: await mk.market.debtBalance(b.address) }); } } // Elapse the 7-day registration timelock once, then execute all pending. await time.increase(REG_DELAY); await network.provider.send("evm_mine"); for (const m of ledger.markets) { await (await fund.connect(pick(rng, [foundation, ...attackers])).registerMarket(m.addr)).wait(); } // Seed the fund with an initial donation. const seed = BigInt(ri(rng, 3000, 12000)) * ONE6; await (await debt.mint(donors[0].address, seed)).wait(); await (await debt.connect(donors[0]).approve(fundAddr, ethers.MaxUint256)).wait(); await (await fund.connect(donors[0]).donate(debtAddr, seed)).wait(); ledger.donated += seed; donations++; await assertInvariants(`c${c} init`, fund, debt, ledger); // Non-foundation cover must be rejected (state-independent guard). await expect( fund.connect(attackers[1]).coverBadDebt(ledger.markets[0].addr, ledger.borrowers[0].addr, 1n), "[ifund G8] attacker coverBadDebt not rejected" ).to.be.revertedWithCustomError(fund, "NotFoundation"); foundationReverts++; for (let s = 0; s < N.STEPS; s++) { const action = pick(rng, [ "cover", "cover", "cover", "cover", "donate", "donate", "stray", "advance", "advance", "coverAttacker", "coverNoDebt", "coverExceedCap", "advance", ]); const ctx = `c${c} s${s} action=${action}`; try { if (action === "donate") { const donor = pick(rng, donors); const amt = BigInt(ri(rng, 1, 5000)) * ONE6; await (await debt.mint(donor.address, amt)).wait(); await (await debt.connect(donor).approve(fundAddr, amt)).wait(); await (await fund.connect(donor).donate(debtAddr, amt)).wait(); ledger.donated += amt; donations++; } else if (action === "stray") { // Untracked direct transfer: must inflate real balance ONLY, never // the tracked mapping, and never enable extra payout (G1/G1b). const donor = pick(rng, donors); const amt = BigInt(ri(rng, 1, 2000)) * ONE6; await (await debt.mint(donor.address, amt)).wait(); await (await debt.connect(donor).transfer(fundAddr, amt)).wait(); ledger.stray += amt; strays++; } else if (action === "advance") { await time.increase(ri(rng, 1, 3 * 86400)); await network.provider.send("evm_mine"); } else if (action === "cover") { const m = pick(rng, ledger.markets); const brs = ledger.borrowers.filter((b) => b.market === m.market); const b = pick(rng, brs); const owed = await m.market.debtBalance(b.addr); const remaining = await fund.coverageRemaining(m.addr); const ready = await fund.cooldownReady(m.addr); const tracked = await fund.balances(debtAddr); // Random request: sometimes partial, sometimes >= owed (forces clamp), // sometimes tiny. let amount; const k = rng(); if (k < 0.3) amount = BigInt(ri(rng, 1, 50)) * ONE6; else if (k < 0.7) amount = owed === 0n ? 1n : (owed * BigInt(ri(rng, 1, 90))) / 100n; else amount = owed + BigInt(ri(rng, 0, 500)) * ONE6; if (amount === 0n) amount = 1n; const clamped = amount > owed ? owed : amount; const willCapRevert = clamped > remaining; // cap applies to the CLAMPED payout const willSucceed = ready && !willCapRevert && owed > 0n && tracked >= clamped; const beforeFund = await debt.balanceOf(fundAddr); const beforeMarket = await debt.balanceOf(m.addr); const beforeDebt = owed; const info = await fund.markets(m.addr); const lastAt = info[3]; if (willSucceed) { const tx = await fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount); const rc = await tx.wait(); const blk = await ethers.provider.getBlock(rc.blockNumber); const afterFund = await debt.balanceOf(fundAddr); const afterMarket = await debt.balanceOf(m.addr); const afterDebt = await m.market.debtBalance(b.addr); const delta = beforeFund - afterFund; // bounded payout == exactly the clamped amount, nothing more. expect(delta, `[ifund PAYOUT] paid ${delta} != clamped ${clamped} ${ctx}`).to.equal(clamped); // market received exactly what the fund paid (no leakage / mint). expect(afterMarket - beforeMarket, `[ifund PAYOUT-MKT] market delta != paid ${ctx}`).to.equal(delta); // borrower debt fell by exactly the paid amount (G6 / G7 clamp). expect(beforeDebt - afterDebt, `[ifund PAYOUT-DEBT] debt drop != paid ${ctx} before=${beforeDebt} after=${afterDebt}`).to.equal(delta); // tracked mapping fell by the same amount (G2). const afterTracked = await fund.balances(debtAddr); expect(tracked - afterTracked, `[ifund PAYOUT-TRACK] tracked delta != paid ${ctx}`).to.equal(delta); // cap accounting. const info2 = await fund.markets(m.addr); expect(info2[2] - info[2], `[ifund PAYOUT-COVERED] covered delta != paid ${ctx}`).to.equal(delta); // G5 cooldown spacing: timestamp advanced to now, and if a prior // cover existed the gap is >= cooldown. expect(info2[3], `[ifund COOLDOWN-STAMP] lastCoverageAt != block time ${ctx}`).to.equal(BigInt(blk.timestamp)); if (lastAt !== 0n) { expect(BigInt(blk.timestamp) - lastAt, `[ifund G5] cover inside cooldown window ${ctx}`).to.be.gte(cooldown); } // borrower value did NOT increase (G6). Ratchet the high-water down. const bl = ledger.borrowers.find((x) => x.addr === b.addr && x.market === b.market); expect(afterDebt, `[ifund G6] debt increased on cover ${ctx}`).to.be.lte(bl.debt); bl.debt = afterDebt; ledger.covered += delta; m.covered += delta; covers++; } else { // Expected to revert. Confirm it does AND that no state moved. let reverted = false; try { await (await fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount)).wait(); } catch (e) { reverted = true; if (/CapExceeded/.test(e.message)) capReverts++; else if (/CooldownActive/.test(e.message)) cooldownReverts++; else if (/NoDebtToCover/.test(e.message)) noDebtReverts++; else if (/InsufficientBalance/.test(e.message)) insufReverts++; } expect(reverted, `[ifund PREDICT] expected cover revert but it succeeded ${ctx} amount=${amount} owed=${owed} remaining=${remaining} ready=${ready} tracked=${tracked}`).to.equal(true); // state unchanged. expect(await debt.balanceOf(fundAddr), `[ifund NOMOVE-fund] ${ctx}`).to.equal(beforeFund); expect((await fund.markets(m.addr))[2], `[ifund NOMOVE-covered] ${ctx}`).to.equal(info[2]); } } else if (action === "coverAttacker") { // Payout path is Foundation-only regardless of state. const m = pick(rng, ledger.markets); const b = pick(rng, ledger.borrowers.filter((x) => x.market === m.market)); await expect( fund.connect(pick(rng, attackers)).coverBadDebt(m.addr, b.addr, 1n * ONE6), `[ifund G8] non-foundation cover succeeded ${ctx}` ).to.be.revertedWithCustomError(fund, "NotFoundation"); foundationReverts++; } else if (action === "coverNoDebt") { // Foundation covering an address with zero debt must revert -- but the // contract checks cooldown BEFORE the debt check, so only assert // NoDebtToCover when the cooldown is actually clear. const m = pick(rng, ledger.markets); if (await fund.cooldownReady(m.addr)) { const remaining = await fund.coverageRemaining(m.addr); if (remaining > 0n) { await expect( fund.connect(foundation).coverBadDebt(m.addr, attackers[0].address, 1n), `[ifund G7] no-debt cover not rejected ${ctx}` ).to.be.revertedWithCustomError(fund, "NoDebtToCover"); noDebtReverts++; } } } else if (action === "coverExceedCap") { // Post-clamp cap semantics: CapExceeded fires only when the amount // that would ACTUALLY be paid (min(request, owed)) exceeds the // remaining cap. Send more than owed so the clamp binds, and only // assert the revert when owed itself is over the remaining cap; if // owed <= remaining the correct behavior is a clamped SUCCESS, not a // revert, so we do not force one here. const m = pick(rng, ledger.markets); if (await fund.cooldownReady(m.addr)) { const remaining = await fund.coverageRemaining(m.addr); const b = pick(rng, ledger.borrowers.filter((x) => x.market === m.market)); const owed = await m.market.debtBalance(b.addr); if (owed > remaining) { const amount = owed + 1n; // clamps to owed, which is over remaining const beforeFund = await debt.balanceOf(fundAddr); await expect( fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount), `[ifund G3] over-cap cover not rejected ${ctx} remaining=${remaining} owed=${owed}` ).to.be.revertedWithCustomError(fund, "CapExceeded"); expect(await debt.balanceOf(fundAddr), `[ifund G3 NOMOVE] ${ctx}`).to.equal(beforeFund); capReverts++; } } } } catch (e) { if (/\[ifund /.test(String(e.message))) throw e; // real invariant break // Otherwise an incidental revert on an edge amount is legal; fall through. } await assertInvariants(ctx, fund, debt, ledger); totalSteps++; } // Finale: verify the ledger closes out consistently against ground truth. await assertInvariants(`c${c} finale`, fund, debt, ledger); } console.log( ` [ifund main] steps=${totalSteps} covers=${covers} donations=${donations} strays=${strays} ` + `capRev=${capReverts} cooldownRev=${cooldownReverts} noDebtRev=${noDebtReverts} insufRev=${insufReverts} foundationRev=${foundationReverts}` ); expect(totalSteps).to.be.greaterThan(N.CAMPAIGNS * N.STEPS - 1); expect(covers, "no successful coverages exercised").to.be.greaterThan(10); expect(capReverts + cooldownReverts + noDebtReverts, "guard reverts never exercised").to.be.greaterThan(3); expect(foundationReverts, "access-control path never exercised").to.be.greaterThan(3); }); // ------------------------------------------------------------------------- // Production-parameter campaign: the LIVE fund runs a 7-day cooldown. Prove the // exact "cannot exceed cap within a cooldown window" bound holds at that value: // after one cover, a second is impossible for 7 days, and cap is a hard lifetime // ceiling even across many windows. it("production 7-day cooldown: one cover per window, cap is a hard lifetime ceiling", async function () { const rng = rngFor("prod", 0); const deployer = signers[0]; const foundation = signers[1]; const supplier = signers[2]; const liquidator = signers[3]; const donor = signers[4]; const borrowers = [signers[9], signers[10]]; const infra = await deployInfra(deployer); const debt = infra.debt; const debtAddr = await debt.getAddress(); const SEVEN_DAYS = 7n * 86400n; const fund = await FundF.deploy(foundation.address, SEVEN_DAYS); await fund.waitForDeployment(); const fundAddr = await fund.getAddress(); assertNoEscapeHatch(fund); const mk = await buildMarketWithBadDebt(infra, deployer, supplier, liquidator, borrowers); const cap = 300n * ONE6; // small lifetime cap await (await fund.connect(foundation).proposeMarket(mk.marketAddr, cap)).wait(); await time.increase(REG_DELAY); await network.provider.send("evm_mine"); await (await fund.connect(foundation).registerMarket(mk.marketAddr)).wait(); await (await debt.mint(donor.address, 100_000n * ONE6)).wait(); await (await debt.connect(donor).approve(fundAddr, ethers.MaxUint256)).wait(); await (await fund.connect(donor).donate(debtAddr, 100_000n * ONE6)).wait(); let covered = 0n, windows = 0; // Cover 50 USDC.e per window; cap is 300 so exactly 6 windows then hard stop. for (let w = 0; w < 10; w++) { const remaining = await fund.coverageRemaining(mk.marketAddr); const b = borrowers[w % borrowers.length].address; const owed = await mk.market.debtBalance(b); const amount = 50n * ONE6; if (remaining === 0n) { await expect( fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount), `[ifund PROD-CAP] cover after cap exhausted (w=${w})` ).to.be.revertedWithCustomError(fund, "CapExceeded"); continue; } expect(owed, `[ifund PROD] borrower unexpectedly debt-free w=${w}`).to.be.gt(0n); const before = await debt.balanceOf(fundAddr); await (await fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount)).wait(); const delta = before - (await debt.balanceOf(fundAddr)); expect(delta, `[ifund PROD] paid != 50 w=${w}`).to.equal(amount); covered += delta; windows++; // Immediately trying again (same block-ish, within 7 days) must revert. await expect( fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount), `[ifund G5 PROD] second cover inside 7-day window succeeded (w=${w})` ).to.be.revertedWithCustomError(fund, "CooldownActive"); // Advance just under 7 days: still locked. await time.increase(Number(SEVEN_DAYS) - 60); await network.provider.send("evm_mine"); if ((await fund.coverageRemaining(mk.marketAddr)) > 0n) { await expect( fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount), `[ifund G5 PROD] cover at T-60s succeeded (w=${w})` ).to.be.revertedWithCustomError(fund, "CooldownActive"); } // Cross the boundary. await time.increase(120); await network.provider.send("evm_mine"); } // Exactly cap/50 = 6 successful windows, covered == cap, and the mapping / // real balance agree. expect(windows, "[ifund PROD] wrong number of covered windows").to.equal(6); expect(covered, "[ifund PROD] total covered != cap").to.equal(cap); const info = await fund.markets(mk.marketAddr); expect(info[2], "[ifund PROD] on-chain covered != cap").to.equal(cap); expect(await fund.coverageRemaining(mk.marketAddr), "[ifund PROD] remaining not zero").to.equal(0n); expect(await debt.balanceOf(fundAddr), "[ifund PROD G1] balance mismatch") .to.equal(await fund.balances(debtAddr)); expect(100_000n * ONE6 - (await fund.balances(debtAddr)), "[ifund PROD G2] conservation") .to.equal(cap); console.log(` [ifund prod] 7-day windows covered=${windows} totalCovered=${ethers.formatUnits(covered, 6)} cap=${ethers.formatUnits(cap, 6)}`); }); });