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.
113 lines
5.2 KiB
JavaScript
113 lines
5.2 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const DAY = 24 * 60 * 60;
|
|
|
|
describe("AereSlashingInsurance", function () {
|
|
let fund, foundation, delegator, donor, attacker;
|
|
const COVERAGE_BPS = 5000; // 50% of a slash
|
|
const PER_EVENT_CAP = ethers.parseEther("100"); // max 100 AERE per coverage
|
|
const COOLDOWN = 3 * DAY;
|
|
const CLAIM_DELAY = 2 * DAY;
|
|
|
|
beforeEach(async function () {
|
|
[foundation, delegator, donor, attacker] = await ethers.getSigners();
|
|
const F = await ethers.getContractFactory("AereSlashingInsurance");
|
|
fund = await F.connect(foundation).deploy(
|
|
foundation.address, COVERAGE_BPS, PER_EVENT_CAP, COOLDOWN, CLAIM_DELAY
|
|
);
|
|
await fund.waitForDeployment();
|
|
// seed the vault with native AERE
|
|
await fund.connect(donor).donate({ value: ethers.parseEther("500") });
|
|
});
|
|
|
|
it("records immutables and accepts donations (donate + receive)", async function () {
|
|
expect(await fund.FOUNDATION()).to.equal(foundation.address);
|
|
expect(await fund.COVERAGE_BPS()).to.equal(COVERAGE_BPS);
|
|
expect(await fund.PER_EVENT_CAP()).to.equal(PER_EVENT_CAP);
|
|
expect(await fund.available()).to.equal(ethers.parseEther("500"));
|
|
// plain transfer hits receive()
|
|
await donor.sendTransaction({ to: await fund.getAddress(), value: ethers.parseEther("10") });
|
|
expect(await fund.available()).to.equal(ethers.parseEther("510"));
|
|
expect(await fund.totalDonated()).to.equal(ethers.parseEther("510"));
|
|
});
|
|
|
|
it("coverageFor applies bps then caps per event", async function () {
|
|
// 50 AERE slash -> 25 AERE (under cap)
|
|
expect(await fund.coverageFor(ethers.parseEther("50"))).to.equal(ethers.parseEther("25"));
|
|
// 400 AERE slash -> 200 -> capped to 100
|
|
expect(await fund.coverageFor(ethers.parseEther("400"))).to.equal(PER_EVENT_CAP);
|
|
});
|
|
|
|
it("only Foundation can propose", async function () {
|
|
await expect(
|
|
fund.connect(attacker).proposeCoverage(delegator.address, ethers.parseEther("50"), ethers.ZeroHash)
|
|
).to.be.revertedWithCustomError(fund, "NotFoundation");
|
|
});
|
|
|
|
it("full happy path: propose, wait, execute pays frozen payout to delegator", async function () {
|
|
await fund.connect(foundation).proposeCoverage(
|
|
delegator.address, ethers.parseEther("50"), ethers.id("slash-evidence")
|
|
);
|
|
const c = await fund.claims(0);
|
|
expect(c.delegator).to.equal(delegator.address);
|
|
expect(c.payout).to.equal(ethers.parseEther("25"));
|
|
|
|
// cannot execute before the timelock
|
|
await expect(fund.connect(delegator).executeCoverage(0))
|
|
.to.be.revertedWithCustomError(fund, "TimelockNotElapsed");
|
|
|
|
await ethers.provider.send("evm_increaseTime", [CLAIM_DELAY + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
|
|
const before = await ethers.provider.getBalance(delegator.address);
|
|
// anyone can trigger; recipient/amount are frozen. Use donor to avoid gas accounting.
|
|
await fund.connect(donor).executeCoverage(0);
|
|
const after = await ethers.provider.getBalance(delegator.address);
|
|
expect(after - before).to.equal(ethers.parseEther("25"));
|
|
|
|
expect(await fund.totalCovered()).to.equal(ethers.parseEther("25"));
|
|
expect((await fund.claims(0)).exists).to.equal(false);
|
|
});
|
|
|
|
it("permissionless cancel blocks a fraudulent claim during the delay", async function () {
|
|
// compromised-key scenario: propose a payout to the attacker
|
|
await fund.connect(foundation).proposeCoverage(
|
|
attacker.address, ethers.parseEther("200"), ethers.ZeroHash
|
|
);
|
|
// community observer cancels it
|
|
await expect(fund.connect(donor).cancelCoverage(0, "payout to attacker"))
|
|
.to.emit(fund, "CoverageCancelled");
|
|
await ethers.provider.send("evm_increaseTime", [CLAIM_DELAY + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await expect(fund.connect(attacker).executeCoverage(0))
|
|
.to.be.revertedWithCustomError(fund, "NoClaim");
|
|
});
|
|
|
|
it("enforces the global cooldown between executed coverages", async function () {
|
|
// claim 0
|
|
await fund.connect(foundation).proposeCoverage(delegator.address, ethers.parseEther("50"), ethers.ZeroHash);
|
|
// claim 1
|
|
await fund.connect(foundation).proposeCoverage(delegator.address, ethers.parseEther("50"), ethers.ZeroHash);
|
|
await ethers.provider.send("evm_increaseTime", [CLAIM_DELAY + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await fund.connect(donor).executeCoverage(0);
|
|
// second execution blocked by cooldown
|
|
await expect(fund.connect(donor).executeCoverage(1))
|
|
.to.be.revertedWithCustomError(fund, "CooldownActive");
|
|
// after cooldown it succeeds
|
|
await ethers.provider.send("evm_increaseTime", [COOLDOWN + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await fund.connect(donor).executeCoverage(1);
|
|
expect(await fund.totalCovered()).to.equal(ethers.parseEther("50"));
|
|
});
|
|
|
|
it("has no admin-withdrawal path (only executeCoverage moves funds)", async function () {
|
|
// there is no withdraw()/sweep() function in the ABI
|
|
const names = fund.interface.fragments.filter((f) => f.type === "function").map((f) => f.name);
|
|
for (const n of names) {
|
|
expect(n.toLowerCase()).to.not.match(/withdraw|sweep|rescue|drain|skim/);
|
|
}
|
|
});
|
|
});
|