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.
257 lines
13 KiB
JavaScript
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");
|
|
});
|
|
});
|