contracts/test/LastLightController.test.js
Edut LLC 32141a89f4
Some checks are pending
check / contracts (push) Waiting to run
contracts: add last light controller and gas-only identity mint
2026-02-20 15:43:16 -08:00

230 lines
10 KiB
JavaScript

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
describe("LastLightController", function () {
async function futureDeadline(seconds) {
const block = await ethers.provider.getBlock("latest");
return block.timestamp + seconds;
}
async function expectRevertWithCustomError(promise, errorName) {
try {
await promise;
expect.fail(`expected revert ${errorName}`);
} catch (err) {
expect(String(err)).to.contain(errorName);
}
}
async function signArm(contract, signer, releaseId, reasonHash, deadline, armEpoch) {
const domain = {
name: "EDUT Last Light",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: contract.address
};
const types = {
ArmIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "reasonHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, reasonHash, deadline, armEpoch };
return signer._signTypedData(domain, types, value);
}
async function signCancel(contract, signer, releaseId, reasonHash, deadline, armEpoch) {
const domain = {
name: "EDUT Last Light",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: contract.address
};
const types = {
CancelIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "reasonHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, reasonHash, deadline, armEpoch };
return signer._signTypedData(domain, types, value);
}
async function signExecute(contract, signer, releaseId, decryptionKey, manifestRefHash, deadline, armEpoch) {
const domain = {
name: "EDUT Last Light",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: contract.address
};
const types = {
ExecuteIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "decryptionKey", type: "bytes32" },
{ name: "manifestRefHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, decryptionKey, manifestRefHash, deadline, armEpoch };
return signer._signTypedData(domain, types, value);
}
async function deployFixture() {
const [owner, g1, g2, g3, g4, g5, relayer, other] = await ethers.getSigners();
const factory = await ethers.getContractFactory("LastLightController", owner);
const contract = await factory.deploy([g1.address, g2.address, g3.address, g4.address, g5.address], 3);
await contract.deployed();
return { contract, owner, g1, g2, g3, g4, g5, relayer, other };
}
async function stageDefault(contract, owner) {
const releaseId = ethers.utils.id("last-light-release-001");
const payloadManifestHash = ethers.utils.id("payload-manifest");
const encryptedPayloadRootHash = ethers.utils.id("encrypted-root");
const decryptionKey = ethers.utils.id("decryption-key");
const decryptionKeyHash = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["bytes32"], [decryptionKey]));
const metadataRefHash = ethers.utils.id("metadata-ref");
const timelock = 3600;
await (await contract.connect(owner).stageRelease(
releaseId,
payloadManifestHash,
encryptedPayloadRootHash,
decryptionKeyHash,
metadataRefHash,
timelock
)).wait();
const record = await contract.releaseRecord(releaseId);
return { releaseId, decryptionKey, armEpoch: Number(record.armEpoch), timelock };
}
it("requires quorum to arm and execute", async function () {
const { contract, owner, g1, g2, g3, relayer } = await deployFixture();
const { releaseId, decryptionKey, armEpoch, timelock } = await stageDefault(contract, owner);
const reasonHash = ethers.utils.id("owner-incapacity");
const manifestRefHash = ethers.utils.id("manifest-ref-hash");
const deadline = await futureDeadline(24 * 3600);
const armSig1 = await signArm(contract, g1, releaseId, reasonHash, deadline, armEpoch);
await expectRevertWithCustomError(
contract.connect(relayer).armReleaseWithSignatures(releaseId, reasonHash, deadline, [armSig1]),
"InvalidGuardianQuorum"
);
const armSig2 = await signArm(contract, g2, releaseId, reasonHash, deadline, armEpoch);
const armSig3 = await signArm(contract, g3, releaseId, reasonHash, deadline, armEpoch);
await (await contract.connect(relayer).armReleaseWithSignatures(releaseId, reasonHash, deadline, [armSig1, armSig2, armSig3])).wait();
const executeSig1 = await signExecute(contract, g1, releaseId, decryptionKey, manifestRefHash, deadline, armEpoch);
const executeSig2 = await signExecute(contract, g2, releaseId, decryptionKey, manifestRefHash, deadline, armEpoch);
const executeSig3 = await signExecute(contract, g3, releaseId, decryptionKey, manifestRefHash, deadline, armEpoch);
await expectRevertWithCustomError(
contract.connect(relayer).executeReleaseWithSignatures(
releaseId,
decryptionKey,
manifestRefHash,
deadline,
[executeSig1, executeSig2, executeSig3]
),
"ExecutionTimelockActive"
);
await network.provider.send("evm_increaseTime", [timelock + 1]);
await network.provider.send("evm_mine");
await (await contract.connect(relayer).executeReleaseWithSignatures(
releaseId,
decryptionKey,
manifestRefHash,
deadline,
[executeSig1, executeSig2, executeSig3]
)).wait();
const record = await contract.releaseRecord(releaseId);
expect(record.status).to.equal(3); // Executed
});
it("requires quorum to cancel; single guardian cannot cancel", async function () {
const { contract, owner, g1, g2, g3, relayer } = await deployFixture();
const { releaseId, armEpoch } = await stageDefault(contract, owner);
const reasonHash = ethers.utils.id("owner-incapacity");
const cancelReasonHash = ethers.utils.id("false-alarm");
const deadline = await futureDeadline(24 * 3600);
const armSig1 = await signArm(contract, g1, releaseId, reasonHash, deadline, armEpoch);
const armSig2 = await signArm(contract, g2, releaseId, reasonHash, deadline, armEpoch);
const armSig3 = await signArm(contract, g3, releaseId, reasonHash, deadline, armEpoch);
await (await contract.connect(relayer).armReleaseWithSignatures(releaseId, reasonHash, deadline, [armSig1, armSig2, armSig3])).wait();
const cancelSig1 = await signCancel(contract, g1, releaseId, cancelReasonHash, deadline, armEpoch);
await expectRevertWithCustomError(
contract.connect(relayer).cancelReleaseWithSignatures(releaseId, cancelReasonHash, deadline, [cancelSig1]),
"InvalidGuardianQuorum"
);
const cancelSig2 = await signCancel(contract, g2, releaseId, cancelReasonHash, deadline, armEpoch);
const cancelSig3 = await signCancel(contract, g3, releaseId, cancelReasonHash, deadline, armEpoch);
await (await contract.connect(relayer).cancelReleaseWithSignatures(
releaseId,
cancelReasonHash,
deadline,
[cancelSig1, cancelSig2, cancelSig3]
)).wait();
const record = await contract.releaseRecord(releaseId);
expect(record.status).to.equal(4); // Canceled
});
it("rejects execute when decryption key hash does not match staged commitment", async function () {
const { contract, owner, g1, g2, g3, relayer } = await deployFixture();
const { releaseId, armEpoch, timelock } = await stageDefault(contract, owner);
const reasonHash = ethers.utils.id("owner-incapacity");
const deadline = await futureDeadline(24 * 3600);
const armSig1 = await signArm(contract, g1, releaseId, reasonHash, deadline, armEpoch);
const armSig2 = await signArm(contract, g2, releaseId, reasonHash, deadline, armEpoch);
const armSig3 = await signArm(contract, g3, releaseId, reasonHash, deadline, armEpoch);
await (await contract.connect(relayer).armReleaseWithSignatures(releaseId, reasonHash, deadline, [armSig1, armSig2, armSig3])).wait();
await network.provider.send("evm_increaseTime", [timelock + 1]);
await network.provider.send("evm_mine");
const wrongKey = ethers.utils.id("wrong-key");
const manifestRefHash = ethers.utils.id("manifest-ref");
const executeSig1 = await signExecute(contract, g1, releaseId, wrongKey, manifestRefHash, deadline, armEpoch);
const executeSig2 = await signExecute(contract, g2, releaseId, wrongKey, manifestRefHash, deadline, armEpoch);
const executeSig3 = await signExecute(contract, g3, releaseId, wrongKey, manifestRefHash, deadline, armEpoch);
await expectRevertWithCustomError(
contract.connect(relayer).executeReleaseWithSignatures(
releaseId,
wrongKey,
manifestRefHash,
deadline,
[executeSig1, executeSig2, executeSig3]
),
"InvalidDecryptionKey"
);
});
it("invalidates staged release if guardian set changes", async function () {
const { contract, owner, g1, g2, g3, g4, g5, other, relayer } = await deployFixture();
const { releaseId, armEpoch } = await stageDefault(contract, owner);
await (await contract.connect(owner).setGuardians([g1.address, g2.address, g3.address, g4.address, other.address], 3)).wait();
const reasonHash = ethers.utils.id("owner-incapacity");
const deadline = await futureDeadline(24 * 3600);
const armSig1 = await signArm(contract, g1, releaseId, reasonHash, deadline, armEpoch);
const armSig2 = await signArm(contract, g2, releaseId, reasonHash, deadline, armEpoch);
const armSig3 = await signArm(contract, g3, releaseId, reasonHash, deadline, armEpoch);
await expectRevertWithCustomError(
contract.connect(relayer).armReleaseWithSignatures(releaseId, reasonHash, deadline, [armSig1, armSig2, armSig3]),
"GuardianSetChanged"
);
// ensure old guardian is no longer active in current set
expect(await contract.isGuardian(g5.address)).to.equal(false);
});
});