Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
279 lines
14 KiB
JavaScript
279 lines
14 KiB
JavaScript
// 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");
|
|
});
|
|
});
|