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

290 lines
12 KiB
JavaScript

// Hardhat tests for the AereProof v0 stack:
// - AereSanctionsRegistry (Merkle attested sanctions list)
// - ChainalysisOracleWrapper (read-only forwarding wrapper)
// - AereTravelRuleHashRegistry (FATF Travel Rule on-chain anchor)
// - AereForensicEventRegistry (Forta-style detection bot registry)
//
// Run: npx hardhat test test/aere-proof-v0.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const LIST_OFAC_SDN = 1;
function sanctionsLeaf(addr, listId) {
return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
["address", "uint16"], [addr, listId]
));
}
function buildSimpleTree(leaves) {
let layer = leaves.slice().map(l => l.toLowerCase());
layer.sort();
const layers = [layer];
while (layer.length > 1) {
const next = [];
for (let i = 0; i < layer.length; i += 2) {
const a = layer[i];
const b = layer[i + 1] ?? layer[i];
const [lo, hi] = a < b ? [a, b] : [b, a];
next.push(ethers.keccak256(ethers.concat([lo, hi])));
}
layer = next;
layers.push(layer);
}
return { root: layer[0], layers };
}
function proofFor(tree, leaf) {
const proof = [];
let target = leaf.toLowerCase();
for (let i = 0; i < tree.layers.length - 1; i++) {
const lay = tree.layers[i];
const idx = lay.indexOf(target);
if (idx < 0) throw new Error("leaf not in layer");
const sib = idx ^ 1;
if (sib < lay.length) proof.push(lay[sib]);
const a = lay[idx];
const b = lay[sib] ?? lay[idx];
const [lo, hi] = a < b ? [a, b] : [b, a];
target = ethers.keccak256(ethers.concat([lo, hi]));
}
return proof;
}
describe("AereSanctionsRegistry", function () {
let reg, deployer, alice, sanctioned1, sanctioned2;
beforeEach(async function () {
[deployer, alice, sanctioned1, sanctioned2] = await ethers.getSigners();
reg = await (await ethers.getContractFactory("AereSanctionsRegistry")).deploy();
await reg.waitForDeployment();
});
it("proposes, attests after 24h, verifies sanctioned status", async function () {
const leaves = [
sanctionsLeaf(sanctioned1.address, LIST_OFAC_SDN),
sanctionsLeaf(sanctioned2.address, LIST_OFAC_SDN),
];
const tree = buildSimpleTree(leaves);
await reg.proposeSnapshot(1, tree.root, "https://www.treasury.gov/ofac/downloads/sdnlist.txt", "ipfs://QmTest");
// Cannot attest early.
await expect(reg.attest(1)).to.be.revertedWithCustomError(reg, "TimelockNotElapsed");
await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
await reg.attest(1);
// sanctioned1 is on the list.
const ok = await reg.isSanctioned(1, sanctioned1.address, LIST_OFAC_SDN, proofFor(tree, leaves[0]));
expect(ok).to.equal(true);
// alice is NOT on the list — even with bogus proof, false.
const notOk = await reg.isSanctioned(1, alice.address, LIST_OFAC_SDN, []);
expect(notOk).to.equal(false);
});
it("challenge → dismiss → attest path", async function () {
await reg.proposeSnapshot(2, ethers.id("root2"), "url", "");
await reg.connect(alice).challenge(2, "incorrect inclusion");
await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
await ethers.provider.send("evm_mine", []);
// Cannot attest while challenged.
await expect(reg.attest(2)).to.be.revertedWithCustomError(reg, "WrongState");
// Owner dismisses.
await reg.dismissChallenge(2, 0, "audit log shows correct");
await reg.attest(2);
});
it("monotonic epoch rejection", async function () {
await reg.proposeSnapshot(5, ethers.ZeroHash, "", "");
await expect(reg.proposeSnapshot(5, ethers.id("other"), "", "")).to.be.revertedWithCustomError(reg, "EpochAlreadyExists");
});
it("isSanctionedLatest returns false when no attested snapshot exists", async function () {
const [sanctioned, epoch] = await reg.isSanctionedLatest(alice.address, LIST_OFAC_SDN, []);
expect(sanctioned).to.equal(false);
expect(epoch).to.equal(0n);
});
});
describe("ChainalysisOracleWrapper", function () {
let wrapper, mockUpstream, deployer, alice, bob;
// Tiny in-memory mock for Chainalysis upstream.
before(async function () {
// Compile a minimal inline mock by reusing existing tooling — we'll deploy
// a tiny stub via a temp Solidity file. For simplicity in this test we
// deploy a stub that returns true for a specific address (sanctioned)
// and false otherwise. The MockUSDC already in the project has no
// isSanctioned method; create one inline using a "JSON" contract def
// approach is not directly possible, so we deploy a contract from raw
// bytecode. Simpler: use the wrapper's own contract address as the
// upstream (the wrapper has its own isSanctioned), so calls forward to
// ourselves and we can predict behaviour. But that would cause infinite
// recursion. Final approach: deploy a tiny SanctionStub via an inline
// Solidity file at test time.
});
beforeEach(async function () {
[deployer, alice, bob] = await ethers.getSigners();
wrapper = await (await ethers.getContractFactory("ChainalysisOracleWrapper")).deploy();
await wrapper.waitForDeployment();
});
it("returns false before upstream is set (failing closed)", async function () {
const result = await wrapper.isSanctioned(alice.address);
expect(result).to.equal(false);
});
it("setUpstream + lockUpstream lifecycle", async function () {
// Use the wrapper itself as a placeholder upstream — it doesn't have a
// matching isSanctioned signature so the call will revert and the
// wrapper catches → returns false. This proves the fail-closed path.
await wrapper.setUpstream(await wrapper.getAddress());
expect(await wrapper.upstreamOracle()).to.equal(await wrapper.getAddress());
await wrapper.lockUpstream();
expect(await wrapper.locked()).to.equal(true);
// Second setUpstream attempt should revert.
await expect(wrapper.setUpstream(alice.address)).to.be.revertedWithCustomError(wrapper, "AlreadyLocked");
await expect(wrapper.lockUpstream()).to.be.revertedWithCustomError(wrapper, "AlreadyLocked");
// isSanctioned forwards to the (self-)upstream which has isSanctioned;
// the wrapper's own implementation queries `upstreamOracle`, which is
// still the wrapper itself — recursion. To avoid that, the wrapper's
// upstream call is `IChainalysisUpstream(up).isSanctioned(account)`.
// The wrapper IS a contract that implements isSanctioned(address) and
// would recurse infinitely. We pre-empt by checking the upstream is
// the wrapper, which would call itself; in Solidity the staticcall
// would consume gas → out-of-gas → caught by try/catch → false.
const r = await wrapper.isSanctioned(bob.address);
expect(r).to.equal(false); // failed closed
});
it("rejects zero address upstream", async function () {
await expect(wrapper.setUpstream(ethers.ZeroAddress)).to.be.revertedWithCustomError(wrapper, "ZeroAddress");
});
});
describe("AereTravelRuleHashRegistry", function () {
let reg, vasp1, vasp2, third;
beforeEach(async function () {
[vasp1, vasp2, third] = await ethers.getSigners();
reg = await (await ethers.getContractFactory("AereTravelRuleHashRegistry")).deploy();
await reg.waitForDeployment();
});
it("commit + acknowledge happy path", async function () {
const hash = ethers.id("ivms-101 payload v1");
const threshold = 100_000; // 1,000 USD in cents
const tx = await reg.connect(vasp1).commit(hash, vasp2.address, threshold);
const r = await tx.wait();
const id = r.logs.find(l => l.fragment && l.fragment.name === "Committed").args.id;
const c0 = await reg.commitments(id);
expect(c0.payloadHash).to.equal(hash);
expect(c0.originatorVasp).to.equal(vasp1.address);
expect(c0.beneficiaryVasp).to.equal(vasp2.address);
expect(c0.state).to.equal(0n); // Committed
await reg.connect(vasp2).acknowledge(id);
const c1 = await reg.commitments(id);
expect(c1.state).to.equal(1n); // Acknowledged
});
it("commit + dispute path", async function () {
const hash = ethers.id("bad payload");
const tx = await reg.connect(vasp1).commit(hash, vasp2.address, 10_000);
const id = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "Committed").args.id;
await reg.connect(vasp2).dispute(id, "ivms-101 payload doesn't match the committed hash");
const c = await reg.commitments(id);
expect(c.state).to.equal(2n); // Disputed
expect(c.disputeReason).to.equal("ivms-101 payload doesn't match the committed hash");
});
it("non-beneficiary cannot acknowledge", async function () {
const tx = await reg.connect(vasp1).commit(ethers.id("h"), vasp2.address, 1000);
const id = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "Committed").args.id;
await expect(reg.connect(third).acknowledge(id)).to.be.revertedWithCustomError(reg, "NotBeneficiary");
});
it("zero beneficiary reverts", async function () {
await expect(reg.connect(vasp1).commit(ethers.id("h"), ethers.ZeroAddress, 1000)).to.be.revertedWithCustomError(reg, "ZeroAddress");
});
});
describe("AereForensicEventRegistry", function () {
let reg, deployer, botOwner, attacker, victim;
beforeEach(async function () {
[deployer, botOwner, attacker, victim] = await ethers.getSigners();
reg = await (await ethers.getContractFactory("AereForensicEventRegistry")).deploy();
await reg.waitForDeployment();
});
it("register bot, emit alert, Foundation confirms", async function () {
const tx = await reg.connect(botOwner).registerBot("AnomalyHunter v1", "ipfs://QmBotMeta");
const botId = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "BotRegistered").args.botId;
const alertTx = await reg.connect(botOwner).emitAlert(botId, attacker.address, 9, 5, "ipfs://QmAlert1");
const alertId = (await alertTx.wait()).logs.find(l => l.fragment && l.fragment.name === "AlertEmitted").args.alertId;
const bot0 = await reg.bots(botId);
expect(bot0.alertCount).to.equal(1n);
expect(bot0.confirmedCount).to.equal(0n);
await reg.connect(deployer).confirmAlert(alertId);
const bot1 = await reg.bots(botId);
expect(bot1.confirmedCount).to.equal(1n);
// recentConfirmedAlertCount picks up the severity-5 alert within a 24h window.
expect(await reg.recentConfirmedAlertCount(attacker.address, 4, 24 * 3600)).to.equal(1n);
// With a higher severity floor than the alert, no match.
expect(await reg.recentConfirmedAlertCount(attacker.address, 6, 24 * 3600)).to.equal(0n);
// Different subject, no match.
expect(await reg.recentConfirmedAlertCount(victim.address, 1, 24 * 3600)).to.equal(0n);
});
it("non-bot-owner cannot emit alerts on a bot they don't own", async function () {
const tx = await reg.connect(botOwner).registerBot("Bot", "uri");
const botId = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "BotRegistered").args.botId;
await expect(
reg.connect(attacker).emitAlert(botId, victim.address, 0, 1, "")
).to.be.revertedWithCustomError(reg, "NotBotOwner");
});
it("inactive bot cannot emit alerts", async function () {
const tx = await reg.connect(botOwner).registerBot("Bot2", "uri");
const botId = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "BotRegistered").args.botId;
await reg.connect(botOwner).updateBot(botId, "uri2", false);
await expect(
reg.connect(botOwner).emitAlert(botId, attacker.address, 0, 1, "")
).to.be.revertedWithCustomError(reg, "BotInactive");
});
it("recentConfirmedAlertCount respects time window", async function () {
const tx = await reg.connect(botOwner).registerBot("B", "u");
const botId = (await tx.wait()).logs.find(l => l.fragment && l.fragment.name === "BotRegistered").args.botId;
const alertTx = await reg.connect(botOwner).emitAlert(botId, attacker.address, 9, 5, "");
const alertId = (await alertTx.wait()).logs.find(l => l.fragment && l.fragment.name === "AlertEmitted").args.alertId;
await reg.connect(deployer).confirmAlert(alertId);
// Advance 48h.
await ethers.provider.send("evm_increaseTime", [48 * 3600]);
await ethers.provider.send("evm_mine", []);
// 24h window no longer includes this alert.
expect(await reg.recentConfirmedAlertCount(attacker.address, 1, 24 * 3600)).to.equal(0n);
// 72h window still includes it.
expect(await reg.recentConfirmedAlertCount(attacker.address, 1, 72 * 3600)).to.equal(1n);
});
});