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.
531 lines
21 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|