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