aere-contracts/test/aere-sink-branch-coverage.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

531 lines
21 KiB
JavaScript

// Branch-focused coverage suite for AereSink (CANONICAL immutable 3-bucket
// router, live at 0x69581B86…1676). Every AERE that the splitter routes into
// the sAERE flywheel passes through here, and the contract has NO admin and NO
// upgrade path, so its branches are the whole of its behaviour.
//
// Measured round-2 baseline: 74.07% branch (the weak axis flagged as the next
// candidate at the end of the round-1 report), with uncovered line 286.
//
// This file targets exactly the gaps the four existing sink suites leave:
// * line 286 — the INNER oracle catch (the oracle answers for tokenIn but
// reverts on the AERE leg). The shared MockPriceOracle can only revert
// GLOBALLY, which reaches the OUTER catch; a selective-revert oracle is the
// only way in.
// * priceIn == 0 (the existing suite only zeroes the AERE leg)
// * the `floor == 0 ? 1 : floor` TRUE side
// * every constructor guard (four zero-address legs + the bps sum)
// * the toBurnTotal == 0 and stakerPart == 0 bucket skips
// * _safeTransfer's TransferFailed branch (a token whose transfer returns
// false without reverting)
//
// Run: npx hardhat test test/aere-sink-branch-coverage.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const ONE = 10n ** 18n;
/// Same shape as the existing sink-oracle-floor helper, but with the bucket bps
/// and the oracle both parameterised so the zero-bucket branches are reachable.
async function deploySink({
oracleAddr = ethers.ZeroAddress,
burnBps = 1500,
buybackBps = 4000,
stakerBps = 4500,
slippageBps = 500,
aereOverride = null,
} = {}) {
const [deployer] = await ethers.getSigners();
const WAERE = await (
await ethers.getContractFactory("contracts/WAERE.sol:WAERE")
).deploy();
await WAERE.waitForDeployment();
const FeeToken = await (
await ethers.getContractFactory("contracts/WAERE.sol:WAERE")
).deploy();
await FeeToken.waitForDeployment();
const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
await burnVault.waitForDeployment();
const router = await (await ethers.getContractFactory("MockSinkRouter")).deploy();
await router.waitForDeployment();
await router.setOutToken(await WAERE.getAddress());
const aere = aereOverride ?? (await WAERE.getAddress());
const sink = await (await ethers.getContractFactory("AereSink")).deploy(
aere,
await burnVault.getAddress(),
await WAERE.getAddress(), // sAERE stub
await router.getAddress(),
burnBps,
buybackBps,
stakerBps,
slippageBps,
oracleAddr
);
await sink.waitForDeployment();
return { deployer, WAERE, FeeToken, burnVault, router, sink };
}
/// Wrap AERE into both tokens and approve the sink, so flush() can pull.
async function fundAndApprove(ctx, amount) {
await ctx.deployer.sendTransaction({
to: await ctx.WAERE.getAddress(),
value: 1000n * ONE,
});
await ctx.WAERE.transfer(await ctx.router.getAddress(), 500n * ONE);
await ctx.deployer.sendTransaction({
to: await ctx.FeeToken.getAddress(),
value: amount,
});
await ctx.FeeToken.approve(await ctx.sink.getAddress(), amount);
}
describe("AereSink — branch coverage (constructor guards, oracle legs, bucket skips)", function () {
// Hermetic: snapshot before each test and revert after, so this file leaves
// signer balances and the block timestamp exactly as it found them. Without
// it, the AERE these tests wrap into WAERE / lock in vaults stays gone and
// later test FILES in the same mocha process can run the default signer dry.
let __snap;
beforeEach(async () => {
__snap = await ethers.provider.send("evm_snapshot", []);
});
afterEach(async () => {
await ethers.provider.send("evm_revert", [__snap]);
});
describe("constructor — every guard leg", function () {
let good;
beforeEach(async () => {
const [d] = await ethers.getSigners();
const W = await (
await ethers.getContractFactory("contracts/WAERE.sol:WAERE")
).deploy();
await W.waitForDeployment();
good = await W.getAddress();
});
async function tryDeploy(args) {
const F = await ethers.getContractFactory("AereSink");
return F.deploy(...args);
}
it("rejects a zero AERE address", async () => {
await expect(
tryDeploy([ethers.ZeroAddress, good, good, good, 1500, 4000, 4500, 500, ethers.ZeroAddress])
).to.be.reverted;
});
it("rejects a zero burn vault", async () => {
await expect(
tryDeploy([good, ethers.ZeroAddress, good, good, 1500, 4000, 4500, 500, ethers.ZeroAddress])
).to.be.reverted;
});
it("rejects a zero sAERE vault", async () => {
await expect(
tryDeploy([good, good, ethers.ZeroAddress, good, 1500, 4000, 4500, 500, ethers.ZeroAddress])
).to.be.reverted;
});
it("rejects a zero DEX router", async () => {
await expect(
tryDeploy([good, good, good, ethers.ZeroAddress, 1500, 4000, 4500, 500, ethers.ZeroAddress])
).to.be.reverted;
});
it("rejects bps that do not sum to 10000 (under and over)", async () => {
await expect(
tryDeploy([good, good, good, good, 1500, 4000, 4499, 500, ethers.ZeroAddress])
).to.be.reverted;
await expect(
tryDeploy([good, good, good, good, 1500, 4000, 4501, 500, ethers.ZeroAddress])
).to.be.reverted;
});
it("accepts a valid deploy and exposes the immutables", async () => {
const ctx = await deploySink({});
expect(await ctx.sink.BURN_BPS()).to.equal(1500);
expect(await ctx.sink.BUYBACK_BPS()).to.equal(4000);
expect(await ctx.sink.STAKER_YIELD_BPS()).to.equal(4500);
expect(await ctx.sink.MAX_SLIPPAGE_BPS()).to.equal(500);
expect(await ctx.sink.ORACLE()).to.equal(ethers.ZeroAddress);
expect(await ctx.sink.ORACLE_DECIMALS()).to.equal(18);
});
});
describe("_computeOracleFloor — the nested try/catch arms", function () {
it("INNER CATCH (line 286): oracle answers for tokenIn but REVERTS on the AERE leg", async () => {
// The shared MockPriceOracle reverts globally, so it can only reach the
// OUTER catch. SelectiveRevertOracle reverts for ONE asset only, which is
// the only way to enter the inner try and then fail inside it.
const oracle = await (
await ethers.getContractFactory("SelectiveRevertOracle")
).deploy();
await oracle.waitForDeployment();
const ctx = await deploySink({ oracleAddr: await oracle.getAddress() });
const feeAddr = await ctx.FeeToken.getAddress();
const aereAddr = await ctx.WAERE.getAddress();
await oracle.setPrice(feeAddr, ONE); // tokenIn leg answers
await oracle.setRevertFor(aereAddr, true); // AERE leg reverts
await fundAndApprove(ctx, 10n * ONE);
await ctx.sink.flush(feeAddr, 10n * ONE);
// Inner catch returns 1 (accept any non-zero output).
expect(await ctx.router.lastAmountOutMin()).to.equal(1n);
});
it("priceIn == 0: the tokenIn leg answers ZERO (the existing suite only zeroes AERE)", async () => {
const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
await oracle.waitForDeployment();
const ctx = await deploySink({ oracleAddr: await oracle.getAddress() });
// AERE priced, FeeToken deliberately left at 0.
await oracle.setPrice(await ctx.WAERE.getAddress(), ONE);
await fundAndApprove(ctx, 10n * ONE);
await ctx.sink.flush(await ctx.FeeToken.getAddress(), 10n * ONE);
expect(await ctx.router.lastAmountOutMin()).to.equal(1n);
});
it("floor == 0 -> returns 1: a tokenIn worth far less than AERE rounds the floor away", async () => {
const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
await oracle.waitForDeployment();
const ctx = await deploySink({ oracleAddr: await oracle.getAddress() });
const feeAddr = await ctx.FeeToken.getAddress();
const aereAddr = await ctx.WAERE.getAddress();
// priceIn = 1 wei of USD, priceAere = 1e18. expected = amountIn/1e18,
// which for a small amountIn floors to 0, so `floor == 0 ? 1 : floor`
// takes its TRUE side.
await oracle.setPrice(feeAddr, 1n);
await oracle.setPrice(aereAddr, ONE);
await fundAndApprove(ctx, 1000n);
await ctx.sink.flush(feeAddr, 1000n);
expect(await ctx.router.lastAmountOutMin()).to.equal(1n);
});
it("both legs priced: the floor is the real slippage-adjusted expectation (non-fallback)", async () => {
const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
await oracle.waitForDeployment();
const ctx = await deploySink({ oracleAddr: await oracle.getAddress() });
const feeAddr = await ctx.FeeToken.getAddress();
await oracle.setPrice(feeAddr, ONE);
await oracle.setPrice(await ctx.WAERE.getAddress(), ONE);
await fundAndApprove(ctx, 10n * ONE);
await ctx.sink.flush(feeAddr, 10n * ONE);
// Last swap is the staker bucket: 45% of 10 AERE, minus 5% slippage.
const expectedStaker = (10n * ONE * 4500n) / 10000n;
expect(await ctx.router.lastAmountOutMin()).to.equal(
(expectedStaker * 9500n) / 10000n
);
});
});
describe("flush — pull failure and the bucket-skip branches", function () {
it("reverts ZeroAmount on a zero flush", async () => {
const ctx = await deploySink({});
await expect(
ctx.sink.flush(await ctx.FeeToken.getAddress(), 0)
).to.be.revertedWithCustomError(ctx.sink, "ZeroAmount");
});
it("reverts TransferFailed when the pull's transferFrom returns FALSE", async () => {
// A token that returns false without reverting: `bool ok = transferFrom(...)`
// is false, so the `if (!ok)` TRUE branch fires.
const bad = await (
await ethers.getContractFactory("MockReturnFalseERC20")
).deploy();
await bad.waitForDeployment();
const ctx = await deploySink({});
await bad.mint(ctx.deployer.address, 10n * ONE);
await expect(
ctx.sink.flush(await bad.getAddress(), 10n * ONE)
).to.be.revertedWithCustomError(ctx.sink, "TransferFailed");
});
it("SKIPS the burn transfer when BURN + BUYBACK are both 0 (toBurnTotal == 0)", async () => {
const ctx = await deploySink({ burnBps: 0, buybackBps: 0, stakerBps: 10000 });
const vaultAddr = await ctx.burnVault.getAddress();
const aereAddr = await ctx.WAERE.getAddress();
await ctx.deployer.sendTransaction({ to: aereAddr, value: 100n * ONE });
await ctx.WAERE.approve(await ctx.sink.getAddress(), 10n * ONE);
const before = await ctx.WAERE.balanceOf(vaultAddr);
await ctx.sink.flush(aereAddr, 10n * ONE);
// Nothing reached the burn vault; the whole flush went to the sAERE leg.
expect(await ctx.WAERE.balanceOf(vaultAddr)).to.equal(before);
});
it("SKIPS the sAERE transfer when the staker bucket rounds to 0 (stakerPart == 0)", async () => {
// burn 5000 + buyback 5000 + staker 0: stakerPart = amount - burn - buyback
// is exactly 0 for an even amount, so the `stakerPart > 0` FALSE branch runs.
const ctx = await deploySink({ burnBps: 5000, buybackBps: 5000, stakerBps: 0 });
const aereAddr = await ctx.WAERE.getAddress();
await ctx.deployer.sendTransaction({ to: aereAddr, value: 100n * ONE });
await ctx.WAERE.approve(await ctx.sink.getAddress(), 10n * ONE);
await ctx.sink.flush(aereAddr, 10n * ONE);
// Everything landed in the burn vault.
expect(await ctx.WAERE.balanceOf(await ctx.burnVault.getAddress())).to.equal(
10n * ONE
);
});
it("SKIPS the staker SWAP on a NON-AERE flush when the staker bucket is 0", async () => {
// The AERE-input case is covered above; this is the swap-side twin, which
// is a separate branch because flush forks on `token == AERE` first.
const ctx = await deploySink({ burnBps: 5000, buybackBps: 5000, stakerBps: 0 });
await fundAndApprove(ctx, 10n * ONE);
await expect(ctx.sink.flush(await ctx.FeeToken.getAddress(), 10n * ONE)).to.emit(
ctx.sink,
"Flushed"
);
// Exactly ONE swap happened (the burn bundle); the staker leg was skipped.
expect(await ctx.router.lastAmountOutMin()).to.equal(1n);
});
it("SKIPS the burn SWAP on a NON-AERE flush when BURN + BUYBACK are 0", async () => {
const ctx = await deploySink({ burnBps: 0, buybackBps: 0, stakerBps: 10000 });
await fundAndApprove(ctx, 10n * ONE);
await expect(ctx.sink.flush(await ctx.FeeToken.getAddress(), 10n * ONE)).to.emit(
ctx.sink,
"Flushed"
);
});
it("emits Flushed with the exact bucket split and retains nothing", async () => {
const ctx = await deploySink({});
const aereAddr = await ctx.WAERE.getAddress();
await ctx.deployer.sendTransaction({ to: aereAddr, value: 100n * ONE });
await ctx.WAERE.approve(await ctx.sink.getAddress(), 10n * ONE);
const amount = 10n * ONE;
const burnPart = (amount * 1500n) / 10000n;
const buybackPart = (amount * 4000n) / 10000n;
const stakerPart = amount - burnPart - buybackPart;
await expect(ctx.sink.flush(aereAddr, amount))
.to.emit(ctx.sink, "Flushed")
.withArgs(
ctx.deployer.address,
aereAddr,
amount,
burnPart,
buybackPart,
stakerPart
);
expect(await ctx.WAERE.balanceOf(await ctx.sink.getAddress())).to.equal(0n);
});
});
describe("_safeTransfer — the return-false revert branch", function () {
it("reverts TransferFailed when the token's transfer returns FALSE", async () => {
// Deploy a sink whose AERE IS the return-false token, then sweepDust: that
// path reads balanceOf (non-zero) and goes straight to _safeTransfer
// WITHOUT a transferFrom pull, so it is the only way to reach
// `ret.length > 0 && !abi.decode(ret,(bool))`.
const bad = await (
await ethers.getContractFactory("MockReturnFalseERC20")
).deploy();
await bad.waitForDeployment();
const badAddr = await bad.getAddress();
const ctx = await deploySink({ aereOverride: badAddr });
await bad.mint(await ctx.sink.getAddress(), 10n * ONE);
await expect(ctx.sink.sweepDust(badAddr)).to.be.revertedWithCustomError(
ctx.sink,
"TransferFailed"
);
});
});
describe("_safeTransfer — the `!ok` (call itself reverted) leg", function () {
it("reverts TransferFailed when the token's transfer REVERTS outright", async () => {
// The return-false token above drives the `ret.length > 0 && !decode` leg.
// A token that actually reverts is the only way into the `!ok` leg.
const bad = await (
await ethers.getContractFactory("RevertingTransferToken")
).deploy();
await bad.waitForDeployment();
const badAddr = await bad.getAddress();
const ctx = await deploySink({ aereOverride: badAddr });
await bad.mint(await ctx.sink.getAddress(), 10n * ONE);
await expect(ctx.sink.sweepDust(badAddr)).to.be.revertedWithCustomError(
ctx.sink,
"TransferFailed"
);
});
});
describe("_swapAndForward — the swap-failure catch and its empty-reason leg", function () {
async function sinkWithRouter(routerAddr) {
const [deployer] = await ethers.getSigners();
const WAERE = await (
await ethers.getContractFactory("contracts/WAERE.sol:WAERE")
).deploy();
await WAERE.waitForDeployment();
const FeeToken = await (
await ethers.getContractFactory("contracts/WAERE.sol:WAERE")
).deploy();
await FeeToken.waitForDeployment();
const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
await burnVault.waitForDeployment();
const sink = await (await ethers.getContractFactory("AereSink")).deploy(
await WAERE.getAddress(),
await burnVault.getAddress(),
await WAERE.getAddress(),
routerAddr,
1500,
4000,
4500,
500,
ethers.ZeroAddress
);
await sink.waitForDeployment();
await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE });
await FeeToken.approve(await sink.getAddress(), 10n * ONE);
return { deployer, sink, FeeToken };
}
it("emits SwapFailed with the literal \"swap-failed\" when the router reverts with EMPTY data", async () => {
const router = await (await ethers.getContractFactory("EmptyRevertRouter")).deploy();
await router.waitForDeployment();
const { sink, FeeToken } = await sinkWithRouter(await router.getAddress());
const feeAddr = await FeeToken.getAddress();
// The flush does NOT revert: a dead pair leaves the tokens as dust and the
// sink continues. Both swap legs fail, so two SwapFailed events fire.
const tx = await sink.flush(feeAddr, 10n * ONE);
const rc = await tx.wait();
const evts = rc.logs
.map((l) => {
try {
return sink.interface.parseLog(l);
} catch {
return null;
}
})
.filter((e) => e && e.name === "SwapFailed");
expect(evts.length).to.be.greaterThan(0);
for (const e of evts) expect(e.args[2]).to.equal("swap-failed");
// The tokens really did stay on the sink as recoverable dust.
expect(await FeeToken.balanceOf(await sink.getAddress())).to.equal(10n * ONE);
});
});
describe("sweepDust — the non-AERE swap path", function () {
it("reverts ZeroAmount when the sink holds none of that token", async () => {
const ctx = await deploySink({});
await expect(
ctx.sink.sweepDust(await ctx.FeeToken.getAddress())
).to.be.revertedWithCustomError(ctx.sink, "ZeroAmount");
});
it("re-routes a NON-AERE dust balance through both swap legs", async () => {
const ctx = await deploySink({});
const feeAddr = await ctx.FeeToken.getAddress();
// Someone sends the fee token straight to the sink (no flush).
await ctx.deployer.sendTransaction({ to: feeAddr, value: 10n * ONE });
await ctx.WAERE.connect(ctx.deployer);
await ctx.deployer.sendTransaction({
to: await ctx.WAERE.getAddress(),
value: 500n * ONE,
});
await ctx.WAERE.transfer(await ctx.router.getAddress(), 400n * ONE);
await ctx.FeeToken.transfer(await ctx.sink.getAddress(), 10n * ONE);
await expect(ctx.sink.sweepDust(feeAddr))
.to.emit(ctx.sink, "DustSwept")
.withArgs(feeAddr, 10n * ONE);
});
it("SKIPS the burn SWAP leg on a NON-AERE dust sweep when BURN + BUYBACK are 0", async () => {
const ctx = await deploySink({ burnBps: 0, buybackBps: 0, stakerBps: 10000 });
const feeAddr = await ctx.FeeToken.getAddress();
await ctx.deployer.sendTransaction({
to: await ctx.WAERE.getAddress(),
value: 500n * ONE,
});
await ctx.WAERE.transfer(await ctx.router.getAddress(), 400n * ONE);
await ctx.deployer.sendTransaction({ to: feeAddr, value: 10n * ONE });
await ctx.FeeToken.transfer(await ctx.sink.getAddress(), 10n * ONE);
await expect(ctx.sink.sweepDust(feeAddr)).to.emit(ctx.sink, "DustSwept");
});
it("SKIPS the staker SWAP leg on a NON-AERE dust sweep when the staker bucket is 0", async () => {
const ctx = await deploySink({ burnBps: 5000, buybackBps: 5000, stakerBps: 0 });
const feeAddr = await ctx.FeeToken.getAddress();
await ctx.deployer.sendTransaction({
to: await ctx.WAERE.getAddress(),
value: 500n * ONE,
});
await ctx.WAERE.transfer(await ctx.router.getAddress(), 400n * ONE);
await ctx.deployer.sendTransaction({ to: feeAddr, value: 10n * ONE });
await ctx.FeeToken.transfer(await ctx.sink.getAddress(), 10n * ONE);
await expect(ctx.sink.sweepDust(feeAddr)).to.emit(ctx.sink, "DustSwept");
});
it("SKIPS the burn leg on a dust sweep when BURN + BUYBACK are 0", async () => {
const ctx = await deploySink({ burnBps: 0, buybackBps: 0, stakerBps: 10000 });
const aereAddr = await ctx.WAERE.getAddress();
await ctx.deployer.sendTransaction({ to: aereAddr, value: 100n * ONE });
await ctx.WAERE.transfer(await ctx.sink.getAddress(), 5n * ONE);
const before = await ctx.WAERE.balanceOf(await ctx.burnVault.getAddress());
await ctx.sink.sweepDust(aereAddr);
expect(await ctx.WAERE.balanceOf(await ctx.burnVault.getAddress())).to.equal(before);
});
it("SKIPS the sAERE leg on a dust sweep when the staker bucket is 0", async () => {
const ctx = await deploySink({ burnBps: 5000, buybackBps: 5000, stakerBps: 0 });
const aereAddr = await ctx.WAERE.getAddress();
await ctx.deployer.sendTransaction({ to: aereAddr, value: 100n * ONE });
await ctx.WAERE.transfer(await ctx.sink.getAddress(), 4n * ONE);
await ctx.sink.sweepDust(aereAddr);
expect(await ctx.WAERE.balanceOf(await ctx.burnVault.getAddress())).to.equal(4n * ONE);
expect(await ctx.WAERE.balanceOf(await ctx.sink.getAddress())).to.equal(0n);
});
});
});