aere-contracts/test/sink-oracle-floor.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

115 lines
5.4 KiB
JavaScript

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