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.
182 lines
9.6 KiB
JavaScript
182 lines
9.6 KiB
JavaScript
// RWATransferAdapter tests.
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ONE6 = 10n ** 6n;
|
|
const ONE18 = 10n ** 18n;
|
|
|
|
async function deployStack(opts = {}) {
|
|
const [deployer, foundation, alice, liquidator] = await ethers.getSigners();
|
|
|
|
const ERC20 = await ethers.getContractFactory("MockERC20Lending");
|
|
const buidl = await ERC20.deploy("BUIDL.e", "BUIDL.e", 18);
|
|
await buidl.waitForDeployment();
|
|
const usdce = await ERC20.deploy("USDC.e", "USDC.e", 6);
|
|
await usdce.waitForDeployment();
|
|
|
|
// Real oracle infra.
|
|
const Agg = await ethers.getContractFactory("MockAggregator");
|
|
const aggBuidl = await Agg.deploy(1_00000000); // $1.00
|
|
await aggBuidl.waitForDeployment();
|
|
const aggUsdc = await Agg.deploy(1_00000000);
|
|
await aggUsdc.waitForDeployment();
|
|
const Oracle = await ethers.getContractFactory("AereLendingOracle");
|
|
const oracle = await Oracle.deploy();
|
|
await oracle.waitForDeployment();
|
|
await oracle.configureFeed(await buidl.getAddress(), 0, await aggBuidl.getAddress(), ethers.ZeroHash, 86400, opts.devBps ?? 5000, 8);
|
|
await oracle.configureFeed(await usdce.getAddress(), 0, await aggUsdc.getAddress(), ethers.ZeroHash, 86400, opts.devBps ?? 5000, 8);
|
|
|
|
const Adapter = await ethers.getContractFactory("RWATransferAdapter");
|
|
const adapter = await Adapter.deploy(await usdce.getAddress(), await oracle.getAddress(), foundation.address, 6);
|
|
await adapter.waitForDeployment();
|
|
|
|
return { deployer, foundation, alice, liquidator, buidl, usdce, oracle, adapter, aggBuidl };
|
|
}
|
|
|
|
describe("RWATransferAdapter", function () {
|
|
it("Foundation registers asset; rejects double-register + bad haircut", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
await expect(env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100n, 86400, 18
|
|
)).to.be.revertedWithCustomError(env.adapter, "AlreadyRegistered");
|
|
// Bad haircut > 50%.
|
|
await expect(env.adapter.connect(env.foundation).registerAsset(
|
|
await env.usdce.getAddress(), 6000, 100n, 86400, 6
|
|
)).to.be.revertedWithCustomError(env.adapter, "BadHaircut");
|
|
});
|
|
|
|
it("non-Foundation cannot register", async function () {
|
|
const env = await deployStack();
|
|
await expect(env.adapter.connect(env.alice).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100n, 86400, 18
|
|
)).to.be.revertedWithCustomError(env.adapter, "NotFoundation");
|
|
});
|
|
|
|
it("redeem: BUIDL.e → USDC.e at oracle price minus haircut", async function () {
|
|
const env = await deployStack();
|
|
// Register BUIDL with 1% haircut, $100k daily cap.
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
// Foundation seeds USDC.e treasury (would normally come from off-chain sale of swept RWA).
|
|
await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6);
|
|
|
|
// Liquidator has 100 BUIDL.e at $1 → $100 → 99 USDC.e after 1% haircut.
|
|
await env.buidl.mint(env.liquidator.address, 100n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18);
|
|
const before = await env.usdce.balanceOf(env.liquidator.address);
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 100n * ONE18);
|
|
const after = await env.usdce.balanceOf(env.liquidator.address);
|
|
expect(after - before).to.equal(99n * ONE6);
|
|
// Accumulated balance for sweep grows by 100 BUIDL.e.
|
|
const s = await env.adapter.states(await env.buidl.getAddress());
|
|
expect(s.totalAccumulated).to.equal(100n * ONE18);
|
|
});
|
|
|
|
it("rate limit enforced per period", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 50n * ONE6, 86400, 18 // $50/day cap
|
|
);
|
|
await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6);
|
|
await env.buidl.mint(env.liquidator.address, 100n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18);
|
|
|
|
// 50 BUIDL.e ≈ $49.5 after haircut → under cap, ok.
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18);
|
|
// Next 5 BUIDL ≈ $4.95 → 49.5 + 4.95 = 54.45 > 50 → rate limit.
|
|
await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 5n * ONE18))
|
|
.to.be.revertedWithCustomError(env.adapter, "RateLimitExceeded");
|
|
});
|
|
|
|
it("rate limit resets after period", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 50n * ONE6, 60, 18 // $50 / 60s cap
|
|
);
|
|
await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6);
|
|
await env.buidl.mint(env.liquidator.address, 100n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18);
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18);
|
|
// Advance time past period.
|
|
await ethers.provider.send("evm_increaseTime", [120]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18);
|
|
});
|
|
|
|
it("paused asset rejects redeem; can resume", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6);
|
|
await env.buidl.mint(env.liquidator.address, 100n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18);
|
|
|
|
await env.adapter.connect(env.foundation).setAssetPaused(await env.buidl.getAddress(), true);
|
|
await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18))
|
|
.to.be.revertedWithCustomError(env.adapter, "AssetIsPaused");
|
|
await env.adapter.connect(env.foundation).setAssetPaused(await env.buidl.getAddress(), false);
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18);
|
|
});
|
|
|
|
it("sweepRWA: Foundation can pull accumulated RWA to off-chain wallet", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6);
|
|
await env.buidl.mint(env.liquidator.address, 100n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18);
|
|
await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 100n * ONE18);
|
|
|
|
const [, , , , securitizeAllowlisted] = await ethers.getSigners();
|
|
await env.adapter.connect(env.foundation).sweepRWA(await env.buidl.getAddress(), 100n * ONE18, securitizeAllowlisted.address);
|
|
expect(await env.buidl.balanceOf(securitizeAllowlisted.address)).to.equal(100n * ONE18);
|
|
const s = await env.adapter.states(await env.buidl.getAddress());
|
|
expect(s.totalAccumulated).to.equal(0n);
|
|
});
|
|
|
|
it("sweepRWA: cannot pull more than accumulated", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
await env.usdce.mint(env.foundation.address, 100_000n * ONE6);
|
|
await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 100_000n * ONE6);
|
|
const [, , , , securitizeAllowlisted] = await ethers.getSigners();
|
|
await expect(env.adapter.connect(env.foundation).sweepRWA(await env.buidl.getAddress(), 1n, securitizeAllowlisted.address))
|
|
.to.be.revertedWithCustomError(env.adapter, "InsufficientSweepBalance");
|
|
});
|
|
|
|
it("Foundation cannot touch USDC.e treasury directly (no withdraw fn)", async function () {
|
|
const env = await deployStack();
|
|
expect(env.adapter.interface.getFunction("withdraw")).to.equal(null);
|
|
// sweepRWA on USDC.e itself reverts because it's not registered as an asset
|
|
const [, , , , recipient] = await ethers.getSigners();
|
|
await expect(env.adapter.connect(env.foundation).sweepRWA(await env.usdce.getAddress(), 1n, recipient.address))
|
|
.to.be.revertedWithCustomError(env.adapter, "UnknownAsset");
|
|
});
|
|
|
|
it("insufficient payout liquidity reverts cleanly", async function () {
|
|
const env = await deployStack();
|
|
await env.adapter.connect(env.foundation).registerAsset(
|
|
await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18
|
|
);
|
|
// No USDC.e seeded.
|
|
await env.buidl.mint(env.liquidator.address, 10n * ONE18);
|
|
await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 10n * ONE18);
|
|
await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18))
|
|
.to.be.revertedWithCustomError(env.adapter, "InsufficientPayoutLiquidity");
|
|
});
|
|
});
|