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