230 lines
10 KiB
JavaScript
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);
|
|
});
|
|
});
|