aere-contracts/test/agentic.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

291 lines
14 KiB
JavaScript

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