// ============================================================================= // TDD proof for the AereInsuranceFund LIFETIME-CAP CHECK ORDER fix (LOW). // // BUG: the lifetime-cap check ran on the REQUESTED amount BEFORE the // clamp-to-owed step, so a legitimate coverage whose clamped amount is under // the remaining cap was spuriously blocked with CapExceeded. // // FIX: fetch owed + clamp `amount` to it FIRST, then run the cap check on the // ACTUAL (clamped) amount that will be paid. Cap semantics unchanged: total // lifetime coverage per market <= cap. // // Uses MockDebtMarketForFund so the debt balance is pinned exactly (the real // AereLendingMarket accrues per-second interest across the 7-day registration // timelock, which would blur the cap boundary this test targets). // // Run in ISOLATION: // npx hardhat test test/insurance-fund-cap-clamp-order-fix.test.js // // No em-dashes in this file. // ============================================================================= const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE6 = 10n ** 6n; describe("AereInsuranceFund - cap check applies to clamped amount (order fix)", function () { this.timeout(120000); async function deploy() { const [, foundation, bob, donor] = await ethers.getSigners(); const ERC20 = await ethers.getContractFactory("MockERC20Lending"); const debt = await ERC20.deploy("USDC.e", "USDC.e", 6); await debt.waitForDeployment(); const Mock = await ethers.getContractFactory("MockDebtMarketForFund"); const mkt = await Mock.deploy(await debt.getAddress()); await mkt.waitForDeployment(); const Fund = await ethers.getContractFactory("AereInsuranceFund"); const fund = await Fund.deploy(foundation.address, 0); // no cooldown await fund.waitForDeployment(); // Fund the pool with 1000 USDC.e. await debt.mint(donor.address, 1000n * ONE6); await debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await fund.connect(donor).donate(await debt.getAddress(), 1000n * ONE6); return { fund, mkt, debt, foundation, bob }; } async function register(fund, foundation, mkt, cap) { await fund.connect(foundation).proposeMarket(await mkt.getAddress(), cap); await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]); await ethers.provider.send("evm_mine", []); await fund.connect(foundation).registerMarket(await mkt.getAddress()); } it("request > owed but clamped amount < remaining cap now SUCCEEDS (pre-fix reverted CapExceeded)", async () => { const { fund, mkt, foundation, bob } = await deploy(); const owed = 10n * ONE6; await mkt.setDebt(bob.address, owed); // cap (20) > owed (10); request (100) > cap. const cap = 20n * ONE6; await register(fund, foundation, mkt, cap); const request = 100n * ONE6; // Pre-fix: cap check on the request (100 > 20) -> CapExceeded. // Post-fix: clamp to owed (10) <= cap (20) -> pays exactly 10. await expect( fund.connect(foundation).coverBadDebt(await mkt.getAddress(), bob.address, request) ).to.emit(fund, "BadDebtCovered").withArgs(await mkt.getAddress(), bob.address, owed); const m = await fund.markets(await mkt.getAddress()); expect(m.covered).to.equal(owed); expect(await mkt.debtBalance(bob.address)).to.equal(0n); }); it("cap semantics preserved: clamped amount OVER the cap still reverts CapExceeded", async () => { const { fund, mkt, foundation, bob } = await deploy(); const owed = 30n * ONE6; await mkt.setDebt(bob.address, owed); // cap (20) < owed (30): even after clamping to 30, 30 > 20 -> CapExceeded. const cap = 20n * ONE6; await register(fund, foundation, mkt, cap); await expect( fund.connect(foundation).coverBadDebt(await mkt.getAddress(), bob.address, 1000n * ONE6) ).to.be.revertedWithCustomError(fund, "CapExceeded"); }); it("cap accumulates across coverages on the clamped amounts", async () => { const { fund, mkt, foundation, bob } = await deploy(); const [, , , , carol] = await ethers.getSigners(); // cap 25; first cover clamps to 10, second to 12 -> total 22 <= 25 ok; // a third clamped 10 would push to 32 > 25 -> CapExceeded. const cap = 25n * ONE6; await register(fund, foundation, mkt, cap); await mkt.setDebt(bob.address, 10n * ONE6); await fund.connect(foundation).coverBadDebt(await mkt.getAddress(), bob.address, 1000n * ONE6); expect((await fund.markets(await mkt.getAddress())).covered).to.equal(10n * ONE6); await mkt.setDebt(carol.address, 12n * ONE6); await fund.connect(foundation).coverBadDebt(await mkt.getAddress(), carol.address, 1000n * ONE6); expect((await fund.markets(await mkt.getAddress())).covered).to.equal(22n * ONE6); await mkt.setDebt(bob.address, 10n * ONE6); await expect( fund.connect(foundation).coverBadDebt(await mkt.getAddress(), bob.address, 1000n * ONE6) ).to.be.revertedWithCustomError(fund, "CapExceeded"); }); });