// Hardhat tests for AereAgent + AERE402Facilitator. // // Covers: // - Agent registration with signer + passkey + rate limits // - Operator rotation, signer rotation, pause/unpause // - topUp / withdraw lifecycle // - Direct debit() rejected from non-facilitator // - EIP-712 settle happy path: split payee/sink, nonce consumed, event // - Nonce replay rejection (NonceAlreadyConsumed) // - Deadline rejection // - Wrong-payee rejection (PayeeMismatch) // - Bad signature rejection (InvalidSignature) // - Per-call cap enforcement // - Per-hour cap enforcement (windowed) // - Pause blocks settlement // // Run: npx hardhat test test/agentic.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE = 10n ** 18n; async function deployStack() { const [deployer, operator, signer, payee, alice] = await ethers.getSigners(); // WAERE const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy(); await WAERE.waitForDeployment(); // sAERE seed 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(); // Burn vault + dummy router for AereSink 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(); // Pre-compute the facilitator address. const nonce2 = await ethers.provider.getTransactionCount(deployer.address); const futureFacilitator = ethers.getCreateAddress({ from: deployer.address, nonce: nonce2 + 1 }); // AereAgent registry (immutable facilitator address). const agent = await (await ethers.getContractFactory("AereAgent")).deploy(futureFacilitator); await agent.waitForDeployment(); // Facilitator. 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, payee, alice, 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("AereAgent + AERE402Facilitator", function () { it("registers an agent with rate limits + signer + passkey", async function () { const { agent, operator, signer } = await deployStack(); const tx = await agent.connect(operator).registerAgent( signer.address, ethers.ZeroHash, ethers.ZeroHash, 1n * ONE, // perCall: 1 AERE max 100n * ONE, // perHour: 100 AERE max 1000n * ONE, // perDay: 1000 AERE max "ipfs://QmAgentMeta" ); const id = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId; const a = await agent.agents(id); expect(a.registered).to.equal(true); expect(a.operator).to.equal(operator.address); expect(a.signer).to.equal(signer.address); expect(a.perCallCap).to.equal(1n * ONE); expect(a.metadataUri).to.equal("ipfs://QmAgentMeta"); }); it("debit rejected from non-facilitator", async function () { const { agent, operator, signer, payee, alice } = await deployStack(); const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, ""); const id = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId; await expect( agent.connect(alice).debit(id, ethers.ZeroAddress, payee.address, 1n) ).to.be.revertedWithCustomError(agent, "NotFacilitator"); }); it("EIP-712 settle: split payee/sink, nonce consumed", async function () { const { deployer, operator, signer, payee, WAERE, agent, fac, sink } = await deployStack(); // Register agent. 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; // Top up 100 WAERE. 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); expect(await agent.balances(agentId, await WAERE.getAddress())).to.equal(100n * ONE); const amount = 5n * ONE; const resourceId = ethers.id("GET /v1/inference/claude"); const nonce = 1n; const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline); const payeeBefore = await WAERE.balanceOf(payee.address); await fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline, sig); const fee = (amount * 25n) / 10_000n; const toPayee = amount - fee; expect(await WAERE.balanceOf(payee.address) - payeeBefore).to.equal(toPayee); expect(await fac.consumed(agentId, nonce)).to.equal(true); // Nonce replay rejected. await expect( fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline, sig) ).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed"); }); it("rejects wrong-payee submission", async function () { const { operator, signer, payee, alice, 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; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const amount = 1n * ONE; const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, amount, ethers.ZeroHash, 1n, deadline); // alice tries to settle a payment intended for payee. await expect( fac.connect(alice).settle(agentId, await WAERE.getAddress(), payee.address, amount, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(fac, "PayeeMismatch"); }); it("rejects expired deadline", async function () { const { operator, signer, payee, 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; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const deadline = 1; // expired const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline); await expect( fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(fac, "DeadlinePassed"); }); it("rejects bad signature (different signer)", async function () { const { operator, signer, payee, alice, 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; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; // alice signs instead of the registered signer. const sig = await signAuth(alice, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline); await expect( fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(fac, "InvalidSignature"); }); it("per-call cap enforced", async function () { const { operator, signer, payee, WAERE, agent, fac } = await deployStack(); // Cap at 1 WAERE per call. const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 1n * ONE, 0, 0, ""); const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 2n * ONE, ethers.ZeroHash, 1n, deadline); await expect( fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 2n * ONE, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(agent, "PerCallCapExceeded"); }); it("paused agent rejected at settle time", async function () { const { operator, signer, payee, 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; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); await agent.connect(operator).setPaused(agentId, true); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline); await expect( fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(agent, "AgentPausedErr"); }); it("operator can rotate signer", async function () { const { operator, signer, payee, alice, agent } = await deployStack(); const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, ""); const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId; await agent.connect(operator).setSigner(agentId, alice.address); expect(await agent.signerOf(agentId)).to.equal(alice.address); }); it("M3 fix: payee == signer rejected (self-deal forbidden)", async function () { const { operator, signer, 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; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600; // Signer is also the payee — compromised key drain attempt. const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), signer.address, 1n * ONE, ethers.ZeroHash, 1n, deadline); await expect( fac.connect(signer).settle(agentId, await WAERE.getAddress(), signer.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig) ).to.be.revertedWithCustomError(fac, "SelfDealForbidden"); }); it("withdraw returns idle balance to operator-chosen recipient", async function () { const { operator, signer, alice, WAERE, agent } = await deployStack(); const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, ""); const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId; await WAERE.connect(operator).deposit({ value: 10n * ONE }); await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE); await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE); const before = await WAERE.balanceOf(alice.address); await agent.connect(operator).withdraw(agentId, await WAERE.getAddress(), 4n * ONE, alice.address); expect(await WAERE.balanceOf(alice.address) - before).to.equal(4n * ONE); expect(await agent.balances(agentId, await WAERE.getAddress())).to.equal(6n * ONE); }); });