aere-contracts/test/retropgf-round1.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

244 lines
10 KiB
JavaScript

// Hardhat tests for AereRetroPGFRound1.
// Covers: fund → propose → challenge → dismiss → finalize → claim path,
// per-category cap enforcement, leaf replay protection, and sweep window.
//
// Run: npx hardhat test test/retropgf-round1.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const ONE = 10n ** 18n;
const TOTAL = 10_000_000n * ONE;
// Per immediate_build_plan default split (10M / 6 buckets):
// 1 Infrastructure, 2 Dev tooling, 3 Applications, 4 Education, 5 Security, 6 Buffer
const CATEGORY_CAPS = [
2_500_000n * ONE, // 0 Infrastructure
2_000_000n * ONE, // 1 Dev tooling
2_500_000n * ONE, // 2 Applications
1_000_000n * ONE, // 3 Education
1_500_000n * ONE, // 4 Security
500_000n * ONE, // 5 Buffer
];
async function deployWithMerkle(leaves) {
const [deployer, reserve, alice, bob] = await ethers.getSigners();
// WAERE (reuse existing).
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
await WAERE.waitForDeployment();
// Round1.
const Round = await ethers.getContractFactory("AereRetroPGFRound1");
const round = await Round.deploy(await WAERE.getAddress(), reserve.address, TOTAL, CATEGORY_CAPS);
await round.waitForDeployment();
// Fund the deployer with 10M WAERE.
// We can wrap from hardhat funded ETH. Hardhat default is 10k ETH; mainnet
// genesis allocation here is irrelevant. Wrap 10M ETH? hardhat default isn't
// enough. Instead, monkey-patch by directly minting via .deposit() chunks
// is impossible — we don't have 10M ETH. Use a smaller TOTAL for tests
// by deploying with smaller caps.
// Alternative: deploy a fresh round with smaller TOTAL for tests.
return { deployer, reserve, alice, bob, WAERE, round };
}
// Smaller test fixture so we can fund within hardhat default balance.
async function deploySmall() {
const [deployer, reserve, alice, bob, carol] = await ethers.getSigners();
const TOTAL_SMALL = 600n * ONE;
const CATEGORY_CAPS_SMALL = [100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE];
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
await WAERE.waitForDeployment();
const Round = await ethers.getContractFactory("AereRetroPGFRound1");
const round = await Round.deploy(await WAERE.getAddress(), reserve.address, TOTAL_SMALL, CATEGORY_CAPS_SMALL);
await round.waitForDeployment();
return { deployer, reserve, alice, bob, carol, WAERE, round, TOTAL: TOTAL_SMALL };
}
// We use a custom leaf-encoding that matches the contract's keccak256(abi.encode(recipient, category, amount)).
// OpenZeppelin's StandardMerkleTree double-hashes; the contract uses single-hash. Implement minimal Merkle manually.
function leafOf(recipient, category, amount) {
return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["address", "uint8", "uint256"], [recipient, category, amount]));
}
function buildSimpleTree(leaves) {
// For tests: small N. Sort + pair + hash.
let layer = leaves.slice().map(l => l.toLowerCase());
layer.sort();
const layers = [layer];
while (layer.length > 1) {
const next = [];
for (let i = 0; i < layer.length; i += 2) {
const a = layer[i];
const b = layer[i + 1] ?? layer[i];
const [lo, hi] = a < b ? [a, b] : [b, a];
next.push(ethers.keccak256(ethers.concat([lo, hi])));
}
layer = next;
layers.push(layer);
}
return { root: layer[0], layers };
}
function proofFor(tree, leaf) {
const proof = [];
let target = leaf.toLowerCase();
for (let i = 0; i < tree.layers.length - 1; i++) {
const lay = tree.layers[i];
const idx = lay.indexOf(target);
if (idx < 0) throw new Error("leaf not in layer");
const sib = idx ^ 1;
if (sib < lay.length) proof.push(lay[sib]);
// Move up.
const a = lay[idx];
const b = lay[sib] ?? lay[idx];
const [lo, hi] = a < b ? [a, b] : [b, a];
target = ethers.keccak256(ethers.concat([lo, hi]));
}
return proof;
}
describe("AereRetroPGFRound1", function () {
it("constructor requires category caps sum == total", async function () {
const [deployer, reserve] = await ethers.getSigners();
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
const Round = await ethers.getContractFactory("AereRetroPGFRound1");
const badCaps = [50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE]; // 300, not 600
await expect(Round.deploy(await WAERE.getAddress(), reserve.address, 600n * ONE, badCaps)).to.be.revertedWithCustomError(Round, "CapsMustSumToTotal");
});
it("fund + propose + finalize + claim flow", async function () {
const { deployer, reserve, alice, bob, carol, WAERE, round, TOTAL } = await deploySmall();
// Fund WAERE.
await WAERE.connect(deployer).deposit({ value: TOTAL });
await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
await round.connect(deployer).fund();
// Build a small payout tree: alice gets 50e18 in cat 0; bob gets 30e18 in cat 1.
const a_amt = 50n * ONE;
const b_amt = 30n * ONE;
const lAlice = leafOf(alice.address, 0, a_amt);
const lBob = leafOf(bob.address, 1, b_amt);
const tree = buildSimpleTree([lAlice, lBob]);
await round.connect(deployer).proposePayoutRoot(tree.root);
// Try to claim before finalize: should revert WrongState.
await expect(round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice))).to.be.revertedWithCustomError(round, "WrongState");
// Try to finalize early.
await expect(round.finalize()).to.be.revertedWithCustomError(round, "TimelockNotElapsed");
// Advance 14 days.
await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await round.finalize();
// Alice claims.
const aliceBefore = await WAERE.balanceOf(alice.address);
await round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice));
expect(await WAERE.balanceOf(alice.address)).to.equal(aliceBefore + a_amt);
// Double-claim reverts.
await expect(round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice))).to.be.revertedWithCustomError(round, "AlreadyClaimed");
// Bob claims.
await round.connect(bob).claim(bob.address, 1, b_amt, proofFor(tree, lBob));
expect(await WAERE.balanceOf(bob.address)).to.equal(b_amt);
});
it("rejects claim with wrong proof", async function () {
const { deployer, alice, WAERE, round, TOTAL } = await deploySmall();
await WAERE.connect(deployer).deposit({ value: TOTAL });
await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
await round.connect(deployer).fund();
const l = leafOf(alice.address, 0, 50n * ONE);
const tree = buildSimpleTree([l, leafOf("0x" + "00".repeat(20), 1, 10n * ONE)]);
await round.connect(deployer).proposePayoutRoot(tree.root);
await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await round.finalize();
// Use a junk proof.
await expect(
round.connect(alice).claim(alice.address, 0, 50n * ONE, [ethers.ZeroHash])
).to.be.revertedWithCustomError(round, "InvalidProof");
});
it("category cap enforcement: overflow reverts", async function () {
const { deployer, alice, WAERE, round, TOTAL } = await deploySmall();
await WAERE.connect(deployer).deposit({ value: TOTAL });
await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
await round.connect(deployer).fund();
// Each category cap is 100 ONE; this leaf claims 101 from cat 2.
const l = leafOf(alice.address, 2, 101n * ONE);
const tree = buildSimpleTree([l]);
await round.connect(deployer).proposePayoutRoot(tree.root);
await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await round.finalize();
await expect(
round.connect(alice).claim(alice.address, 2, 101n * ONE, proofFor(tree, l))
).to.be.revertedWithCustomError(round, "CategoryOverflow");
});
it("challenge flow: anyone can challenge; owner can dismiss; window doesn't reset", async function () {
const { deployer, alice, bob, WAERE, round, TOTAL } = await deploySmall();
await WAERE.connect(deployer).deposit({ value: TOTAL });
await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
await round.connect(deployer).fund();
const l = leafOf(alice.address, 0, 50n * ONE);
const tree = buildSimpleTree([l]);
await round.connect(deployer).proposePayoutRoot(tree.root);
// Bob challenges.
await round.connect(bob).challenge("alice doesnt deserve cat 0");
// State is Challenged → cannot finalize even after window.
await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await expect(round.finalize()).to.be.revertedWithCustomError(round, "WrongState");
// Owner dismisses; state returns to Proposed; CAN finalize now (window already passed).
await round.connect(deployer).dismissChallenge(0, "evidence below shows alice DOES deserve");
await round.finalize();
// Alice claims.
await round.connect(alice).claim(alice.address, 0, 50n * ONE, proofFor(tree, l));
});
it("sweep unclaimed after 365 days returns funds to ecosystem reserve", async function () {
const { deployer, reserve, alice, WAERE, round, TOTAL } = await deploySmall();
await WAERE.connect(deployer).deposit({ value: TOTAL });
await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
await round.connect(deployer).fund();
const l = leafOf(alice.address, 0, 50n * ONE);
const tree = buildSimpleTree([l]);
await round.connect(deployer).proposePayoutRoot(tree.root);
await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await round.finalize();
// Alice claims her 50.
await round.connect(alice).claim(alice.address, 0, 50n * ONE, proofFor(tree, l));
const remaining = TOTAL - 50n * ONE;
// Try to sweep early.
await expect(round.sweepUnclaimed()).to.be.revertedWithCustomError(round, "SweepWindowNotElapsed");
await ethers.provider.send("evm_increaseTime", [365 * 24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await round.sweepUnclaimed();
expect(await WAERE.balanceOf(reserve.address)).to.equal(remaining);
});
});