// AereInsuranceFund + repayOnBehalfOf tests. const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE6 = 10n ** 6n; const ONE18 = 10n ** 18n; async function deployStack(opts = {}) { const ERC20 = await ethers.getContractFactory("MockERC20Lending"); const collateral = await ERC20.deploy("BUIDL.e", "BUIDL.e", 18); await collateral.waitForDeployment(); const debt = await ERC20.deploy("USDC.e", "USDC.e", 6); await debt.waitForDeployment(); const Agg = await ethers.getContractFactory("MockAggregator"); const aggC = await Agg.deploy(1_00000000); await aggC.waitForDeployment(); const aggD = await Agg.deploy(1_00000000); await aggD.waitForDeployment(); const Oracle = await ethers.getContractFactory("AereLendingOracle"); const oracle = await Oracle.deploy(); await oracle.waitForDeployment(); // 30d staleness so 7-day insurance-fund timelock doesn't trip FeedStale. await oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 30 * 86400, opts.devBps ?? 5000, 8); await oracle.configureFeed(await debt.getAddress(), 0, await aggD.getAddress(), ethers.ZeroHash, 30 * 86400, opts.devBps ?? 5000, 8); const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy(); await WAERE.waitForDeployment(); const burn = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy(); await burn.waitForDeployment(); const sink = await (await ethers.getContractFactory("AereSink")).deploy( await WAERE.getAddress(), await burn.getAddress(), await WAERE.getAddress(), await WAERE.getAddress(), 1500, 4000, 4500, 200, ethers.ZeroAddress ); await sink.waitForDeployment(); const cfg = { collateral: await collateral.getAddress(), debtToken: await debt.getAddress(), sink: await sink.getAddress(), oracle: await oracle.getAddress(), ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, borrowFeeBps: 100, marketDebtCap: 1_000_000n * ONE6, collateralDecimals: 18, debtDecimals: 6, }; const Market = await ethers.getContractFactory("AereLendingMarket"); const market = await Market.deploy(cfg); await market.waitForDeployment(); return { collateral, debt, oracle, market, aggC, sink }; } describe("AereLendingMarket.repayOnBehalfOf", function () { it("permissionless: anyone can repay anyone else's debt", async function () { const { collateral, debt, market } = await deployStack(); const [, alice, bob, samaritan] = await ethers.getSigners(); await debt.mint(alice.address, 100_000n * ONE6); await debt.connect(alice).approve(await market.getAddress(), 100_000n * ONE6); await market.connect(alice).supply(100_000n * ONE6); await collateral.mint(bob.address, 100n * ONE18); await collateral.connect(bob).approve(await market.getAddress(), 100n * ONE18); await market.connect(bob).depositCollateral(100n * ONE18); await market.connect(bob).borrow(50n * ONE6); expect(await market.debtBalance(bob.address)).to.equal(50n * ONE6); await debt.mint(samaritan.address, 50n * ONE6); await debt.connect(samaritan).approve(await market.getAddress(), 50n * ONE6); await market.connect(samaritan).repayOnBehalfOf(bob.address, 50n * ONE6); expect(await market.debtBalance(bob.address)).to.equal(0n); }); }); describe("AereInsuranceFund", function () { async function deployFund(opts = {}) { const [, foundation] = await ethers.getSigners(); const Fund = await ethers.getContractFactory("AereInsuranceFund"); const fund = await Fund.deploy(foundation.address, opts.cooldown ?? 3600); await fund.waitForDeployment(); return { fund, foundation }; } /// Helper: propose + warp 7 days + execute registerMarket. async function registerWithTimelock(fund, foundation, marketAddr, cap) { await fund.connect(foundation).proposeMarket(marketAddr, cap); await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]); await ethers.provider.send("evm_mine", []); await fund.connect(foundation).registerMarket(marketAddr); } it("permissionless donate increases balance + emits Donated", async function () { const { debt } = await deployStack(); const { fund } = await deployFund(); const [, , donor] = await ethers.getSigners(); await debt.mint(donor.address, 1000n * ONE6); await debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await expect(fund.connect(donor).donate(await debt.getAddress(), 1000n * ONE6)) .to.emit(fund, "Donated"); expect(await fund.balances(await debt.getAddress())).to.equal(1000n * ONE6); }); it("only Foundation can proposeMarket + reverts on double-propose-after-register", async function () { const env = await deployStack(); const { fund, foundation } = await deployFund(); const [, , alice] = await ethers.getSigners(); const marketAddr = await env.market.getAddress(); // Non-foundation cannot propose. await expect(fund.connect(alice).proposeMarket(marketAddr, 10_000n * ONE6)) .to.be.revertedWithCustomError(fund, "NotFoundation"); // Foundation proposes. await fund.connect(foundation).proposeMarket(marketAddr, 10_000n * ONE6); // Cannot execute before timelock. await expect(fund.connect(foundation).registerMarket(marketAddr)) .to.be.revertedWithCustomError(fund, "TimelockNotElapsed"); // Advance 7 days + 1s. await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]); await ethers.provider.send("evm_mine", []); // Anyone may execute the pending registration. await fund.connect(alice).registerMarket(marketAddr); // Now AlreadyRegistered for any further proposal. await expect(fund.connect(foundation).proposeMarket(marketAddr, 1n)) .to.be.revertedWithCustomError(fund, "AlreadyRegistered"); }); it("H4 fix: anyone can cancelPendingMarket during 7-day delay", async function () { const env = await deployStack(); const { fund, foundation } = await deployFund(); const [, , alice] = await ethers.getSigners(); const marketAddr = await env.market.getAddress(); await fund.connect(foundation).proposeMarket(marketAddr, 10_000n * ONE6); // Alice (community observer) detects this looks fishy + cancels. await fund.connect(alice).cancelPendingMarket(marketAddr, "suspicious admin powers"); await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]); await ethers.provider.send("evm_mine", []); // Even after timelock, the cancelled registration cannot be executed. await expect(fund.connect(foundation).registerMarket(marketAddr)) .to.be.revertedWithCustomError(fund, "NoPendingRegistration"); }); it("Foundation covers BadDebt via market.repayOnBehalfOf", async function () { const env = await deployStack({ devBps: 0 }); const { fund, foundation } = await deployFund({ cooldown: 0 }); const [, , alice, bob, keeper, donor] = await ethers.getSigners(); // Set up bad debt: bob borrows, price crashes, keeper liquidates insolvent. await env.debt.mint(alice.address, 100_000n * ONE6); await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6); await env.market.connect(alice).supply(100_000n * ONE6); await env.collateral.mint(bob.address, 100n * ONE18); await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18); await env.market.connect(bob).depositCollateral(100n * ONE18); await env.market.connect(bob).borrow(60n * ONE6); await env.aggC.set(30_000_000); // $0.30 await env.market.accrueInterest(); await env.debt.mint(keeper.address, 200n * ONE6); await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6); await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6); const remaining = await env.market.debtBalance(bob.address); expect(remaining).to.be.gt(0n); // Fund the insurance pool + register the market. await env.debt.mint(donor.address, 1000n * ONE6); await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6); await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6); // Force an accrual + read post-accrual to lock in the current debt // for this same block. Then pass exactly that to coverBadDebt — same // block means no extra interest accrues between view and tx. await env.market.accrueInterest(); const remainingNow = await env.market.debtBalance(bob.address); await expect(fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, remainingNow)) .to.emit(fund, "BadDebtCovered") .to.emit(env.market, "Repay"); // Allow tiny dust (sub-cent) from per-second interest between accrue + cover. const final = await env.market.debtBalance(bob.address); expect(final).to.be.lt(1000n); }); it("rejects non-Foundation caller on coverBadDebt", async function () { const env = await deployStack(); const { fund } = await deployFund(); const [, , alice] = await ethers.getSigners(); await expect(fund.connect(alice).coverBadDebt(await env.market.getAddress(), alice.address, 1n)) .to.be.revertedWithCustomError(fund, "NotFoundation"); }); it("enforces lifetime cap", async function () { const env = await deployStack({ devBps: 0 }); const { fund, foundation } = await deployFund({ cooldown: 0 }); const [, , alice, bob, keeper, donor] = await ethers.getSigners(); // Bad debt setup (shortened). await env.debt.mint(alice.address, 100_000n * ONE6); await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6); await env.market.connect(alice).supply(100_000n * ONE6); await env.collateral.mint(bob.address, 100n * ONE18); await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18); await env.market.connect(bob).depositCollateral(100n * ONE18); await env.market.connect(bob).borrow(60n * ONE6); await env.aggC.set(30_000_000); await env.market.accrueInterest(); await env.debt.mint(keeper.address, 200n * ONE6); await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6); await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6); await env.debt.mint(donor.address, 1000n * ONE6); await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6); // Cap = 5 USDC.e — tiny. await registerWithTimelock(fund, foundation, await env.market.getAddress(), 5n * ONE6); const remaining = await env.market.debtBalance(bob.address); await expect(fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, remaining)) .to.be.revertedWithCustomError(fund, "CapExceeded"); }); it("enforces cooldown between consecutive coverages", async function () { const env = await deployStack({ devBps: 0 }); const { fund, foundation } = await deployFund({ cooldown: 3600 }); const [, , alice, bob, keeper, donor] = await ethers.getSigners(); await env.debt.mint(alice.address, 100_000n * ONE6); await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6); await env.market.connect(alice).supply(100_000n * ONE6); await env.collateral.mint(bob.address, 100n * ONE18); await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18); await env.market.connect(bob).depositCollateral(100n * ONE18); await env.market.connect(bob).borrow(60n * ONE6); await env.aggC.set(30_000_000); await env.market.accrueInterest(); await env.debt.mint(keeper.address, 200n * ONE6); await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6); await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6); await env.debt.mint(donor.address, 1000n * ONE6); await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6); await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6); const remaining = await env.market.debtBalance(bob.address); const firstPay = remaining / 2n; // First cover bob's bad debt (real outstanding). await fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, firstPay); // Setup another bad-debt borrower to attempt cover during cooldown. const [, , , , , , carol] = await ethers.getSigners(); await env.collateral.mint(carol.address, 100n * ONE18); await env.collateral.connect(carol).approve(await env.market.getAddress(), 100n * ONE18); await env.market.connect(carol).depositCollateral(100n * ONE18); // Borrow before price recovers? We've already crashed price to $0.30. // Carol's borrow capacity at $0.30 = 100 * $0.30 * 70% = $21. Borrow $20. await env.market.connect(carol).borrow(20n * ONE6); await expect( fund.connect(foundation).coverBadDebt(await env.market.getAddress(), carol.address, 1n * ONE6) ).to.be.revertedWithCustomError(fund, "CooldownActive"); }); it("no admin withdrawal — funds are sticky", async function () { const { fund, foundation } = await deployFund(); // No `withdraw` function exists on the contract — proven by the absence. // Just verify the Foundation has no escape hatch. expect(fund.interface.getFunction("withdraw")).to.equal(null); }); it("rejects coverage of borrower with no debt", async function () { const env = await deployStack(); const { fund, foundation } = await deployFund({ cooldown: 0 }); const [, , alice, , , donor] = await ethers.getSigners(); await env.debt.mint(donor.address, 1000n * ONE6); await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6); await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6); await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6); await expect( fund.connect(foundation).coverBadDebt(await env.market.getAddress(), alice.address, 1n) ).to.be.revertedWithCustomError(fund, "NoDebtToCover"); }); });