// AereRaaSFactory + AereRollupSettlement tests. const { expect } = require("chai"); const { ethers } = require("hardhat"); async function deployStack(opts = {}) { const [deployer, foundation, alice, bob, keeper] = await ethers.getSigners(); // BOND_TOKEN + DEBT_TOKEN — reuse MockERC20Lending const ERC20 = await ethers.getContractFactory("MockERC20Lending"); const bond = await ERC20.deploy("WAERE", "WAERE", 18); await bond.waitForDeployment(); const debt = await ERC20.deploy("USDC.e", "USDC.e", 6); await debt.waitForDeployment(); // Sink — reuse the real AereSink. 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(); // Factory. const Fac = await ethers.getContractFactory("AereRaaSFactory"); const fac = await Fac.deploy( await bond.getAddress(), await sink.getAddress(), foundation.address, 1000n * 10n ** 18n, // registrationBond = 1000 WAERE 1000, // minSinkBps = 10% 6 * 3600, // defaultChallengeWindow = 6h ); await fac.waitForDeployment(); return { deployer, foundation, alice, bob, keeper, bond, debt, sink, fac }; } async function registerRollup(env, opts = {}) { const id = opts.id ?? ethers.id("rollup-" + Math.floor(Math.random() * 1e9)); await env.bond.mint(env.alice.address, 1000n * 10n ** 18n); await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n); const tx = await env.fac.connect(env.alice).registerRollup( id, env.alice.address, // rollupOwner env.bob.address, // sequencer await env.debt.getAddress(), opts.sinkBps ?? 1500, opts.rollupBps ?? 7500, opts.sequencerBps ?? 1000, opts.minChallengeBond ?? 100n * 10n ** 6n, opts.challengeWindow ?? 0, "ipfs://QmMeta" ); await tx.wait(); const r = await env.fac.rollups(id); const settlement = await ethers.getContractAt("AereRollupSettlement", r.settlement); return { id, settlement }; } describe("AereRaaSFactory + AereRollupSettlement", function () { it("registers a rollup, deploys settlement contract", async function () { const env = await deployStack(); const { id, settlement } = await registerRollup(env); const r = await env.fac.rollups(id); expect(r.registered).to.equal(true); expect(r.bond).to.equal(1000n * 10n ** 18n); expect(await settlement.ROLLUP_ID()).to.equal(id); expect(await settlement.SINK_BPS()).to.equal(1500); }); it("rejects splits that don't sum to 10000", async function () { const env = await deployStack(); await env.bond.mint(env.alice.address, 1000n * 10n ** 18n); await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n); await expect(env.fac.connect(env.alice).registerRollup( ethers.id("badsplit"), env.alice.address, env.bob.address, await env.debt.getAddress(), 1000, 7500, 1000, 0, 0, "" )).to.be.revertedWithCustomError(env.fac, "InvalidSplits"); }); it("rejects sinkBps below floor", async function () { const env = await deployStack(); await env.bond.mint(env.alice.address, 1000n * 10n ** 18n); await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n); await expect(env.fac.connect(env.alice).registerRollup( ethers.id("lowsink"), env.alice.address, env.bob.address, await env.debt.getAddress(), 500, 8500, 1000, 0, 0, "" // 5% < 10% floor )).to.be.revertedWithCustomError(env.fac, "SinkBpsTooLow"); }); it("sequencer proposes + auto-finalises after window", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1")); expect(await settlement.isFinalised(1)).to.equal(false); // Advance past 6h window. await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]); await ethers.provider.send("evm_mine", []); expect(await settlement.isFinalised(1)).to.equal(true); await expect(settlement.advanceFinalisation()).to.emit(settlement, "StateRootFinalised"); }); it("non-sequencer rejected on propose", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await expect( settlement.connect(env.alice).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1")) ).to.be.revertedWithCustomError(settlement, "NotSequencer"); }); it("challenge blocks finalisation; sequencer resolves IN WINDOW; bond routes", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1")); // Mint bond and challenge. await env.debt.mint(env.keeper.address, 200n * 10n ** 6n); await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n); await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("oh no")); // While window is still open, sequencer can resolve. // (Sequencer rejects challenge → keeps bond.) const seqBefore = await env.debt.balanceOf(env.bob.address); await settlement.connect(env.bob).resolveChallenge(1, false); expect(await env.debt.balanceOf(env.bob.address) - seqBefore).to.equal(200n * 10n ** 6n); // After window passes the root finalises (was never rejected). await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]); await ethers.provider.send("evm_mine", []); expect(await settlement.isFinalised(1)).to.equal(true); }); it("revenue settlement splits sink/rollup/sequencer", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env, { sinkBps: 2000, rollupBps: 6000, sequencerBps: 2000 }); // rollupOwner = alice settles 10,000 USDC.e. const amount = 10_000n * 10n ** 6n; await env.debt.mint(env.alice.address, amount); await env.debt.connect(env.alice).approve(await settlement.getAddress(), amount); const seqBefore = await env.debt.balanceOf(env.bob.address); const rolBefore = await env.debt.balanceOf(env.alice.address); await settlement.connect(env.alice).settleRevenue(amount); // sequencer (bob) +20%, rollupOwner (alice) +60% net (paid in 100% then receives 60%) — so net = -40%. expect(await env.debt.balanceOf(env.bob.address) - seqBefore).to.equal(amount * 2000n / 10000n); expect(rolBefore - (await env.debt.balanceOf(env.alice.address))).to.equal(amount * 4000n / 10000n); }); it("exit returns bond on valid Foundation attestation", async function () { const env = await deployStack(); const { id } = await registerRollup(env); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "address", "uint64"], ["AereRaaSFactory:exit", (await ethers.provider.getNetwork()).chainId, await env.fac.getAddress(), id, env.alice.address, deadline] )); const sig = await env.foundation.signMessage(ethers.getBytes(hash)); const before = await env.bond.balanceOf(env.alice.address); await env.fac.connect(env.alice).exitRollup(id, env.alice.address, deadline, sig); expect(await env.bond.balanceOf(env.alice.address) - before).to.equal(1000n * 10n ** 18n); }); it("exit rejects bad signature", async function () { const env = await deployStack(); const { id } = await registerRollup(env); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; // signed by alice (NOT foundation) const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "address", "uint64"], ["AereRaaSFactory:exit", (await ethers.provider.getNetwork()).chainId, await env.fac.getAddress(), id, env.alice.address, deadline] )); const sig = await env.alice.signMessage(ethers.getBytes(hash)); await expect( env.fac.connect(env.alice).exitRollup(id, env.alice.address, deadline, sig) ).to.be.revertedWithCustomError(env.fac, "InvalidExitSig"); }); it("H6 fix: resolveChallenge after window closed reverts", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1")); await env.debt.mint(env.keeper.address, 200n * 10n ** 6n); await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n); await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("fraud")); // Wait past window — sequencer is now TOO LATE. await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]); await ethers.provider.send("evm_mine", []); await expect( settlement.connect(env.bob).resolveChallenge(1, false) ).to.be.revertedWithCustomError(settlement, "WindowClosed"); }); it("H6 fix: resolveChallengeExpired returns bond + marks root rejected", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1")); await env.debt.mint(env.keeper.address, 200n * 10n ** 6n); await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n); await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("fraud")); // Cannot expire-resolve while window still open. await expect( settlement.connect(env.alice).resolveChallengeExpired(1) ).to.be.revertedWithCustomError(settlement, "WindowOpen"); await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]); await ethers.provider.send("evm_mine", []); const before = await env.debt.balanceOf(env.keeper.address); await settlement.connect(env.alice).resolveChallengeExpired(1); expect(await env.debt.balanceOf(env.keeper.address) - before).to.equal(200n * 10n ** 6n); expect(await settlement.isFinalised(1)).to.equal(false); // rejected }); it("sequencer rotation by rollup owner", async function () { const env = await deployStack(); const { settlement } = await registerRollup(env); await settlement.connect(env.alice).setSequencer(env.keeper.address); expect(await settlement.sequencer()).to.equal(env.keeper.address); }); });