aere-contracts/test/settlement-hub.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

195 lines
8.6 KiB
JavaScript

// Hardhat tests for AereSettlementHub.
//
// Covers:
// - list asset with per-tx limit + restricted-recipient flag
// - deposit pulls full amount, routes 50bps to sink, parks the rest
// - claim requires allowed solver + sufficient bond
// - settle after 6h challenge window releases asset to solver
// - challenge inside window emits event; outside window reverts
// - per-tx limit enforcement
// - restricted recipient enforcement
// - bond slash routes amount to sink
//
// Run: npx hardhat test test/settlement-hub.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const ONE = 10n ** 18n;
const SIX = 10n ** 6n;
async function deployStack() {
const [deployer, alice, solver1, solver2, recipient] = await ethers.getSigners();
// WAERE + tiny AereSink fixture.
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
await WAERE.waitForDeployment();
// Mock USDC-like asset (6 decimals) for tests.
const TestUSDC = await ethers.getContractFactory("MockUSDC");
const usdc = await TestUSDC.deploy();
await usdc.waitForDeployment();
const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
await burnVault.waitForDeployment();
const fakeRouter = await (await ethers.getContractFactory("WAERE")).deploy();
await fakeRouter.waitForDeployment();
const nonce = await ethers.provider.getTransactionCount(deployer.address);
const futureSaere = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 2 });
await WAERE.connect(deployer).deposit({ value: 1000n });
await WAERE.connect(deployer).approve(futureSaere, 1000n);
const sAERE = await (await ethers.getContractFactory("sAERE")).deploy(await WAERE.getAddress());
await sAERE.waitForDeployment();
const sink = await (await ethers.getContractFactory("AereSink")).deploy(
await WAERE.getAddress(),
await burnVault.getAddress(),
await sAERE.getAddress(),
await fakeRouter.getAddress(),
1500, 4000, 4500, 200, ethers.ZeroAddress
);
await sink.waitForDeployment();
// Hub.
const Hub = await ethers.getContractFactory("AereSettlementHub");
const hub = await Hub.deploy(await sink.getAddress());
await hub.waitForDeployment();
return { deployer, alice, solver1, solver2, recipient, WAERE, usdc, sink, hub };
}
describe("AereSettlementHub", function () {
it("deposit pulls full amount and parks solverPortion", async function () {
const { deployer, alice, recipient, WAERE, hub } = await deployStack();
// List WAERE as an asset (use WAERE here because AereSink.flush handles it via fast path).
await hub.listAsset(await WAERE.getAddress(), 18, 0, 0, false);
const amount = 100n * ONE;
await WAERE.connect(alice).deposit({ value: amount });
await WAERE.connect(alice).approve(await hub.getAddress(), amount);
const intentId = ethers.id("intent1");
await hub.connect(alice).deposit(await WAERE.getAddress(), amount, intentId, recipient.address);
const fee = (amount * 50n) / 10_000n;
const solverPortion = amount - fee;
expect(await WAERE.balanceOf(await hub.getAddress())).to.equal(solverPortion);
const intent = await hub.intents(intentId);
expect(intent.amount).to.equal(solverPortion);
expect(intent.depositor).to.equal(alice.address);
expect(intent.recipient).to.equal(recipient.address);
expect(intent.open).to.equal(true);
});
it("claim requires allowed solver AND sufficient bond", async function () {
const { deployer, alice, solver1, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 50n * ONE, 0, false);
const amount = 100n * ONE;
await WAERE.connect(alice).deposit({ value: amount });
await WAERE.connect(alice).approve(await hub.getAddress(), amount);
const intentId = ethers.id("intent2");
await hub.connect(alice).deposit(await WAERE.getAddress(), amount, intentId, recipient.address);
// Solver not allowed → revert.
await expect(hub.connect(solver1).claim(intentId, "0x")).to.be.revertedWithCustomError(hub, "NotAllowedSolver");
// Foundation allows solver1.
await hub.connect(deployer).setSolverAllowed(await WAERE.getAddress(), solver1.address, true);
// Solver1 has no bond → revert InsufficientBond.
await expect(hub.connect(solver1).claim(intentId, "0x")).to.be.revertedWithCustomError(hub, "InsufficientBond");
// Post the bond.
const bondAmt = 50n * ONE;
await WAERE.connect(alice).deposit({ value: bondAmt });
await WAERE.connect(alice).approve(await hub.getAddress(), bondAmt);
await hub.connect(alice).depositSolverBond(await WAERE.getAddress(), solver1.address, bondAmt);
await hub.connect(solver1).claim(intentId, "0x" + "ab".repeat(8));
const intent = await hub.intents(intentId);
expect(intent.solver).to.equal(solver1.address);
expect(intent.claimedAt).to.not.equal(0);
});
it("settle after 6h window releases solverPortion", async function () {
const { deployer, alice, solver1, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 1n, 0, false);
await hub.connect(deployer).setSolverAllowed(await WAERE.getAddress(), solver1.address, true);
const amount = 50n * ONE;
await WAERE.connect(alice).deposit({ value: amount + 1n });
await WAERE.connect(alice).approve(await hub.getAddress(), amount + 1n);
await hub.connect(alice).depositSolverBond(await WAERE.getAddress(), solver1.address, 1n);
const intentId = ethers.id("intent3");
await hub.connect(alice).deposit(await WAERE.getAddress(), amount, intentId, recipient.address);
await hub.connect(solver1).claim(intentId, "0x");
await expect(hub.settle(intentId)).to.be.revertedWithCustomError(hub, "WithinChallengeWindow");
await ethers.provider.send("evm_increaseTime", [6 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
const solverPortion = amount - (amount * 50n) / 10_000n;
const balBefore = await WAERE.balanceOf(solver1.address);
await hub.settle(intentId);
expect(await WAERE.balanceOf(solver1.address) - balBefore).to.equal(solverPortion);
});
it("per-tx limit enforces", async function () {
const { alice, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 0, 10n * ONE, false);
await WAERE.connect(alice).deposit({ value: 100n * ONE });
await WAERE.connect(alice).approve(await hub.getAddress(), 100n * ONE);
await expect(
hub.connect(alice).deposit(await WAERE.getAddress(), 11n * ONE, ethers.id("intent4"), recipient.address)
).to.be.revertedWithCustomError(hub, "PerTxLimitExceeded");
});
it("restricted recipient enforced", async function () {
const { deployer, alice, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 0, 0, true);
await WAERE.connect(alice).deposit({ value: 10n * ONE });
await WAERE.connect(alice).approve(await hub.getAddress(), 10n * ONE);
// Recipient not on allowlist → revert.
await expect(
hub.connect(alice).deposit(await WAERE.getAddress(), 10n * ONE, ethers.id("intent5"), recipient.address)
).to.be.revertedWithCustomError(hub, "RecipientNotAllowed");
// Foundation allows recipient.
await hub.setRecipientAllowed(await WAERE.getAddress(), recipient.address, true);
await hub.connect(alice).deposit(await WAERE.getAddress(), 10n * ONE, ethers.id("intent5"), recipient.address);
});
it("duplicate intentId reverts", async function () {
const { alice, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 0, 0, false);
await WAERE.connect(alice).deposit({ value: 20n * ONE });
await WAERE.connect(alice).approve(await hub.getAddress(), 20n * ONE);
await hub.connect(alice).deposit(await WAERE.getAddress(), 10n * ONE, ethers.id("dup"), recipient.address);
await expect(
hub.connect(alice).deposit(await WAERE.getAddress(), 10n * ONE, ethers.id("dup"), recipient.address)
).to.be.revertedWithCustomError(hub, "DuplicateIntent");
});
it("paused asset rejects deposit", async function () {
const { alice, recipient, WAERE, hub } = await deployStack();
await hub.listAsset(await WAERE.getAddress(), 18, 0, 0, false);
await hub.setAssetPaused(await WAERE.getAddress(), true);
await WAERE.connect(alice).deposit({ value: 10n * ONE });
await WAERE.connect(alice).approve(await hub.getAddress(), 10n * ONE);
await expect(
hub.connect(alice).deposit(await WAERE.getAddress(), 10n * ONE, ethers.id("intent6"), recipient.address)
).to.be.revertedWithCustomError(hub, "AssetPausedErr");
});
});