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.
328 lines
15 KiB
JavaScript
328 lines
15 KiB
JavaScript
// AereLendingMarket + AereLendingOracle tests.
|
|
// Updated after H1/H2/H3/H5 fixes: scaled-principal accounting + supplyIndex
|
|
// + insolvent-liquidation clamp + oracle auto-poke + owner-only poke.
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ONE6 = 10n ** 6n;
|
|
const ONE18 = 10n ** 18n;
|
|
const RAY = 10n ** 27n;
|
|
|
|
async function deployStack(opts = {}) {
|
|
const ltvBps = opts.ltvBps ?? 9000;
|
|
const liqThresholdBps = opts.liqThresholdBps ?? 9200;
|
|
const liqBonusBps = opts.liqBonusBps ?? 100;
|
|
const borrowFeeBps = opts.borrowFeeBps ?? 1000; // 10% of interest by default — sharp for visible test math
|
|
|
|
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); // $1
|
|
await aggC.waitForDeployment();
|
|
const aggD = await Agg.deploy(1_00000000); // $1
|
|
await aggD.waitForDeployment();
|
|
|
|
const Oracle = await ethers.getContractFactory("AereLendingOracle");
|
|
const oracle = await Oracle.deploy();
|
|
await oracle.waitForDeployment();
|
|
// configureFeed AUTO-POKES now — lastGoodPrice locked at config time.
|
|
// Deviation cap is configurable per test scenario (default 50% for liquidation tests
|
|
// that simulate big crash; tests that target deviation behaviour override explicitly).
|
|
const devBps = opts.devBps ?? 5000;
|
|
await oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 86400, devBps, 8);
|
|
await oracle.configureFeed(await debt.getAddress(), 0, await aggD.getAddress(), ethers.ZeroHash, 86400, devBps, 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,
|
|
liqThresholdBps,
|
|
liqBonusBps,
|
|
borrowFeeBps,
|
|
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, aggD, sink };
|
|
}
|
|
|
|
async function advance(seconds) {
|
|
await ethers.provider.send("evm_increaseTime", [seconds]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
}
|
|
|
|
describe("AereLendingMarket — accounting (H1/H2 fixes)", function () {
|
|
it("supplier earns yield via supplyIndex (H2 fix)", async function () {
|
|
const { collateral, debt, market } = await deployStack();
|
|
const [, alice, bob] = await ethers.getSigners();
|
|
|
|
// Alice supplies 100,000 USDC.e.
|
|
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);
|
|
const aliceBefore = await market.supplierBalance(alice.address);
|
|
expect(aliceBefore).to.equal(100_000n * ONE6);
|
|
|
|
// Bob borrows 50,000 against 100,000 BUIDL.e collateral.
|
|
await collateral.mint(bob.address, 100_000n * ONE18);
|
|
await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
|
|
await market.connect(bob).depositCollateral(100_000n * ONE18);
|
|
await market.connect(bob).borrow(50_000n * ONE6);
|
|
|
|
// Advance 1 year.
|
|
await advance(365 * 24 * 3600);
|
|
await market.accrueInterest();
|
|
|
|
// Alice's real balance now > principal.
|
|
const aliceAfter = await market.supplierBalance(alice.address);
|
|
expect(aliceAfter).to.be.gt(aliceBefore);
|
|
// ~5% APY * 50% utilisation * 90% net (10% to sink) ≈ 2.25% on full supply
|
|
// Sanity floor: at least 1% growth.
|
|
expect(aliceAfter - aliceBefore).to.be.gt(1000n * ONE6);
|
|
});
|
|
|
|
it("sink fee accumulates as pendingSinkFee (H2 fix) + flushable", async function () {
|
|
const { collateral, debt, market } = await deployStack({ borrowFeeBps: 2000 });
|
|
const [, alice, bob] = 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, 100_000n * ONE18);
|
|
await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
|
|
await market.connect(bob).depositCollateral(100_000n * ONE18);
|
|
await market.connect(bob).borrow(50_000n * ONE6);
|
|
await advance(365 * 24 * 3600);
|
|
await market.accrueInterest();
|
|
|
|
const pending = await market.pendingSinkFee();
|
|
expect(pending).to.be.gt(0n);
|
|
|
|
// flushSinkFee zeroes the pending.
|
|
await market.flushSinkFee();
|
|
expect(await market.pendingSinkFee()).to.equal(0n);
|
|
});
|
|
|
|
it("totalBorrowsCurrent grows with interest; cap still enforced (H1 fix)", async function () {
|
|
const { collateral, debt, market } = await deployStack();
|
|
const [, alice, bob] = await ethers.getSigners();
|
|
await debt.mint(alice.address, 500_000n * ONE6);
|
|
await debt.connect(alice).approve(await market.getAddress(), 500_000n * ONE6);
|
|
await market.connect(alice).supply(500_000n * ONE6);
|
|
await collateral.mint(bob.address, 100_000n * ONE18);
|
|
await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
|
|
await market.connect(bob).depositCollateral(100_000n * ONE18);
|
|
await market.connect(bob).borrow(50_000n * ONE6);
|
|
|
|
const t0 = await market.totalBorrowsCurrent();
|
|
expect(t0).to.equal(50_000n * ONE6);
|
|
await advance(180 * 24 * 3600);
|
|
await market.accrueInterest();
|
|
const t1 = await market.totalBorrowsCurrent();
|
|
expect(t1).to.be.gt(t0);
|
|
});
|
|
|
|
it("repay correctly reduces scaled debt; no underflow on totalBorrows", async function () {
|
|
const { collateral, debt, market } = await deployStack();
|
|
const [, alice, bob] = 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, 100_000n * ONE18);
|
|
await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
|
|
await market.connect(bob).depositCollateral(100_000n * ONE18);
|
|
await market.connect(bob).borrow(50_000n * ONE6);
|
|
|
|
await advance(30 * 24 * 3600);
|
|
await market.accrueInterest();
|
|
|
|
const owed = await market.debtBalance(bob.address);
|
|
expect(owed).to.be.gt(50_000n * ONE6);
|
|
// Overpay slightly so that even if interest accrues between view + tx,
|
|
// the contract clamps `pay` to the exact `debtReal` and fully zeros state.
|
|
const overpay = owed + 1000n * ONE6;
|
|
await debt.mint(bob.address, overpay);
|
|
await debt.connect(bob).approve(await market.getAddress(), overpay);
|
|
await market.connect(bob).repay(overpay);
|
|
expect(await market.debtBalance(bob.address)).to.equal(0n);
|
|
expect(await market.totalBorrowsScaled()).to.equal(0n);
|
|
});
|
|
});
|
|
|
|
describe("AereLendingMarket — liquidation (H3 fix)", function () {
|
|
it("partial liquidation routes correctly", async function () {
|
|
const { collateral, debt, market, aggC } = await deployStack({
|
|
ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500,
|
|
});
|
|
const [, alice, bob, keeper] = 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, 1000n * ONE18);
|
|
await collateral.connect(bob).approve(await market.getAddress(), 1000n * ONE18);
|
|
await market.connect(bob).depositCollateral(1000n * ONE18);
|
|
await market.connect(bob).borrow(700n * ONE6);
|
|
|
|
await aggC.set(80000000); // $0.80 — liquidatable
|
|
await market.accrueInterest();
|
|
|
|
await debt.mint(keeper.address, 500n * ONE6);
|
|
await debt.connect(keeper).approve(await market.getAddress(), 500n * ONE6);
|
|
await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
|
|
.to.emit(market, "Liquidated");
|
|
});
|
|
|
|
it("insolvent position: clamps pay AND emits BadDebt (H3 fix)", async function () {
|
|
const { collateral, debt, market, aggC } = await deployStack({
|
|
ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500,
|
|
devBps: 0, // disable deviation cap so the simulated crash works
|
|
});
|
|
const [, alice, bob, keeper] = 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(60n * ONE6); // 60% LTV at $1/$1 — safe
|
|
|
|
// Collateral crashes to $0.30 — collateral worth $30, debt $60 → deeply insolvent.
|
|
await aggC.set(30000000);
|
|
await market.accrueInterest();
|
|
|
|
await debt.mint(keeper.address, 200n * ONE6);
|
|
await debt.connect(keeper).approve(await market.getAddress(), 200n * ONE6);
|
|
|
|
const keeperBefore = await debt.balanceOf(keeper.address);
|
|
const colBefore = await collateral.balanceOf(keeper.address);
|
|
await expect(market.connect(keeper).liquidate(bob.address, 60n * ONE6))
|
|
.to.emit(market, "BadDebt");
|
|
|
|
const keeperAfter = await debt.balanceOf(keeper.address);
|
|
const colAfter = await collateral.balanceOf(keeper.address);
|
|
const paid = keeperBefore - keeperAfter;
|
|
const received = colAfter - colBefore;
|
|
// Liquidator received some collateral and paid LESS than requested 60.
|
|
expect(received).to.be.gt(0n);
|
|
expect(paid).to.be.lt(60n * ONE6);
|
|
// Bob's collateral is now zero; debt residual remains.
|
|
expect(await market.collateralOf(bob.address)).to.equal(0n);
|
|
expect(await market.debtBalance(bob.address)).to.be.gt(0n);
|
|
});
|
|
});
|
|
|
|
describe("AereLendingMarket — LIQUIDATIONS NEVER PAUSE (oracle-freeze fix, 2026-07-12)", function () {
|
|
// Adversarial-review HIGH: getPrice()'s pause/deviation/staleness reverts froze
|
|
// liquidate(), contradicting the "LIQUIDATIONS NEVER PAUSE" invariant. Fix: the
|
|
// liquidation path reads getPriceForLiquidation() which never freezes.
|
|
async function liquidatablePosition() {
|
|
// 10% deviation cap so a 30% crash trips getPrice's deviation revert.
|
|
const s = await deployStack({ ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, devBps: 1000 });
|
|
const [, alice, bob, keeper] = await ethers.getSigners();
|
|
await s.debt.mint(alice.address, 100_000n * ONE6);
|
|
await s.debt.connect(alice).approve(await s.market.getAddress(), 100_000n * ONE6);
|
|
await s.market.connect(alice).supply(100_000n * ONE6);
|
|
await s.collateral.mint(bob.address, 1000n * ONE18);
|
|
await s.collateral.connect(bob).approve(await s.market.getAddress(), 1000n * ONE18);
|
|
await s.market.connect(bob).depositCollateral(1000n * ONE18);
|
|
await s.market.connect(bob).borrow(700n * ONE6);
|
|
await s.debt.mint(keeper.address, 500n * ONE6);
|
|
await s.debt.connect(keeper).approve(await s.market.getAddress(), 500n * ONE6);
|
|
return { ...s, alice, bob, keeper };
|
|
}
|
|
|
|
it("deviation-cap breach: getPrice() reverts, but liquidate() still succeeds", async function () {
|
|
const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
|
|
await aggC.set(70000000); // $0.70, a 30% crash > the 10% deviation cap
|
|
await market.accrueInterest();
|
|
await expect(oracle.getPrice(await collateral.getAddress()))
|
|
.to.be.revertedWithCustomError(oracle, "FeedDeviationExceeded");
|
|
await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
|
|
.to.emit(market, "Liquidated");
|
|
});
|
|
|
|
it("paused feed: getPrice() reverts, but liquidate() still succeeds", async function () {
|
|
const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
|
|
await aggC.set(70000000);
|
|
await market.accrueInterest();
|
|
await oracle.setFeedPaused(await collateral.getAddress(), true);
|
|
await expect(oracle.getPrice(await collateral.getAddress()))
|
|
.to.be.revertedWithCustomError(oracle, "FeedPausedErr");
|
|
await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
|
|
.to.emit(market, "Liquidated");
|
|
});
|
|
|
|
it("stale feed: getPrice() reverts, but liquidate() still succeeds", async function () {
|
|
const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
|
|
await aggC.setStale(70000000, 1); // crash reading, ancient timestamp
|
|
await market.accrueInterest();
|
|
await expect(oracle.getPrice(await collateral.getAddress()))
|
|
.to.be.revertedWithCustomError(oracle, "FeedStale");
|
|
await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
|
|
.to.emit(market, "Liquidated");
|
|
});
|
|
});
|
|
|
|
describe("AereLendingOracle — H5 fix", function () {
|
|
it("configureFeed auto-pokes lastGoodPrice (closes deviation-skip exploit)", async function () {
|
|
const [deployer] = await ethers.getSigners();
|
|
const Oracle = await ethers.getContractFactory("AereLendingOracle");
|
|
const oracle = await Oracle.deploy();
|
|
await oracle.waitForDeployment();
|
|
const Agg = await ethers.getContractFactory("MockAggregator");
|
|
const agg = await Agg.deploy(1_00000000);
|
|
await agg.waitForDeployment();
|
|
const ERC20 = await ethers.getContractFactory("MockERC20Lending");
|
|
const t = await ERC20.deploy("X", "X", 18);
|
|
await t.waitForDeployment();
|
|
|
|
await oracle.configureFeed(await t.getAddress(), 0, await agg.getAddress(), ethers.ZeroHash, 86400, 1000, 8);
|
|
const feed = await oracle.feeds(await t.getAddress());
|
|
expect(feed.lastGoodPrice).to.equal(10n ** 18n);
|
|
});
|
|
|
|
it("poke is owner-only", async function () {
|
|
const { oracle, collateral } = await deployStack();
|
|
const [, alice] = await ethers.getSigners();
|
|
await expect(oracle.connect(alice).poke(await collateral.getAddress()))
|
|
.to.be.revertedWith("Ownable: caller is not the owner");
|
|
});
|
|
|
|
it("pokeBounded permissionless but rejects out-of-band moves", async function () {
|
|
const { oracle, collateral, aggC } = await deployStack({ devBps: 1000 }); // 10% cap
|
|
const [, alice] = await ethers.getSigners();
|
|
await aggC.set(105_000_000); // $1.05 — within 10% deviation
|
|
await oracle.connect(alice).pokeBounded(await collateral.getAddress());
|
|
const feed = await oracle.feeds(await collateral.getAddress());
|
|
expect(feed.lastGoodPrice).to.equal(105n * 10n ** 16n);
|
|
|
|
// Now $1.50 — outside 10% cap from $1.05 reference.
|
|
await aggC.set(150_000_000);
|
|
await expect(oracle.connect(alice).pokeBounded(await collateral.getAddress()))
|
|
.to.be.revertedWithCustomError(oracle, "FeedDeviationExceeded");
|
|
});
|
|
});
|