aere-contracts/test/audit-fix-v2-f12-f13-2026-07.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

257 lines
13 KiB
JavaScript

// AUDIT FIX VERIFICATION — F12 + F13 — 2026-07-10
//
// Closes the campaign at 13/13. For each finding the live V1 REPRODUCES the bug
// and the V2 fix CORRECTS it. Runs fully local against the REAL sources that
// get deployed to chain 2800 (no mainnet txs here).
//
// F12 AereLendingOracle NavOracle feed is a DEAD PATH: configureFeed
// (NavOracle -> AereNavOracle) reverts because the deployed NAV oracle
// (Merkle reserve snapshots) has no latestForAsset(bytes32). FIX: a
// purpose-built owner-attested AereNavOracleAdapter implements
// latestForAsset, so a NavOracle-type feed configures + reads cleanly.
//
// F13 AERE402Facilitator replay key consumed[agentId][nonce] is GLOBAL per
// agent, so two providers with colliding per-provider nonces collide and
// a valid settlement reverts. FIX: AERE402FacilitatorV2 keys replay per
// (agentId, payee, nonce); AereAgentV2 is cross-wired to it.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const ONE = 10n ** 18n;
// ─────────────────────────────────────────────────────────────────────────────
// F12
// ─────────────────────────────────────────────────────────────────────────────
describe("F12 AereLendingOracle NavOracle dead path — V1 reproduces / V2 corrects", function () {
this.timeout(120000);
it("V1 REPRODUCES: configureFeed(NavOracle -> raw AereNavOracle) reverts (no latestForAsset)", async () => {
const [owner] = await ethers.getSigners();
const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
await lo.waitForDeployment();
const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
await nav.waitForDeployment();
// The deployed AereNavOracle exposes NO latestForAsset(bytes32).
const navNames = nav.interface.fragments.filter(f => f.type === "function").map(f => f.name);
expect(navNames).to.not.include("latestForAsset");
const asset = ethers.Wallet.createRandom().address;
// FeedType.NavOracle = 1. Auto-poke calls latestForAsset -> revert.
await expect(
lo.connect(owner).configureFeed(asset, 1, await nav.getAddress(), ethers.id("BUIDL"), 86400, 0, 18)
).to.be.reverted;
});
it("V2 CORRECTS: a NavOracle feed pointed at AereNavOracleAdapter configures + reads without reverting", async () => {
const [owner] = await ethers.getSigners();
const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
await lo.waitForDeployment();
const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
await nav.waitForDeployment();
// Adapter references the (real) NavOracle for provenance but attests prices itself.
const adapter = await (await ethers.getContractFactory("AereNavOracleAdapter")).connect(owner).deploy(await nav.getAddress());
await adapter.waitForDeployment();
expect(await adapter.NAV_ORACLE()).to.equal(await nav.getAddress());
// The adapter implements the exact interface AereLendingOracle expects.
const adNames = adapter.interface.fragments.filter(f => f.type === "function").map(f => f.name);
expect(adNames).to.include("latestForAsset");
const asset = ethers.Wallet.createRandom().address;
const assetKey = ethers.id("BUIDL");
// Un-attested asset -> (0,0) -> LendingOracle treats as InvalidFeedResponse
// (the auto-poke's ts==0 check fires before the zero-price check).
const empty = await adapter.latestForAsset(assetKey);
expect(empty[0]).to.equal(0n);
expect(empty[1]).to.equal(0n);
await expect(
lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), assetKey, 86400, 0, 18)
).to.be.revertedWithCustomError(lo, "InvalidFeedResponse");
// Foundation attests a $1.02 NAV (18-dec convention).
const navValue = 102n * ONE / 100n; // 1.02e18
await adapter.connect(owner).setNav(assetKey, navValue);
const reading = await adapter.latestForAsset(assetKey);
expect(reading[0]).to.equal(navValue);
expect(reading[1]).to.be.gt(0n);
// NavOracle feed now configures WITHOUT reverting (F12 fixed).
await expect(
lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), assetKey, 86400, 500, 18)
).to.not.be.reverted;
// And getPrice returns the normalised NAV.
const price = await lo.getPrice(asset);
expect(price).to.equal(navValue);
// Sanity: the feed really is FeedType.NavOracle (1) pointed at the adapter.
const feed = await lo.feeds(asset);
expect(feed.feedType).to.equal(1n);
expect(feed.source).to.equal(await adapter.getAddress());
});
it("V2 guards: setNav is owner-only and rejects a zero NAV; staleness still enforced by the LendingOracle", async () => {
const [owner, stranger] = await ethers.getSigners();
const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
await nav.waitForDeployment();
const adapter = await (await ethers.getContractFactory("AereNavOracleAdapter")).connect(owner).deploy(await nav.getAddress());
await adapter.waitForDeployment();
const key = ethers.id("USDY");
await expect(adapter.connect(stranger).setNav(key, ONE)).to.be.revertedWith("Ownable: caller is not the owner");
await expect(adapter.connect(owner).setNav(key, 0)).to.be.revertedWithCustomError(adapter, "ZeroNav");
// Configure a NavOracle feed with a 1s staleness, then let it go stale.
const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
await lo.waitForDeployment();
await adapter.connect(owner).setNav(key, ONE);
const asset = ethers.Wallet.createRandom().address;
await lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), key, 1, 0, 18);
// fresh read works
expect(await lo.getPrice(asset)).to.equal(ONE);
// age it past staleness
await ethers.provider.send("evm_increaseTime", [5]);
await ethers.provider.send("evm_mine", []);
await expect(lo.getPrice(asset)).to.be.revertedWithCustomError(lo, "FeedStale");
});
});
// ─────────────────────────────────────────────────────────────────────────────
// F13
// ─────────────────────────────────────────────────────────────────────────────
async function deployAgenticStack(Version) {
// Version: "V1" (AereAgent + AERE402Facilitator) or "V2" (AereAgentV2 + AERE402FacilitatorV2)
const [deployer] = await ethers.getSigners();
const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
await WAERE.waitForDeployment();
const nonce = await ethers.provider.getTransactionCount(deployer.address);
const futureSaere = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 2 });
await WAERE.connect(deployer).deposit({ value: 1000n });
await WAERE.connect(deployer).approve(futureSaere, 1000n);
const sAERE = await (await ethers.getContractFactory("sAERE")).deploy(await WAERE.getAddress());
await sAERE.waitForDeployment();
const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
await burnVault.waitForDeployment();
const fakeRouter = await (await ethers.getContractFactory("WAERE")).deploy();
await fakeRouter.waitForDeployment();
const sink = await (await ethers.getContractFactory("AereSink")).deploy(
await WAERE.getAddress(),
await burnVault.getAddress(),
await sAERE.getAddress(),
await fakeRouter.getAddress(),
1500, 4000, 4500, 200, ethers.ZeroAddress
);
await sink.waitForDeployment();
const agentName = Version === "V2" ? "AereAgentV2" : "AereAgent";
const facName = Version === "V2" ? "AERE402FacilitatorV2" : "AERE402Facilitator";
const nonce2 = await ethers.provider.getTransactionCount(deployer.address);
const futureFacilitator = ethers.getCreateAddress({ from: deployer.address, nonce: nonce2 + 1 });
const agent = await (await ethers.getContractFactory(agentName)).deploy(futureFacilitator);
await agent.waitForDeployment();
const fac = await (await ethers.getContractFactory(facName)).deploy(await agent.getAddress(), await sink.getAddress());
await fac.waitForDeployment();
expect((await fac.getAddress()).toLowerCase()).to.equal(futureFacilitator.toLowerCase());
// cross-wiring
expect(await agent.AERE402_FACILITATOR()).to.equal(await fac.getAddress());
expect(await fac.AGENT_REGISTRY()).to.equal(await agent.getAddress());
return { WAERE, sink, agent, fac };
}
async function signAuth(signerWallet, fac, agentId, token, payee, amount, resourceId, nonce, deadline) {
const domain = {
name: "AERE402Facilitator",
version: "1",
chainId: (await ethers.provider.getNetwork()).chainId,
verifyingContract: await fac.getAddress(),
};
const types = {
PaymentAuth: [
{ name: "agentId", type: "uint256" },
{ name: "token", type: "address" },
{ name: "payee", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "resourceId", type: "bytes32" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
return signerWallet.signTypedData(domain, types, { agentId, token, payee, amount, resourceId, nonce, deadline });
}
async function setupAgent(agent, WAERE, operator) {
const signer = ethers.Wallet.createRandom();
const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 10n * ONE, 0, 0, "");
const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;
await WAERE.connect(operator).deposit({ value: 100n * ONE });
await WAERE.connect(operator).approve(await agent.getAddress(), 100n * ONE);
await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 100n * ONE);
return { signer, agentId };
}
describe("F13 AERE402Facilitator per-agent nonce collision — V1 reproduces / V2 corrects", function () {
this.timeout(120000);
it("V1 REPRODUCES: two payees, same nonce -> provider B's valid settlement reverts NonceAlreadyConsumed", async () => {
const [, operator, , payeeA, payeeB] = await ethers.getSigners();
const { WAERE, agent, fac } = await deployAgenticStack("V1");
const { signer, agentId } = await setupAgent(agent, WAERE, operator);
const amount = 2n * ONE;
const token = await WAERE.getAddress();
const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
const sigA = await signAuth(signer, fac, agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline);
await fac.connect(payeeA).settle(agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline, sigA);
expect(await fac.consumed(agentId, 1n)).to.equal(true);
// Provider B, own monotonic nonce=1, DIFFERENT payee, fully valid -> collides.
const sigB = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
await expect(
fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigB)
).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
});
it("V2 CORRECTS: two payees, same nonce BOTH settle; true replay (same agentId+payee+nonce) still reverts", async () => {
const [, operator, , payeeA, payeeB] = await ethers.getSigners();
const { WAERE, agent, fac } = await deployAgenticStack("V2");
const { signer, agentId } = await setupAgent(agent, WAERE, operator);
const amount = 2n * ONE;
const token = await WAERE.getAddress();
const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
// Both providers use nonce=1 (per-provider monotonic convention).
const sigA = await signAuth(signer, fac, agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline);
await fac.connect(payeeA).settle(agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline, sigA);
const balBBefore = await WAERE.balanceOf(payeeB.address);
const sigB = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
await fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigB);
// Per-payee namespace: both consumed independently.
expect(await fac.consumed(agentId, payeeA.address, 1n)).to.equal(true);
expect(await fac.consumed(agentId, payeeB.address, 1n)).to.equal(true);
// Provider B actually got paid (amount - 25bps fee).
const fee = amount * 25n / 10_000n;
expect(await WAERE.balanceOf(payeeB.address)).to.equal(balBBefore + (amount - fee));
// TRUE replay still blocked: same (agentId, payeeB, nonce=1).
const sigReplay = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
await expect(
fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigReplay)
).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
});
});