const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("AereDeathSwitch", () => { let foundation, alice, bob; let bridge, dex, ds; const BUCKET_BRIDGE = ethers.keccak256(ethers.toUtf8Bytes("bridge")); const BUCKET_DEX = ethers.keccak256(ethers.toUtf8Bytes("dex")); beforeEach(async () => { [foundation, alice, bob] = await ethers.getSigners(); const M = await ethers.getContractFactory("MockDeathSwitchCondition"); bridge = await M.deploy(BUCKET_BRIDGE, "bridge collateral ratio breached"); await bridge.waitForDeployment(); dex = await M.deploy(BUCKET_DEX, "dex pool drained > 50%"); await dex.waitForDeployment(); const D = await ethers.getContractFactory("AereDeathSwitch"); ds = await D.deploy(foundation.address, [await bridge.getAddress(), await dex.getAddress()]); await ds.waitForDeployment(); }); it("constructor registers conditions + buckets", async () => { expect(await ds.conditionsLength()).to.equal(2n); const [addr0, b0] = await ds.conditionAt(0); expect(addr0).to.equal(await bridge.getAddress()); expect(b0).to.equal(BUCKET_BRIDGE); expect(await ds.isTripped()).to.equal(false); }); it("only Foundation can declare emergency; declares + lock", async () => { await expect( ds.connect(alice).armForDeclaredEmergency("rogue Foundation key?") ).to.be.revertedWithCustomError(ds, "NotFoundation"); await ds.connect(foundation).armForDeclaredEmergency("test incident"); expect(await ds.isTripped()).to.equal(true); expect(await ds.lastTripLabel()).to.equal("test incident"); expect(await ds.lastTripReason()).to.equal(ethers.keccak256(ethers.toUtf8Bytes("declared"))); }); it("permissionless trip succeeds when condition breached", async () => { await bridge.setBreached(true); await expect(ds.connect(alice).permissionlessTrip(0)) .to.emit(ds, "Tripped") .withArgs(BUCKET_BRIDGE, alice.address, 0n, "bridge collateral ratio breached"); expect(await ds.isTripped()).to.equal(true); expect(await ds.isBucketTripped(BUCKET_BRIDGE)).to.equal(true); }); it("permissionless trip rejects when condition not breached / unknown id", async () => { await expect(ds.connect(alice).permissionlessTrip(0)) .to.be.revertedWithCustomError(ds, "ConditionNotBreached"); await expect(ds.connect(alice).permissionlessTrip(99)) .to.be.revertedWithCustomError(ds, "UnknownCondition"); }); it("cannot trip twice; rearm flow gated by 7-day timelock", async () => { await ds.connect(foundation).armForDeclaredEmergency("incident"); await expect(ds.connect(foundation).armForDeclaredEmergency("twice")) .to.be.revertedWithCustomError(ds, "AlreadyTripped"); // Only Foundation can request rearm. await expect(ds.connect(alice).requestRearm(ethers.ZeroHash)) .to.be.revertedWithCustomError(ds, "NotFoundation"); const reason = ethers.keccak256(ethers.toUtf8Bytes("rotated keys")); await ds.connect(foundation).requestRearm(reason); // Double request prevented. await expect(ds.connect(foundation).requestRearm(reason)) .to.be.revertedWithCustomError(ds, "RearmAlreadyRequested"); // Cannot finalize before timelock. await expect(ds.connect(foundation).finalizeRearm()) .to.be.revertedWithCustomError(ds, "RearmTimelockOpen"); // Fast-forward 7d. await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(ds.connect(foundation).finalizeRearm()).to.emit(ds, "Rearmed"); expect(await ds.isTripped()).to.equal(false); expect(await ds.rearmReadyAt()).to.equal(0n); }); it("finalizeRearm reverts when no request pending", async () => { await ds.connect(foundation).armForDeclaredEmergency("incident"); await expect(ds.connect(foundation).finalizeRearm()) .to.be.revertedWithCustomError(ds, "NoRearmRequest"); }); it("cancelRearmRequest drops a pending request + emits (audit fix MED #37)", async () => { await ds.connect(foundation).armForDeclaredEmergency("incident"); const reason = ethers.keccak256(ethers.toUtf8Bytes("reason")); await ds.connect(foundation).requestRearm(reason); await expect(ds.connect(foundation).cancelRearmRequest()).to.emit(ds, "RearmCancelled").withArgs(reason); expect(await ds.rearmReadyAt()).to.equal(0n); await ds.connect(foundation).requestRearm(ethers.ZeroHash); expect(await ds.rearmReadyAt()).to.be.gt(0n); }); it("permissionlessUnarmDeclared after 24h if no condition breached (audit fix HIGH #19)", async () => { await ds.connect(foundation).armForDeclaredEmergency("Foundation pager test"); // Too early. await expect(ds.connect(alice).permissionlessUnarmDeclared()).to.be.revertedWithCustomError(ds, "RearmTimelockOpen"); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); // If a condition is breached now, unarm still rejected. await bridge.setBreached(true); await expect(ds.connect(alice).permissionlessUnarmDeclared()).to.be.revertedWithCustomError(ds, "NoConditionsBreached"); await bridge.setBreached(false); // Now succeeds — any address can call. await ds.connect(alice).permissionlessUnarmDeclared(); expect(await ds.isTripped()).to.equal(false); }); it("permissionlessUnarmDeclared not applicable for condition-tripped emergencies", async () => { await bridge.setBreached(true); await ds.connect(alice).permissionlessTrip(0); await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await expect(ds.connect(alice).permissionlessUnarmDeclared()).to.be.revertedWithCustomError(ds, "ConditionNotBreached"); }); it("bucketTripped flags clear after rearm finalize", async () => { await bridge.setBreached(true); await ds.connect(alice).permissionlessTrip(0); expect(await ds.bucketTripped(BUCKET_BRIDGE)).to.equal(true); await bridge.setBreached(false); await ds.connect(foundation).requestRearm(ethers.ZeroHash); await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]); await ethers.provider.send("evm_mine", []); await ds.connect(foundation).finalizeRearm(); expect(await ds.bucketTripped(BUCKET_BRIDGE)).to.equal(false); }); it("isBucketTripped is true while master tripped, even for unregistered buckets", async () => { const FOO = ethers.keccak256(ethers.toUtf8Bytes("foo")); await ds.connect(foundation).armForDeclaredEmergency("incident"); expect(await ds.isBucketTripped(FOO)).to.equal(true); }); it("constructor with empty conditions list works (manual-only mode)", async () => { const D = await ethers.getContractFactory("AereDeathSwitch"); const manual = await D.deploy(foundation.address, []); expect(await manual.conditionsLength()).to.equal(0n); await manual.connect(foundation).armForDeclaredEmergency("manual-only"); expect(await manual.isTripped()).to.equal(true); }); });