aere-contracts/test/splitter-v2.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

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");
});
});