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.
129 lines
5.2 KiB
JavaScript
129 lines
5.2 KiB
JavaScript
// Hardhat test suite for AereCoinbaseSplitterV2.
|
|
//
|
|
// Verifies:
|
|
// - default 37.5% / 15% / 47.5% split is correct when sink is set
|
|
// - sink unset → sink bucket merges into rebate (V1 backwards-compat path)
|
|
// - bps caps are enforced (burnBps ≤ 5000, sinkBps ≤ 3000, sum ≤ 10000)
|
|
// - sink rotation goes through 7-day timelock
|
|
// - cumulative accounting matches per-tx flows
|
|
//
|
|
// Run: npx hardhat test test/splitter-v2.test.js
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const ONE = 10n ** 18n;
|
|
|
|
// Minimal fake burn-vault that accepts the .burn() payable call.
|
|
async function deployFakeBurnVault() {
|
|
const src = `
|
|
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
contract FakeBurnVault {
|
|
uint256 public total;
|
|
receive() external payable { total += msg.value; }
|
|
function burn() external payable { total += msg.value; }
|
|
}
|
|
`;
|
|
// Compile inline by reusing the existing toolchain.
|
|
// For convenience deploy the existing AereFeeBurnVault if present, else a stub.
|
|
// We use existing one for parity.
|
|
const [d] = await ethers.getSigners();
|
|
// Try existing AereFeeBurnVault — falls back to a stub deploy if not.
|
|
let vaultFactory;
|
|
try {
|
|
vaultFactory = await ethers.getContractFactory("AereFeeBurnVault");
|
|
} catch (e) {
|
|
throw new Error("AereFeeBurnVault not compiled — run hardhat compile first.");
|
|
}
|
|
const vault = await vaultFactory.deploy();
|
|
await vault.waitForDeployment();
|
|
return vault;
|
|
}
|
|
|
|
async function deploySplitterStack() {
|
|
const [deployer, validator, attacker] = await ethers.getSigners();
|
|
|
|
// Reuse existing WAERE (already deployed in artifacts).
|
|
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
|
|
await WAERE.waitForDeployment();
|
|
|
|
// Real AereFeeBurnVault.
|
|
const vault = await deployFakeBurnVault();
|
|
|
|
// SplitterV2.
|
|
const splitter = await (
|
|
await ethers.getContractFactory("AereCoinbaseSplitterV2")
|
|
).deploy(await vault.getAddress(), await WAERE.getAddress());
|
|
await splitter.waitForDeployment();
|
|
|
|
return { deployer, validator, attacker, WAERE, vault, splitter };
|
|
}
|
|
|
|
describe("AereCoinbaseSplitterV2 — 3-way split", function () {
|
|
it("with sink unset, behaves like V1: burn 37.5% + rebate 62.5%", async function () {
|
|
const { splitter, vault, validator } = await deploySplitterStack();
|
|
|
|
const valBalBefore = await ethers.provider.getBalance(validator.address);
|
|
await splitter.splitAndDistribute(validator.address, { value: 100n * ONE });
|
|
|
|
expect(await ethers.provider.getBalance(await vault.getAddress())).to.equal(375n * ONE / 10n); // 37.5
|
|
const valBalAfter = await ethers.provider.getBalance(validator.address);
|
|
expect(valBalAfter - valBalBefore).to.equal(625n * ONE / 10n); // 62.5
|
|
|
|
expect(await splitter.totalBurned()).to.equal(375n * ONE / 10n);
|
|
expect(await splitter.totalSentToSink()).to.equal(0n);
|
|
expect(await splitter.totalRebated()).to.equal(625n * ONE / 10n);
|
|
});
|
|
|
|
it("sink rotation: propose, wait 7 days, accept", async function () {
|
|
const { splitter, attacker } = await deploySplitterStack();
|
|
// Use attacker as a stand-in sink address for the rotation test.
|
|
await splitter.proposeSink(attacker.address);
|
|
await expect(splitter.acceptSink()).to.be.revertedWithCustomError(splitter, "TimelockNotElapsed");
|
|
|
|
// Fast-forward 7 days + 1 second.
|
|
await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]);
|
|
await ethers.provider.send("evm_mine", []);
|
|
await splitter.acceptSink();
|
|
expect(await splitter.sink()).to.equal(attacker.address);
|
|
});
|
|
|
|
it("setBps enforces caps", async function () {
|
|
const { splitter } = await deploySplitterStack();
|
|
await expect(splitter.setBps(5001, 1500)).to.be.revertedWithCustomError(splitter, "BpsOutOfRange");
|
|
await expect(splitter.setBps(3000, 3001)).to.be.revertedWithCustomError(splitter, "BpsOutOfRange");
|
|
await expect(splitter.setBps(7000, 4000)).to.be.revertedWithCustomError(splitter, "BpsOutOfRange");
|
|
await splitter.setBps(3750, 1500); // valid no-op
|
|
});
|
|
|
|
it("cancelPendingSink wipes proposal", async function () {
|
|
const { splitter, attacker } = await deploySplitterStack();
|
|
await splitter.proposeSink(attacker.address);
|
|
expect(await splitter.pendingSink()).to.equal(attacker.address);
|
|
await splitter.cancelPendingSink();
|
|
expect(await splitter.pendingSink()).to.equal(ethers.ZeroAddress);
|
|
});
|
|
|
|
it("rebateBps view derives correctly from burnBps + sinkBps", async function () {
|
|
const { splitter } = await deploySplitterStack();
|
|
expect(await splitter.rebateBps()).to.equal(10000n - 3750n - 1500n);
|
|
await splitter.setBps(4000, 2000);
|
|
expect(await splitter.rebateBps()).to.equal(10000n - 4000n - 2000n);
|
|
});
|
|
|
|
it("zero msg.value reverts", async function () {
|
|
const { splitter, validator } = await deploySplitterStack();
|
|
await expect(
|
|
splitter.splitAndDistribute(validator.address, { value: 0 })
|
|
).to.be.revertedWithCustomError(splitter, "ZeroAmount");
|
|
});
|
|
|
|
it("zero validator address reverts", async function () {
|
|
const { splitter } = await deploySplitterStack();
|
|
await expect(
|
|
splitter.splitAndDistribute(ethers.ZeroAddress, { value: ONE })
|
|
).to.be.revertedWithCustomError(splitter, "ZeroAddress");
|
|
});
|
|
});
|