aere-contracts/test/rwa-transfer-adapter.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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");
});
});