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.
227 lines
11 KiB
JavaScript
227 lines
11 KiB
JavaScript
// 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);
|
|
});
|
|
});
|