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