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