// Hardhat tests for AereNavOracle. // // Covers: // - propose snapshot + 24h challenge window // - attest after window with no challenges (permissionless) // - challenge + dismiss path; attest after dismissals // - verifyReserve against attested root with Merkle proof // - latestAttestedEpoch view // - monotonic epoch enforcement (cannot overwrite an existing epoch) // // Run: npx hardhat test test/nav-oracle.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); function leafOf(asset, chainId, reserveAmount, decimals) { return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode( ["address", "uint256", "uint256", "uint8"], [asset, chainId, reserveAmount, decimals] )); } 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("AereNavOracle", function () { let oracle, deployer, alice, bob; beforeEach(async function () { [deployer, alice, bob] = await ethers.getSigners(); oracle = await (await ethers.getContractFactory("AereNavOracle")).deploy(); await oracle.waitForDeployment(); }); it("proposes a snapshot and attests after 24h with no challenge", async function () { const usdce = "0x" + "11".repeat(20); const usdte = "0x" + "22".repeat(20); const leaves = [ leafOf(usdce, 2800, 1_000_000n * 10n ** 6n, 6), leafOf(usdte, 2800, 5_000_000n * 10n ** 6n, 6), ]; const tree = buildSimpleTree(leaves); await oracle.proposeSnapshot(1, tree.root, "ipfs://QmExampleCid"); // Cannot attest early. await expect(oracle.attest(1)).to.be.revertedWithCustomError(oracle, "TimelockNotElapsed"); // Advance 24h. await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await oracle.attest(1); // Verify a reserve via Merkle proof. const leaf = leaves[0]; const ok = await oracle.verifyReserve(1, usdce, 2800, 1_000_000n * 10n ** 6n, 6, proofFor(tree, leaf)); expect(ok).to.equal(true); // Wrong amount → false. const bad = await oracle.verifyReserve(1, usdce, 2800, 999_999n * 10n ** 6n, 6, proofFor(tree, leaf)); expect(bad).to.equal(false); }); it("monotonic epoch — cannot overwrite an existing one", async function () { await oracle.proposeSnapshot(1, ethers.ZeroHash, ""); await expect(oracle.proposeSnapshot(1, ethers.id("other"), "")).to.be.revertedWithCustomError(oracle, "EpochAlreadyExists"); }); it("challenge → dismiss → attest path; window does not reset", async function () { await oracle.proposeSnapshot(2, ethers.id("root2"), "ipfs://r2"); await oracle.connect(bob).challenge(2, "double counting USDC.e"); // Cannot attest while challenged even after window elapsed. await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(oracle.attest(2)).to.be.revertedWithCustomError(oracle, "WrongState"); // Owner dismisses. await oracle.dismissChallenge(2, 0, "double-count claim is false; see audit log"); // Now attestable. await oracle.attest(2); const status = await oracle.snapshotStatus(2); expect(status.state).to.equal(2); // Attested }); it("challenge after window elapses reverts", async function () { await oracle.proposeSnapshot(3, ethers.id("root3"), ""); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(oracle.connect(bob).challenge(3, "too late")).to.be.revertedWithCustomError(oracle, "TimelockNotElapsed"); }); it("latestAttestedEpoch returns the highest attested epoch", async function () { await oracle.proposeSnapshot(5, ethers.id("a"), ""); await oracle.proposeSnapshot(7, ethers.id("b"), ""); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await oracle.attest(5); await oracle.attest(7); const [epoch, root] = await oracle.latestAttestedEpoch(); expect(epoch).to.equal(7n); expect(root).to.equal(ethers.id("b")); }); it("verifyReserve returns false for non-attested snapshot", async function () { const usdce = "0x" + "11".repeat(20); const leaves = [leafOf(usdce, 2800, 1n, 6)]; const tree = buildSimpleTree(leaves); await oracle.proposeSnapshot(9, tree.root, ""); // Not yet attested. const ok = await oracle.verifyReserve(9, usdce, 2800, 1n, 6, proofFor(tree, leaves[0])); expect(ok).to.equal(false); }); });