aere-contracts/test/audit-verify-402-nonce.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

116 lines
5.5 KiB
JavaScript

// AUDIT VERIFICATION — Finding 13
// AERE402Facilitator replay key is consumed[agentId][nonce] (GLOBAL per agent,
// not per payee). Two providers following the documented "monotonic nonce per
// provider-agent pair" convention collide, and a valid settlement reverts.
// Uses the SAME real deploy stack as test/agentic.test.js. No sources modified.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const ONE = 10n ** 18n;
async function deployStack() {
const [deployer, operator, signer, payeeA, payeeB] = 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 nonce2 = await ethers.provider.getTransactionCount(deployer.address);
const futureFacilitator = ethers.getCreateAddress({ from: deployer.address, nonce: nonce2 + 1 });
const agent = await (await ethers.getContractFactory("AereAgent")).deploy(futureFacilitator);
await agent.waitForDeployment();
const fac = await (await ethers.getContractFactory("AERE402Facilitator")).deploy(
await agent.getAddress(),
await sink.getAddress()
);
await fac.waitForDeployment();
expect((await fac.getAddress()).toLowerCase()).to.equal(futureFacilitator.toLowerCase());
return { deployer, operator, signer, payeeA, payeeB, 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" },
],
};
const value = { agentId, token, payee, amount, resourceId, nonce, deadline };
return signerWallet.signTypedData(domain, types, value);
}
describe("AUDIT VERIFY — F13 AERE402Facilitator per-agent (not per-payee) nonce collision", function () {
it("CONFIRMED: two providers using the same monotonic nonce collide; provider B's valid settlement reverts", async () => {
const { operator, signer, payeeA, payeeB, WAERE, agent, fac } = await deployStack();
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;
// Fund the agent generously so balance is NOT the limiting factor.
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);
const amount = 2n * ONE;
const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
const token = await WAERE.getAddress();
// Provider A: agentId, nonce=1, payee = payeeA. (A's first invoice.)
const sigA = await signAuth(signer, fac, agentId, token, payeeA.address, amount, ethers.id("providerA:/gpu"), 1n, deadline);
await fac.connect(payeeA).settle(agentId, token, payeeA.address, amount, ethers.id("providerA:/gpu"), 1n, deadline, sigA);
expect(await fac.consumed(agentId, 1n)).to.equal(true);
// Provider B: a DIFFERENT, fully valid payment to payeeB, but with B's own
// monotonic nonce=1 (per the documented per-provider convention). The agent
// signed it and has ample balance — yet it reverts because the nonce key is
// global per agent, so A already consumed (agentId, 1).
const sigB = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("providerB:/api"), 1n, deadline);
await expect(
fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("providerB:/api"), 1n, deadline, sigB)
).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
// Control: provider B succeeds only by picking a non-colliding nonce (proving
// it was the shared nonce namespace, not signature/balance, that blocked it).
const sigB2 = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("providerB:/api"), 2n, deadline);
await fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("providerB:/api"), 2n, deadline, sigB2);
expect(await fac.consumed(agentId, 2n)).to.equal(true);
});
});