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.
244 lines
10 KiB
JavaScript
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);
|
|
});
|
|
});
|