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.
171 lines
9.4 KiB
JavaScript
171 lines
9.4 KiB
JavaScript
// AUDIT FIX VERIFICATION — 2026-07-10
|
|
// Proves the two CONFIRMED bugs (findings 9 & 10 in audit-verify-2026-07.test.js)
|
|
// are FIXED in the V2 redeploys, by running the ORIGINAL exploit against BOTH:
|
|
// * V1 -> the exploit STILL reproduces (regression anchor)
|
|
// * V2 -> the exploit is BLOCKED (the attack call reverts) while legit flows work
|
|
//
|
|
// Runs fully local against the REAL V1 + V2 sources (same bytecode as deployed).
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ONE = 10n ** 18n;
|
|
|
|
describe("AUDIT FIX V2 — findings 9 & 10", function () {
|
|
this.timeout(300000);
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Finding 9 — AereDACommittee removeMember -> threshold 0 -> forge availability
|
|
// ─────────────────────────────────────────────────────────────
|
|
describe("F9 AereDACommittee fail-open (removeMember drives threshold to 0)", function () {
|
|
async function committeeSigs(dac, members, rollupId, epoch, dc) {
|
|
// members must already be sorted ascending by address
|
|
const net = await ethers.provider.getNetwork();
|
|
const inner = ethers.keccak256(
|
|
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
["string", "uint256", "address", "bytes32", "uint256", "bytes32"],
|
|
["AereDACommittee:attest", net.chainId, await dac.getAddress(), rollupId, epoch, dc]
|
|
)
|
|
);
|
|
const sigs = [];
|
|
for (const m of members) sigs.push(await m.signMessage(ethers.getBytes(inner)));
|
|
return sigs;
|
|
}
|
|
|
|
it("V1 CONFIRMED: admin removes all members; attest([]) forges availability with 0 sigs", async () => {
|
|
const [admin] = await ethers.getSigners();
|
|
const members = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()]
|
|
.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1));
|
|
const DAC = await ethers.getContractFactory("AereDACommittee");
|
|
const dac = await DAC.deploy(admin.address, members.map(m => m.address), 2);
|
|
await dac.waitForDeployment();
|
|
|
|
for (const m of members) await dac.connect(admin).removeMember(m.address);
|
|
expect(await dac.memberCount()).to.equal(0n);
|
|
expect(await dac.threshold()).to.equal(0n); // clamped to 0
|
|
|
|
const rollupId = ethers.id("rollup:forge");
|
|
await dac.attest(rollupId, 42n, ethers.id("forged"), []); // zero sigs accepted
|
|
expect(await dac.isAvailable(rollupId, 42n)).to.equal(true);
|
|
const [available, , , sigs] = await dac.getAttestation(rollupId, 42n);
|
|
expect(available).to.equal(true);
|
|
expect(sigs).to.equal(0n); // forged with zero committee signatures
|
|
});
|
|
|
|
it("V2 BLOCKED: threshold can never reach 0; removeMember reverts; attest([]) reverts; legit M-of-N still works", async () => {
|
|
const [admin] = await ethers.getSigners();
|
|
const members = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()]
|
|
.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1));
|
|
const DAC = await ethers.getContractFactory("AereDACommitteeV2");
|
|
const dac = await DAC.deploy(admin.address, members.map(m => m.address), 2);
|
|
await dac.waitForDeployment();
|
|
|
|
// ── the original attack: try to remove all members ──
|
|
// memberCount 3 -> 2 is fine (>= threshold 2)
|
|
await dac.connect(admin).removeMember(members[2].address);
|
|
expect(await dac.memberCount()).to.equal(2n);
|
|
expect(await dac.threshold()).to.equal(2n); // NOT clamped
|
|
|
|
// memberCount 2 -> 1 would drop below threshold 2 -> REVERT
|
|
await expect(dac.connect(admin).removeMember(members[1].address))
|
|
.to.be.revertedWithCustomError(dac, "WouldBreakThreshold").withArgs(1n, 2n);
|
|
|
|
// admin must lower threshold explicitly first (still can't be 0)
|
|
await expect(dac.connect(admin).setThreshold(0)).to.be.revertedWithCustomError(dac, "BadThreshold");
|
|
await dac.connect(admin).setThreshold(1);
|
|
await dac.connect(admin).removeMember(members[1].address); // now ok, memberCount 1
|
|
expect(await dac.memberCount()).to.equal(1n);
|
|
expect(await dac.threshold()).to.equal(1n);
|
|
|
|
// the LAST member can never be removed -> threshold can never reach 0
|
|
await expect(dac.connect(admin).removeMember(members[0].address))
|
|
.to.be.revertedWithCustomError(dac, "WouldBreakThreshold").withArgs(0n, 1n);
|
|
expect(await dac.threshold()).to.equal(1n);
|
|
|
|
// even so, attest([]) is rejected outright (belt-and-suspenders)
|
|
await expect(dac.attest(ethers.id("r"), 1n, ethers.id("d"), []))
|
|
.to.be.revertedWithCustomError(dac, "EmptySignatureSet");
|
|
expect(await dac.isAvailable(ethers.id("r"), 1n)).to.equal(false);
|
|
|
|
// ── legit flows on a fresh 2-of-3 committee ──
|
|
const m2 = [ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom()]
|
|
.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1));
|
|
const dac2 = await DAC.deploy(admin.address, m2.map(m => m.address), 2);
|
|
await dac2.waitForDeployment();
|
|
|
|
const rollupId = ethers.id("rollup:legit");
|
|
const dc = ethers.id("batch-commitment");
|
|
const allSigs = await committeeSigs(dac2, m2, rollupId, 7n, dc);
|
|
|
|
// 1 sig reverts ThresholdNotMet(1,2)
|
|
await expect(dac2.attest(rollupId, 7n, dc, [allSigs[0]]))
|
|
.to.be.revertedWithCustomError(dac2, "ThresholdNotMet").withArgs(1n, 2n);
|
|
|
|
// 2 distinct ascending sigs -> recorded
|
|
await dac2.attest(rollupId, 7n, dc, [allSigs[0], allSigs[1]]);
|
|
expect(await dac2.isAvailable(rollupId, 7n)).to.equal(true);
|
|
const [avail, , , cnt] = await dac2.getAttestation(rollupId, 7n);
|
|
expect(avail).to.equal(true);
|
|
expect(cnt).to.equal(2n);
|
|
|
|
// empty sigs still rejected on this live committee too
|
|
await expect(dac2.attest(ethers.id("rollup:x"), 1n, dc, []))
|
|
.to.be.revertedWithCustomError(dac2, "EmptySignatureSet");
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Finding 10 — AereSlashingInsurance matured claim griefed via cancel
|
|
// ─────────────────────────────────────────────────────────────
|
|
describe("F10 AereSlashingInsurance griefing (cancel a matured payout)", function () {
|
|
it("V1 CONFIRMED: griefer cancels a matured claim, permanently blocking the payout", async () => {
|
|
const [foundation, griefer, delegatee] = await ethers.getSigners();
|
|
const SI = await ethers.getContractFactory("AereSlashingInsurance");
|
|
const si = await SI.deploy(foundation.address, 5000, 10n ** 24n, 0, 100);
|
|
await si.waitForDeployment();
|
|
await si.connect(foundation).donate({ value: 100n * ONE });
|
|
|
|
await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence"));
|
|
await ethers.provider.send("evm_increaseTime", [101]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
|
|
// matured — but cancel has no timelock guard, so griefer deletes it
|
|
await si.connect(griefer).cancelCoverage(0, "griefing a valid claim");
|
|
await expect(si.connect(delegatee).executeCoverage(0)).to.be.revertedWithCustomError(si, "NoClaim");
|
|
});
|
|
|
|
it("V2 BLOCKED: matured claim can NOT be cancelled (grief reverts) and executes; un-matured claim still cancellable", async () => {
|
|
const [foundation, griefer, delegatee] = await ethers.getSigners();
|
|
const SI = await ethers.getContractFactory("AereSlashingInsuranceV2");
|
|
const si = await SI.deploy(foundation.address, 5000, 10n ** 24n, 0, 100);
|
|
await si.waitForDeployment();
|
|
await si.connect(foundation).donate({ value: 100n * ONE });
|
|
|
|
// ── claim 0: matured -> grief blocked, executes ──
|
|
await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence"));
|
|
const claim = await si.claims(0);
|
|
const payout = claim.payout; // 20 * 50% = 10 AERE
|
|
expect(payout).to.equal(10n * ONE);
|
|
|
|
await ethers.provider.send("evm_increaseTime", [101]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
|
|
// griefer front-run now REVERTS (ClaimMatured)
|
|
await expect(si.connect(griefer).cancelCoverage(0, "grief"))
|
|
.to.be.revertedWithCustomError(si, "ClaimMatured");
|
|
|
|
// payout executes to the frozen delegator
|
|
const before = await ethers.provider.getBalance(delegatee.address);
|
|
await si.connect(griefer).executeCoverage(0); // anyone can trigger; recipient frozen
|
|
const after = await ethers.provider.getBalance(delegatee.address);
|
|
expect(after - before).to.equal(payout);
|
|
|
|
// ── claim 1: NOT matured -> community veto still works (permissionless cancel) ──
|
|
await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence2"));
|
|
// do NOT advance time past readyAt
|
|
await si.connect(griefer).cancelCoverage(1, "legit community veto during window");
|
|
await expect(si.connect(delegatee).executeCoverage(1)).to.be.revertedWithCustomError(si, "NoClaim");
|
|
});
|
|
});
|
|
});
|