// Round 3 fix: AereSink oracle-based amountOutMin computation. // // Sandwich-resistance: amountOutMin must be derived from an EXTERNAL oracle // (price feed independent of DEX pool state), not from DEX reserves. const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE = 10n ** 18n; describe("AereSink — oracle floor (Round 3 H1 fix)", function () { async function deployWithOracle(oracleAddr) { 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(); // Router will return AERE (the first WAERE). await router.setOutToken(await WAERE.getAddress()); const sink = await (await ethers.getContractFactory("AereSink")).deploy( await WAERE.getAddress(), // AERE await burnVault.getAddress(), // BURN_VAULT await WAERE.getAddress(), // sAERE stub (just receives) await router.getAddress(), // DEX_ROUTER 1500, 4000, 4500, // burn/buyback/staker bps 500, // 5% slippage oracleAddr, // ORACLE ); await sink.waitForDeployment(); return { deployer, WAERE, FeeToken, burnVault, router, sink }; } it("ORACLE = address(0) → amountOutMin = 1 (legacy behavior, sandwich-vulnerable)", async function () { const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(ethers.ZeroAddress); // Fund router with AERE so it can pay the swap output. await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE }); await WAERE.transfer(await router.getAddress(), 100n * ONE); // Get fee tokens via WAERE wrap, then approve sink. await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE }); await FeeToken.approve(await sink.getAddress(), 10n * ONE); await sink.flush(await FeeToken.getAddress(), 10n * ONE); expect(await router.lastAmountOutMin()).to.equal(1n); }); it("ORACLE set with equal prices → amountOutMin reflects 5% slippage tolerance", async function () { const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy(); await oracle.waitForDeployment(); const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress()); // Set both at $1 (1e18). await oracle.setPrice(await FeeToken.getAddress(), ONE); await oracle.setPrice(await WAERE.getAddress(), ONE); // Fund router with AERE. await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE }); await WAERE.transfer(await router.getAddress(), 100n * ONE); await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE }); await FeeToken.approve(await sink.getAddress(), 10n * ONE); await sink.flush(await FeeToken.getAddress(), 10n * ONE); // Two swaps inside flush: burn+buyback bundle (55% of 10 = 5.5 AERE) // and staker (45% = 4.5 AERE). Last call captured by mock: // Expected on staker swap (4.5 AERE): floor = 4.5 * 95% = 4.275 AERE. const lastFloor = await router.lastAmountOutMin(); const expectedStaker = (45n * 10n * ONE) / 100n; // 4.5e18 const minStaker = expectedStaker * 9500n / 10_000n; // 4.275e18 expect(lastFloor).to.equal(minStaker); }); it("ORACLE returns zero for one asset → fallback amountOutMin = 1", async function () { const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy(); await oracle.waitForDeployment(); const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress()); // FeeToken priced; AERE not (returns 0). await oracle.setPrice(await FeeToken.getAddress(), ONE); // AERE intentionally not set. await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE }); await WAERE.transfer(await router.getAddress(), 100n * ONE); await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE }); await FeeToken.approve(await sink.getAddress(), 10n * ONE); await sink.flush(await FeeToken.getAddress(), 10n * ONE); expect(await router.lastAmountOutMin()).to.equal(1n); }); it("ORACLE reverts → fallback amountOutMin = 1", async function () { const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy(); await oracle.waitForDeployment(); const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress()); await oracle.setRevert(true); await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE }); await WAERE.transfer(await router.getAddress(), 100n * ONE); await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE }); await FeeToken.approve(await sink.getAddress(), 10n * ONE); await sink.flush(await FeeToken.getAddress(), 10n * ONE); expect(await router.lastAmountOutMin()).to.equal(1n); }); });