// ERC-8004 + x402/AP2 conformance adapters over Aere's LIVE, already-deployed agent stack. // // The three adapters wrap (never redeploy) the live primitives: // - AereIdentityRegistry8004 over AereAgentDID (agent identity / DID) // - AereReputationRegistry8004 over AereAIReputation (attest / read feedback) // - AereValidationRegistry8004 over AerePQCKeyRegistry (POST-QUANTUM request/respond proof) // plus AereAP2MandateVerifier: a Falcon-512-signed x402/AP2 spend mandate. // // Aere has no PQC precompile in the local Hardhat EVM, so we install the repo's MockPQCPrecompile // at 0x0AE1 (Falcon-512) and 0x0AE3 (ML-DSA-44) via hardhat_setCode, exactly as the fault-harness // AereRecoveryRegistry test does. The mock parses the input at the LIVE precompile's spec offsets // and accepts iff commitment == keccak256(pk || message), so a positive result here proves the // adapter routed the correct input to the correct precompile and enforces fail-closed semantics. // // [MEASURE] The REAL precompile path (that on-chain 0x0AE1 / 0x0AE3 accept a genuine Falcon-512 / // ML-DSA-44 signature) is exercised separately against LIVE mainnet 2800 via eth_call (the same way // AerePQCKeyRegistry / AerePQCAttestation are proven) and is NOT re-proven inside this unit test. // The adapters stay hard-wired to the live registry / precompiles for production. // // Run: npx hardhat test test/erc8004-adapters.test.js const { expect } = require("chai"); const { ethers, network } = require("hardhat"); const PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44 }; const META = { 1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true }, // Falcon-512 3: { pkLen: 1312, sigLen: 2420, falcon: false }, // ML-DSA-44 }; const REG_STATUS_ACTIVE = 1; // ---- deterministic seeded RNG (keccak hash-chain) so the test is fully reproducible ---- function makeRng(seedText) { let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); return { bytes(n) { const out = new Uint8Array(n); let i = 0; while (i < n) { seed = ethers.keccak256(seed); const chunk = ethers.getBytes(seed); for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j]; } return out; }, }; } function makePubKey(scheme, rng) { const m = META[scheme]; const raw = rng.bytes(m.pkLen); if (m.falcon) raw[0] = m.pkHeader; // Falcon requires the header byte return ethers.hexlify(raw); } function commitmentFor(pubKey, message) { return ethers.keccak256(ethers.concat([pubKey, message])); } // A genuine PQC signature envelope binding `message` to `pubKey` under the mock's semantics. function genuine(scheme, pubKey, message, rng) { const m = META[scheme]; const commitment = commitmentFor(pubKey, message); if (m.falcon) { // Falcon: envelope = nonce(40) || esig ; esig = esigHeader || commitment(32) => 73 bytes. const nonce = rng.bytes(40); const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); return ethers.hexlify(ethers.concat([nonce, esig])); } // ML-DSA / fixed: sig = commitment(32) || filler ; total sigLen bytes. const filler = rng.bytes(m.sigLen - 32); return ethers.hexlify(ethers.concat([commitment, filler])); } // Flip one byte inside the commitment region (a genuine forgery the precompile must reject). function tamper(scheme, sigHex) { const b = ethers.getBytes(sigHex); const off = META[scheme].falcon ? 41 : 0; // Falcon commitment starts at 41; ML-DSA at 0. b[off] ^= 0x01; return ethers.hexlify(b); } async function futureTs(secs) { const b = await ethers.provider.getBlock("latest"); return b.timestamp + secs; } describe("ERC-8004 + x402/AP2 adapters over Aere's live agent stack", function () { let owner, other, foundation; let reg, did, identity, validation, mandate; let mockCode; before(async function () { [owner, other, foundation] = await ethers.getSigners(); mockCode = {}; const F = await ethers.getContractFactory("MockPQCPrecompile"); for (const scheme of [1, 3]) { const mock = await F.deploy(scheme); await mock.waitForDeployment(); mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress()); } }); async function installMocks() { for (const scheme of [1, 3]) { await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]); } } // Register a PQC key under `signer` in the live registry (proof-of-possession via the precompile). async function registerKey(scheme, signer, rng) { const pk = makePubKey(scheme, rng); const nonce = await reg.identityNonce(signer.address); const ch = await reg.popChallenge(signer.address, scheme, pk, nonce); const tx = await reg.connect(signer).registerKey(scheme, pk, genuine(scheme, pk, ch, rng)); const rc = await tx.wait(); const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered"); return { keyId: Number(log.args.keyId), pk, scheme }; } // A Falcon root -> DID agent -> registered ERC-8004 identity, all over the live primitives. async function makeAgent(seed) { const rng = makeRng(seed); const root = await registerKey(1, owner, rng); await did.connect(owner).createAgent(root.keyId); const domain = `agent-${seed}.aere.network`; const card = `https://${domain}/.well-known/agent-card.json`; await identity.connect(owner).register(root.keyId, domain, card); return { rng, agentId: root.keyId, rootPk: root.pk, domain, card }; } beforeEach(async function () { await installMocks(); reg = await (await ethers.getContractFactory("AerePQCKeyRegistry")).deploy(); await reg.waitForDeployment(); did = await (await ethers.getContractFactory("AereAgentDID")).deploy( await reg.getAddress(), ethers.ZeroAddress, // agentBond cross-link (informational; unused by the adapters) ethers.ZeroAddress // aiReputation cross-link (informational) ); await did.waitForDeployment(); identity = await (await ethers.getContractFactory("AereIdentityRegistry8004")).deploy(await did.getAddress()); await identity.waitForDeployment(); validation = await (await ethers.getContractFactory("AereValidationRegistry8004")).deploy( await reg.getAddress(), await did.getAddress() ); await validation.waitForDeployment(); // Mandate verifier with an OPEN accounting path (SETTLEMENT_EXECUTOR = 0) for verification. mandate = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy( await reg.getAddress(), ethers.ZeroAddress ); await mandate.waitForDeployment(); }); // ========================================================================= // 1. Identity adapter over AereAgentDID // ========================================================================= describe("AereIdentityRegistry8004 (identity: register / resolve)", function () { it("wires the live DID and reads the registry from it", async function () { expect(await identity.DID()).to.equal(await did.getAddress()); expect(await identity.KEY_REGISTRY()).to.equal(await reg.getAddress()); }); it("registers an existing DID agent with a DID string + AgentCard URI, and resolves it", async function () { const a = await makeAgent("id-1"); const info = await identity.getAgent(a.agentId); expect(info.agentId).to.equal(BigInt(a.agentId)); expect(info.did).to.equal(`did:aere:31337:${a.agentId}`); // chainId 31337 in Hardhat expect(info.agentDomain).to.equal(a.domain); expect(info.agentCardURI).to.equal(a.card); expect(info.agentAddress).to.equal(owner.address); // current controller, read live from the registry expect(info.registered).to.equal(true); // Resolve by domain and by controller address. const byDomain = await identity.resolveByDomain(a.domain); expect(byDomain.agentId).to.equal(BigInt(a.agentId)); const byAddr = await identity.resolveByAddress(owner.address); expect(byAddr.agentId).to.equal(BigInt(a.agentId)); expect(await identity.isRegistered(a.agentId)).to.equal(true); expect(await identity.agentCount()).to.equal(1n); }); it("only the agent's controller (Falcon-root owner) can register / update", async function () { const rng = makeRng("id-ctrl"); const root = await registerKey(1, owner, rng); await did.connect(owner).createAgent(root.keyId); // A stranger cannot bind metadata to someone else's agent. await expect( identity.connect(other).register(root.keyId, "evil.aere.network", "ipfs://x") ).to.be.revertedWithCustomError(identity, "NotController"); // The controller can, then update. await identity.connect(owner).register(root.keyId, "ok.aere.network", "ipfs://card1"); await expect( identity.connect(other).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2") ).to.be.revertedWithCustomError(identity, "NotController"); await identity.connect(owner).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2"); const info = await identity.getAgent(root.keyId); expect(info.agentDomain).to.equal("ok2.aere.network"); expect(info.agentCardURI).to.equal("ipfs://card2"); }); it("reverts for an unknown DID agent and rejects duplicate domains", async function () { await expect(identity.connect(owner).register(999, "d.aere.network", "u")).to.be.revertedWithCustomError( identity, "UnknownAgent" ); const a = await makeAgent("id-dup"); const rng = makeRng("id-dup2"); const root = await registerKey(1, owner, rng); await did.connect(owner).createAgent(root.keyId); await expect(identity.connect(owner).register(root.keyId, a.domain, "u")).to.be.revertedWithCustomError( identity, "DomainTaken" ); }); }); // ========================================================================= // 2. Reputation adapter over AereAIReputation (attest / read) // ========================================================================= describe("AereReputationRegistry8004 (reputation: attest / read feedback)", function () { let token, sink, bond, rep, repAdapter; async function deployRep(attestorAdapter) { token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("AERE", "AERE", 18); await token.waitForDeployment(); sink = await (await ethers.getContractFactory("MockSinkSimple")).deploy(); await sink.waitForDeployment(); bond = await (await ethers.getContractFactory("AereAgentBond")).deploy( await token.getAddress(), await sink.getAddress(), [foundation.address] ); await bond.waitForDeployment(); if (attestorAdapter) { // The live AereAIReputation attestor set is fixed at deploy with no add path, so to let the // adapter WRITE feedback through it, the reputation instance must list the adapter as an // attestor. Predict the adapter address so it can be authorized at reputation deploy. const nonce = await owner.getNonce(); const predicted = ethers.getCreateAddress({ from: owner.address, nonce: nonce + 1 }); rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [predicted]); await rep.waitForDeployment(); repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy( await rep.getAddress(), await identity.getAddress() ); await repAdapter.waitForDeployment(); expect(await repAdapter.getAddress()).to.equal(predicted); // M1: giveFeedback is gated to an owner-curated author allowlist. Authorize `other` so the // existing feedback flows below (which submit as `other`) exercise the write path. await repAdapter.connect(owner).addFeedbackAuthor(other.address); } else { // Reputation that does NOT authorize the adapter (mirrors the immutable mainnet Foundation set). rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [ foundation.address, ]); await rep.waitForDeployment(); repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy( await rep.getAddress(), await identity.getAddress() ); await repAdapter.waitForDeployment(); // M1: authorize `other` as a feedback author (owner-curated allowlist). await repAdapter.connect(owner).addFeedbackAuthor(other.address); } } it("forwards ERC-8004 feedback into the live reputation and reads the score back", async function () { const a = await makeAgent("rep-1"); await deployRep(true); // Maps the ERC-8004 agentId to exactly one live (operator, agentKey) slot. const [operator, agentKey] = await repAdapter.liveKeyOf(a.agentId); expect(operator).to.equal(owner.address); // the agent's controller expect(agentKey).to.equal(ethers.zeroPadValue(ethers.toBeHex(a.agentId), 32)); expect(await repAdapter.canWriteThrough()).to.equal(true); // Attest a positive interaction through the adapter; it forwards to the live attest. const ev1 = ethers.keccak256(ethers.toUtf8Bytes("evidence-1")); await expect(repAdapter.connect(other).giveFeedback(a.agentId, 1, ev1, "ipfs://ev1")).to.emit( repAdapter, "NewFeedback" ); // Read the canonical live score straight from AereAIReputation via the adapter. // scoreOf = positiveCount*10 (no slash, no dispute) => 1 positive => 10. expect(await repAdapter.getScore(a.agentId)).to.equal(10n); const stats = await repAdapter.getStats(a.agentId); expect(stats.positiveCount).to.equal(1n); expect(stats.disputeCount).to.equal(0n); // A dispute (-1) lowers the score; two positives raise it. const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-2")); await repAdapter.connect(other).giveFeedback(a.agentId, 1, ev2, ""); const ev3 = ethers.keccak256(ethers.toUtf8Bytes("evidence-3")); await repAdapter.connect(other).giveFeedback(a.agentId, -1, ev3, ""); // positive=2 (=>20), dispute=1 (=>-20) => 0. expect(await repAdapter.getScore(a.agentId)).to.equal(0n); const stats2 = await repAdapter.getStats(a.agentId); expect(stats2.positiveCount).to.equal(2n); expect(stats2.disputeCount).to.equal(1n); }); it("rejects feedback for an unregistered agent and an out-of-range delta", async function () { await deployRep(true); // `other` is authorized as a feedback author in deployRep await expect(repAdapter.connect(other).giveFeedback(12345, 1, ethers.id("x"), "")).to.be.revertedWithCustomError( repAdapter, "AgentNotRegistered" ); const a = await makeAgent("rep-bad"); await expect(repAdapter.connect(other).giveFeedback(a.agentId, 2, ethers.id("x"), "")).to.be.revertedWithCustomError( repAdapter, "InvalidDelta" ); }); // M1 (MEDIUM) fix: giveFeedback is gated to an owner-curated allowlist so the adapter cannot be an // open reputation firehose once it is a live attestor. Proves: an unauthorized caller reverts, an // authorized caller succeeds, revocation re-blocks, and only the owner may manage the allowlist. it("gates giveFeedback to owner-authorized feedback authors (M1)", async function () { const a = await makeAgent("rep-gate"); await deployRep(true); // adapter is a live attestor; deployRep authorizes `other`, not `foundation` expect(await repAdapter.canWriteThrough()).to.equal(true); // An UNAUTHORIZED caller cannot write, even though the adapter is a live attestor. expect(await repAdapter.isAuthorizedFeedbackAuthor(foundation.address)).to.equal(false); const ev = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate")); await expect( repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "") ).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor"); expect(await repAdapter.getScore(a.agentId)).to.equal(0n); // nothing written // Owner authorizes `foundation`; the SAME call now succeeds and moves the live score. await expect(repAdapter.connect(owner).addFeedbackAuthor(foundation.address)) .to.emit(repAdapter, "FeedbackAuthorSet") .withArgs(foundation.address, true); await expect(repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "")).to.emit(repAdapter, "NewFeedback"); expect(await repAdapter.getScore(a.agentId)).to.equal(10n); // Owner revokes; the caller is fail-closed blocked again. await repAdapter.connect(owner).removeFeedbackAuthor(foundation.address); const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate-2")); await expect( repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev2, "") ).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor"); // The author allowlist is owner-only (OZ v4 Ownable: "Ownable: caller is not the owner"). await expect(repAdapter.connect(other).addFeedbackAuthor(other.address)).to.be.reverted; }); it("is honest about the immutable attestor set: write-through reverts when the adapter is not authorized", async function () { const a = await makeAgent("rep-noauth"); await deployRep(false); // reputation lists Foundation only, not the adapter (the mainnet case) expect(await repAdapter.canWriteThrough()).to.equal(false); // `other` is an authorized feedback author (deployRep), so it clears the caller gate and reaches // the fail-closed attestor check on the live reputation. await expect( repAdapter.connect(other).giveFeedback(a.agentId, 1, ethers.id("ev"), "") ).to.be.revertedWithCustomError(repAdapter, "AdapterNotAttestor"); // The READ path still adapts the live contract with no authorization: score starts at 0. expect(await repAdapter.getScore(a.agentId)).to.equal(0n); }); }); // ========================================================================= // 3. Validation adapter over the live PQC precompiles (the differentiator) // ========================================================================= describe("AereValidationRegistry8004 (validation: request/respond with a PQC proof)", function () { it("accepts a genuine Falcon-512 (0x0AE1) validation response and records it", async function () { const a = await makeAgent("val-falcon"); const rng = makeRng("val-falcon-key"); const v = await registerKey(1, other, rng); // a Falcon-512 validator key expect(await reg.statusOf(v.keyId)).to.equal(REG_STATUS_ACTIVE); const dataHash = ethers.id("model-output-commitment-A"); const tx = await validation.requestValidation(v.keyId, a.agentId, dataHash); const rc = await tx.wait(); const reqLog = rc.logs.map((l) => validation.interface.parseLog(l)).find((p) => p && p.name === "ValidationRequest"); const requestId = reqLog.args.requestId; const response = 92; // ERC-8004 score 0..100 const challenge = await validation.responseChallenge(requestId, response); const proof = genuine(1, v.pk, challenge, rng); await expect(validation.respondValidation(requestId, response, proof)) .to.emit(validation, "ValidationResponse") .withArgs(requestId, v.keyId, a.agentId, response, 1); expect(await validation.isValidated(requestId)).to.equal(true); const rec = await validation.getValidation(requestId); expect(rec.status).to.equal(2); // Completed expect(rec.response).to.equal(92n); expect(rec.pqcScheme).to.equal(1n); expect(rec.serverAgentId).to.equal(BigInt(a.agentId)); }); it("accepts a genuine ML-DSA-44 (0x0AE3) validation response too", async function () { const a = await makeAgent("val-mldsa"); const rng = makeRng("val-mldsa-key"); const v = await registerKey(3, other, rng); // an ML-DSA-44 validator key const dataHash = ethers.id("model-output-commitment-B"); const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash); await validation.requestValidation(v.keyId, a.agentId, dataHash); const response = 77; const challenge = await validation.responseChallenge(requestId, response); const proof = genuine(3, v.pk, challenge, rng); await expect(validation.respondValidation(requestId, response, proof)) .to.emit(validation, "ValidationResponse") .withArgs(requestId, v.keyId, a.agentId, response, 3); expect(await validation.isValidated(requestId)).to.equal(true); }); it("REJECTS a tampered proof fail-closed (nothing recorded)", async function () { const a = await makeAgent("val-tamper"); const rng = makeRng("val-tamper-key"); const v = await registerKey(1, other, rng); const dataHash = ethers.id("model-output-commitment-C"); const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash); await validation.requestValidation(v.keyId, a.agentId, dataHash); const response = 88; const challenge = await validation.responseChallenge(requestId, response); const good = genuine(1, v.pk, challenge, rng); const bad = tamper(1, good); await expect(validation.respondValidation(requestId, response, bad)).to.be.revertedWithCustomError( validation, "PQCValidationFailed" ); // Fail-closed: request stays Pending, nothing recorded. expect(await validation.isValidated(requestId)).to.equal(false); const rec = await validation.getValidation(requestId); expect(rec.status).to.equal(1); // Pending }); it("REJECTS a proof bound to a DIFFERENT response score (binding), and treats a stale precompile as invalid", async function () { const a = await makeAgent("val-bind"); const rng = makeRng("val-bind-key"); const v = await registerKey(1, other, rng); const dataHash = ethers.id("model-output-commitment-D"); const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash); await validation.requestValidation(v.keyId, a.agentId, dataHash); // Sign the challenge for response=50, then try to submit response=51 with it. const challenge50 = await validation.responseChallenge(requestId, 50); const proof50 = genuine(1, v.pk, challenge50, rng); await expect(validation.respondValidation(requestId, 51, proof50)).to.be.revertedWithCustomError( validation, "PQCValidationFailed" ); // Wipe the precompile (pre-fork / stale chain): an empty account returns empty data => invalid. await network.provider.send("hardhat_setCode", [PRECOMPILE[1], "0x"]); await expect(validation.respondValidation(requestId, 50, proof50)).to.be.revertedWithCustomError( validation, "PQCValidationFailed" ); expect(await validation.isValidated(requestId)).to.equal(false); }); it("rejects requests with a non-PQC / inactive validator key or an unknown server agent", async function () { const a = await makeAgent("val-guard"); const rng = makeRng("val-guard-key"); const v = await registerKey(1, other, rng); // Unknown server agent. await expect(validation.requestValidation(v.keyId, 424242, ethers.id("d"))).to.be.revertedWithCustomError( validation, "ServerAgentUnknown" ); // Revoked validator key is no longer ACTIVE. await reg.connect(other).revokeKey(v.keyId); await expect(validation.requestValidation(v.keyId, a.agentId, ethers.id("d"))).to.be.revertedWithCustomError( validation, "ValidatorKeyNotActive" ); }); }); // ========================================================================= // 4. x402 / AP2 post-quantum mandate verifier // ========================================================================= describe("AereAP2MandateVerifier (Falcon-512-signed x402/AP2 spend mandate)", function () { async function makeMandate(overrides = {}) { const rng = makeRng(overrides.seed || "mandate-key"); const agent = await registerKey(1, owner, rng); // the paying agent's Falcon-512 key const start = overrides.periodStart ?? (await futureTs(-10)); const end = overrides.periodEnd ?? (await futureTs(3600)); const m = { agentKeyId: agent.keyId, token: overrides.token ?? "0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8", // live WAERE address (label only) maxAmount: overrides.maxAmount ?? ethers.parseUnits("10", 18), periodStart: start, periodEnd: end, purpose: overrides.purpose ?? ethers.id("inference:anthropic"), mandateNonce: overrides.mandateNonce ?? 0, }; return { rng, agent, m }; } it("verifies a genuine Falcon mandate signature (pure view) and authorizes spend up to the cap", async function () { const { rng, agent, m } = await makeMandate(); const mh = await mandate.mandateHash(m); const sig = genuine(1, agent.pk, mh, rng); expect(await mandate.verifyMandate(m, sig)).to.equal(true); // Authorize two spends within the 10-unit cap. await expect(mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig)).to.emit(mandate, "MandateAuthorized"); await mandate.authorizeSpend(m, ethers.parseUnits("3", 18), sig); expect(await mandate.spentUnder(mh)).to.equal(ethers.parseUnits("7", 18)); expect(await mandate.remainingUnder(m)).to.equal(ethers.parseUnits("3", 18)); }); it("rejects spend over the cumulative cap", async function () { const { rng, agent, m } = await makeMandate({ maxAmount: ethers.parseUnits("5", 18) }); const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng); await mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig); await expect(mandate.authorizeSpend(m, ethers.parseUnits("2", 18), sig)).to.be.revertedWithCustomError( mandate, "MandateCapExceeded" ); }); it("rejects an expired mandate window", async function () { const { rng, agent, m } = await makeMandate({ periodStart: await futureTs(-100), periodEnd: await futureTs(-10) }); const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng); await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError( mandate, "MandateExpired" ); }); it("REJECTS a tampered mandate signature fail-closed (verifyMandate false, authorize reverts)", async function () { const { rng, agent, m } = await makeMandate(); const good = genuine(1, agent.pk, await mandate.mandateHash(m), rng); const bad = tamper(1, good); expect(await mandate.verifyMandate(m, bad)).to.equal(false); await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), bad)).to.be.revertedWithCustomError( mandate, "MandateSignatureInvalid" ); expect(await mandate.spentUnder(await mandate.mandateHash(m))).to.equal(0n); }); it("halts the mandate the instant the Falcon key is revoked (root lifecycle)", async function () { const { rng, agent, m } = await makeMandate({ seed: "mandate-revoke" }); const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng); await mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig); // works before revoke await reg.connect(owner).revokeKey(agent.keyId); expect(await mandate.verifyMandate(m, sig)).to.equal(false); await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError( mandate, "AgentKeyNotActiveFalcon" ); }); it("enforces the settlement-executor gate when one is configured", async function () { // A verifier wired to a specific executor rejects authorizeSpend from anyone else. const gated = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy( await reg.getAddress(), other.address // SETTLEMENT_EXECUTOR ); await gated.waitForDeployment(); const { rng, agent, m } = await makeMandate({ seed: "mandate-gated" }); const sig = genuine(1, agent.pk, await gated.mandateHash(m), rng); await expect(gated.connect(owner).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError( gated, "NotSettlementExecutor" ); // The configured executor can. await expect(gated.connect(other).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.emit( gated, "MandateAuthorized" ); }); }); });