From cf98121b9e5cfe4ac9cc67e6b082f22476ab4a55 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 9 Jan 2025 12:33:58 +0400 Subject: [PATCH 01/65] fix: vebo access control and deploy tests --- ...ator-exit-bus-oracle.accessControl.test.ts | 205 ++++++++++++++++++ .../validator-exit-bus-oracle.deploy.test.ts | 81 +++++++ test/deploy/index.ts | 1 + test/deploy/validatorExitBusOracle.ts | 120 ++++++++++ 4 files changed, 407 insertions(+) create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts create mode 100644 test/deploy/validatorExitBusOracle.ts diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts new file mode 100644 index 000000000..e7463a013 --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -0,0 +1,205 @@ +import { expect } from "chai"; +import { ContractTransactionResponse, ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness, ValidatorsExitBusOracle } from "typechain-types"; + +import { CONSENSUS_VERSION, de0x, numberToHex, SECONDS_PER_SLOT } from "lib"; + +import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; +import { Snapshot } from "test/suite"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +]; + +describe("ValidatorsExitBusOracle.sol:accessControl", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + let originalState: string; + + let initTx: ContractTransactionResponse; + let oracleVersion: bigint; + let exitRequests: ExitRequest[]; + let reportFields: ReportFields; + let reportItems: ReturnType; + let reportHash: string; + + let member1: HardhatEthersSigner; + let member2: HardhatEthersSigner; + let member3: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + let account1: HardhatEthersSigner; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + requestsCount: number; + dataFormat: number; + data: string; + } + + const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + return ethers.keccak256(data); + }; + + const getValidatorsExitBusReportDataItems = (r: ReportFields) => { + return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; + }; + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + + initTx = await initVEBO({ admin: admin.address, oracle, consensus, resumeAfterDeploy: true }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); + + const { refSlot } = await consensus.getCurrentFrame(); + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + ]; + + reportFields = { + consensusVersion: CONSENSUS_VERSION, + dataFormat: DATA_FORMAT_LIST, + refSlot: refSlot, + requestsCount: exitRequests.length, + data: encodeExitRequestsDataList(exitRequests), + }; + + reportItems = getValidatorsExitBusReportDataItems(reportFields); + reportHash = calcValidatorsExitBusReportDataHash(reportItems); + + await consensus.connect(member1).submitReport(refSlot, reportHash, CONSENSUS_VERSION); + await consensus.connect(member3).submitReport(refSlot, reportHash, CONSENSUS_VERSION); + }; + + before(async () => { + [admin, member1, member2, member3, stranger, account1] = await ethers.getSigners(); + + await deploy(); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("deploying", () => { + it("deploying accounting oracle", async () => { + expect(oracle).to.be.not.null; + expect(consensus).to.be.not.null; + expect(initTx).to.be.not.null; + expect(oracleVersion).to.be.not.null; + expect(exitRequests).to.be.not.null; + expect(reportFields).to.be.not.null; + expect(reportItems).to.be.not.null; + expect(reportHash).to.be.not.null; + }); + }); + + context("DEFAULT_ADMIN_ROLE", () => { + context("Admin is set at initialize", () => { + it("should set admin at initialize", async () => { + const DEFAULT_ADMIN_ROLE = await oracle.DEFAULT_ADMIN_ROLE(); + await expect(initTx).to.emit(oracle, "RoleGranted").withArgs(DEFAULT_ADMIN_ROLE, admin, admin); + }); + it("should revert without admin address", async () => { + await expect( + oracle.initialize(ZeroAddress, await consensus.getAddress(), CONSENSUS_VERSION, 0), + ).to.be.revertedWithCustomError(oracle, "AdminCannotBeZero"); + }); + }); + }); + + context("PAUSE_ROLE", () => { + it("should revert without PAUSE_ROLE role", async () => { + await expect(oracle.connect(stranger).pauseFor(0)).to.be.revertedWithOZAccessControlError( + await stranger.getAddress(), + await oracle.PAUSE_ROLE(), + ); + }); + + it("should allow calling from a possessor of PAUSE_ROLE role", async () => { + await oracle.grantRole(await oracle.PAUSE_ROLE(), account1); + + const tx = await oracle.connect(account1).pauseFor(9999); + await expect(tx).to.emit(oracle, "Paused").withArgs(9999); + }); + }); + + context("RESUME_ROLE", () => { + it("should revert without RESUME_ROLE role", async () => { + await oracle.connect(admin).pauseFor(9999); + + await expect(oracle.connect(stranger).resume()).to.be.revertedWithOZAccessControlError( + await stranger.getAddress(), + await oracle.RESUME_ROLE(), + ); + }); + + it("should allow calling from a possessor of RESUME_ROLE role", async () => { + await oracle.pauseFor(9999, { from: admin }); + await oracle.grantRole(await oracle.RESUME_ROLE(), account1); + + const tx = await oracle.connect(account1).resume(); + await expect(tx).to.emit(oracle, "Resumed").withArgs(); + }); + }); + + context("SUBMIT_DATA_ROLE", () => { + context("_checkMsgSenderIsAllowedToSubmitData", () => { + it("should revert from not consensus member without SUBMIT_DATA_ROLE role", async () => { + await expect( + oracle.connect(stranger).submitReportData(reportFields, oracleVersion), + ).to.be.revertedWithCustomError(oracle, "SenderNotAllowed"); + }); + + it("should allow calling from a possessor of SUBMIT_DATA_ROLE role", async () => { + await oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), account1); + const deadline = (await oracle.getConsensusReport()).processingDeadlineTime; + await consensus.setTime(deadline); + + const tx = await oracle.connect(account1).submitReportData(reportFields, oracleVersion); + + await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); + }); + + it("should allow calling from a member", async () => { + const tx = await oracle.connect(member2).submitReportData(reportFields, oracleVersion); + + await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); + }); + }); + }); +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts new file mode 100644 index 000000000..48dee32af --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness, ValidatorsExitBusOracle } from "typechain-types"; + +import { CONSENSUS_VERSION, SECONDS_PER_SLOT } from "lib"; + +import { deployVEBO, initVEBO } from "test/deploy"; + +describe("ValidatorsExitBusOracle.sol:deploy", () => { + context("Deployment and initial configuration", () => { + let admin: HardhatEthersSigner; + let defaultOracle: ValidatorsExitBusOracle; + + before(async () => { + [admin] = await ethers.getSigners(); + defaultOracle = (await deployVEBO(admin.address)).oracle; + }); + + it("initialize reverts if admin address is zero", async () => { + const deployed = await deployVEBO(admin.address); + + await expect( + deployed.oracle.initialize(ZeroAddress, await deployed.consensus.getAddress(), CONSENSUS_VERSION, 0), + ).to.be.revertedWithCustomError(defaultOracle, "AdminCannotBeZero"); + }); + + it("reverts when slotsPerSecond is zero", async () => { + await expect(deployVEBO(admin.address, { secondsPerSlot: 0n })).to.be.revertedWithCustomError( + defaultOracle, + "SecondsPerSlotCannotBeZero", + ); + }); + + context("deployment and init finishes successfully (default setup)", async () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + + before(async () => { + const deployed = await deployVEBO(admin.address); + await initVEBO({ + admin: admin.address, + oracle: deployed.oracle, + consensus: deployed.consensus, + }); + + consensus = deployed.consensus; + oracle = deployed.oracle; + }); + + it("mock time-travellable setup is correct", async () => { + const time1 = await consensus.getTime(); + expect(await oracle.getTime()).to.equal(time1); + + await consensus.advanceTimeBy(SECONDS_PER_SLOT); + + const time2 = await consensus.getTime(); + expect(time2).to.equal(time1 + SECONDS_PER_SLOT); + expect(await oracle.getTime()).to.equal(time2); + }); + + it("initial configuration is correct", async () => { + expect(await oracle.getConsensusContract()).to.equal(await consensus.getAddress()); + expect(await oracle.getConsensusVersion()).to.equal(CONSENSUS_VERSION); + expect(await oracle.SECONDS_PER_SLOT()).to.equal(SECONDS_PER_SLOT); + expect(await oracle.isPaused()).to.equal(true); + }); + + it("pause/resume operations work", async () => { + expect(await oracle.isPaused()).to.equal(true); + await oracle.resume(); + expect(await oracle.isPaused()).to.equal(false); + await oracle.pauseFor(123); + expect(await oracle.isPaused()).to.equal(true); + }); + }); + }); +}); diff --git a/test/deploy/index.ts b/test/deploy/index.ts index d7afaf858..281dd47ab 100644 --- a/test/deploy/index.ts +++ b/test/deploy/index.ts @@ -4,3 +4,4 @@ export * from "./locator"; export * from "./dao"; export * from "./hashConsensus"; export * from "./withdrawalQueue"; +export * from "./validatorExitBusOracle"; diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts new file mode 100644 index 000000000..1b5e0e280 --- /dev/null +++ b/test/deploy/validatorExitBusOracle.ts @@ -0,0 +1,120 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HashConsensus__Harness, ReportProcessor__Mock, ValidatorsExitBusOracle } from "typechain-types"; + +import { + CONSENSUS_VERSION, + EPOCHS_PER_FRAME, + GENESIS_TIME, + INITIAL_EPOCH, + SECONDS_PER_SLOT, + SLOTS_PER_EPOCH, +} from "lib"; + +import { deployHashConsensus } from "./hashConsensus"; +import { deployLidoLocator, updateLidoLocatorImplementation } from "./locator"; + +export const DATA_FORMAT_LIST = 1; + +async function deployMockAccountingOracle(secondsPerSlot = SECONDS_PER_SLOT, genesisTime = GENESIS_TIME) { + const lido = await ethers.deployContract("Lido__MockForAccountingOracle"); + const ao = await ethers.deployContract("AccountingOracle__MockForSanityChecker", [ + await lido.getAddress(), + secondsPerSlot, + genesisTime, + ]); + return { ao, lido }; +} + +async function deployOracleReportSanityCheckerForExitBus(lidoLocator: string, admin: string) { + const maxValidatorExitRequestsPerReport = 2000; + const limitsList = [0, 0, 0, 0, maxValidatorExitRequestsPerReport, 0, 0, 0, 0, 0, 0, 0]; + + return await ethers.deployContract("OracleReportSanityChecker", [lidoLocator, admin, limitsList]); +} + +export async function deployVEBO( + admin: string, + { + epochsPerFrame = EPOCHS_PER_FRAME, + secondsPerSlot = SECONDS_PER_SLOT, + slotsPerEpoch = SLOTS_PER_EPOCH, + genesisTime = GENESIS_TIME, + initialEpoch = INITIAL_EPOCH, + } = {}, +) { + const locator = await deployLidoLocator(); + const locatorAddr = await locator.getAddress(); + + const oracle = await ethers.deployContract("ValidatorsExitBus__Harness", [secondsPerSlot, genesisTime, locatorAddr]); + + const { consensus } = await deployHashConsensus(admin, { + reportProcessor: oracle as unknown as ReportProcessor__Mock, + epochsPerFrame, + secondsPerSlot, + genesisTime, + }); + + const { ao, lido } = await deployMockAccountingOracle(secondsPerSlot, genesisTime); + + await updateLidoLocatorImplementation(locatorAddr, { + lido: await lido.getAddress(), + accountingOracle: await ao.getAddress(), + }); + + const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); + + await updateLidoLocatorImplementation(locatorAddr, { + validatorsExitBusOracle: await oracle.getAddress(), + oracleReportSanityChecker: await oracleReportSanityChecker.getAddress(), + }); + + await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot); + + return { + locatorAddr, + oracle, + consensus, + oracleReportSanityChecker, + }; +} + +interface VEBOConfig { + admin: string; + oracle: ValidatorsExitBusOracle; + consensus: HashConsensus__Harness; + dataSubmitter?: string; + consensusVersion?: bigint; + lastProcessingRefSlot?: number; + resumeAfterDeploy?: boolean; +} + +export async function initVEBO({ + admin, + oracle, + consensus, + dataSubmitter = undefined, + consensusVersion = CONSENSUS_VERSION, + lastProcessingRefSlot = 0, + resumeAfterDeploy = false, +}: VEBOConfig) { + const initTx = await oracle.initialize(admin, await consensus.getAddress(), consensusVersion, lastProcessingRefSlot); + + await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); + await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), admin); + await oracle.grantRole(await oracle.PAUSE_ROLE(), admin); + await oracle.grantRole(await oracle.RESUME_ROLE(), admin); + + if (dataSubmitter) { + await oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), dataSubmitter); + } + + expect(await oracle.DATA_FORMAT_LIST()).to.equal(DATA_FORMAT_LIST); + + if (resumeAfterDeploy) { + await oracle.resume(); + } + + return initTx; +} From 517076ef8d725fe8bbd30a5d5b91ef390ac76b1a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 10 Jan 2025 04:01:57 +0400 Subject: [PATCH 02/65] feat: vebo gas, submitReportData, happyPath tests --- ...ator-exit-bus-oracle.accessControl.test.ts | 4 +- .../validator-exit-bus-oracle.gas.test.ts | 243 +++++++++++++++ ...alidator-exit-bus-oracle.happyPath.test.ts | 256 ++++++++++++++++ ...r-exit-bus-oracle.submitReportData.test.ts | 278 ++++++++++++++++++ 4 files changed, 779 insertions(+), 2 deletions(-) create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index e7463a013..53c0e1e29 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -4,9 +4,9 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, ValidatorsExitBusOracle } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; -import { CONSENSUS_VERSION, de0x, numberToHex, SECONDS_PER_SLOT } from "lib"; +import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; import { Snapshot } from "test/suite"; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts new file mode 100644 index 000000000..44a7080ba --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -0,0 +1,243 @@ +import { expect } from "chai"; +import { ContractTransactionReceipt, ZeroHash } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; + +import { trace } from "lib"; +import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; + +import { + computeTimestampAtSlot, + DATA_FORMAT_LIST, + deployVEBO, + initVEBO, + SECONDS_PER_FRAME, + SLOTS_PER_FRAME, +} from "test/deploy"; +import { Snapshot } from "test/suite"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +describe("ValidatorsExitBusOracle.sol:gas", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + + let oracleVersion: bigint; + + let member1: HardhatEthersSigner; + let member2: HardhatEthersSigner; + let member3: HardhatEthersSigner; + + const NUM_MODULES = 5; + const NODE_OPS_PER_MODULE = 100; + + let nextValIndex = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + requestsCount: number; + dataFormat: number; + data: string; + } + + const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + return ethers.keccak256(data); + }; + + const getValidatorsExitBusReportDataItems = (r: ReportFields) => { + return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; + }; + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); + }; + + const triggerConsensusOnHash = async (hash: string) => { + const { refSlot } = await consensus.getCurrentFrame(); + await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); + await consensus.connect(member3).submitReport(refSlot, hash, CONSENSUS_VERSION); + expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); + }; + + const generateExitRequests = (totalRequests: number) => { + const requestsPerModule = Math.max(1, Math.floor(totalRequests / NUM_MODULES)); + const requestsPerNodeOp = Math.max(1, Math.floor(requestsPerModule / NODE_OPS_PER_MODULE)); + + const requests = []; + + for (let i = 0; i < totalRequests; ++i) { + const moduleId = Math.floor(i / requestsPerModule); + const nodeOpId = Math.floor((i - moduleId * requestsPerModule) / requestsPerNodeOp); + const valIndex = nextValIndex++; + const valPubkey = PUBKEYS[valIndex % PUBKEYS.length]; + requests.push({ moduleId: moduleId + 1, nodeOpId, valIndex, valPubkey }); + } + + return { requests, requestsPerModule, requestsPerNodeOp }; + }; + + const gasUsages: { totalRequests: number; requestsPerModule: number; requestsPerNodeOp: number; gasUsed: number }[] = + []; + + before(async () => { + [admin, member1, member2, member3] = await ethers.getSigners(); + await deploy(); + }); + + after(async () => { + gasUsages.forEach(({ totalRequests, requestsPerModule, requestsPerNodeOp, gasUsed }) => + console.log( + `${totalRequests} requests (per module ${requestsPerModule}, ` + + `per node op ${requestsPerNodeOp}): total gas ${gasUsed}, ` + + `gas per request: ${Math.round(gasUsed / totalRequests)}`, + ), + ); + }); + + for (const totalRequests of [10, 50, 100, 1000, 2000]) { + context(`Total requests: ${totalRequests}`, () => { + let exitRequests: { requests: ExitRequest[]; requestsPerModule: number; requestsPerNodeOp: number }; + let reportFields: ReportFields; + let reportItems: ReturnType; + let reportHash: string; + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("initially, consensus report is not being processed", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + + const report = await oracle.getConsensusReport(); + expect(refSlot).to.above(report.refSlot); + + const procState = await oracle.getProcessingState(); + expect(procState.dataHash, ZeroHash); + expect(procState.dataSubmitted).to.equal(false); + }); + + it("committee reaches consensus on a report hash", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + + exitRequests = generateExitRequests(totalRequests); + + reportFields = { + consensusVersion: CONSENSUS_VERSION, + refSlot: refSlot, + requestsCount: exitRequests.requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests.requests), + }; + + reportItems = getValidatorsExitBusReportDataItems(reportFields); + reportHash = calcValidatorsExitBusReportDataHash(reportItems); + + await triggerConsensusOnHash(reportHash); + }); + + it("oracle gets the report hash", async () => { + const report = await oracle.getConsensusReport(); + expect(report.hash).to.equal(reportHash); + expect(report.refSlot).to.equal(reportFields.refSlot); + expect(report.processingDeadlineTime).to.equal(computeTimestampAtSlot(report.refSlot + SLOTS_PER_FRAME)); + expect(report.processingStarted).to.equal(false); + + const procState = await oracle.getProcessingState(); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.dataSubmitted).to.equal(false); + expect(procState.dataFormat).to.equal(0); + expect(procState.requestsCount).to.equal(0); + expect(procState.requestsSubmitted).to.equal(0); + }); + + it("some time passes", async () => { + await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); + }); + + it(`a committee member submits the report data, exit requests are emitted`, async () => { + const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); + const receipt = await trace("oracle.submit", tx); + await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); + expect((await oracle.getConsensusReport()).processingStarted).to.equal(true); + + const timestamp = await oracle.getTime(); + + for (const request of exitRequests.requests) { + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + const { gasUsed } = receipt; + gasUsages.push({ + totalRequests, + requestsPerModule: exitRequests.requestsPerModule, + requestsPerNodeOp: exitRequests.requestsPerNodeOp, + gasUsed: Number(gasUsed), + }); + }); + + it(`reports are marked as processed`, async () => { + const procState = await oracle.getProcessingState(); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.dataSubmitted).to.equal(true); + expect(procState.dataFormat).to.equal(DATA_FORMAT_LIST); + expect(procState.requestsCount).to.equal(exitRequests.requests.length); + expect(procState.requestsSubmitted).to.equal(exitRequests.requests.length); + }); + + it("some time passes", async () => { + const prevFrame = await consensus.getCurrentFrame(); + await consensus.advanceTimeBy(SECONDS_PER_FRAME - SECONDS_PER_FRAME / 3n); + const newFrame = await consensus.getCurrentFrame(); + expect(newFrame.refSlot).to.above(prevFrame.refSlot); + }); + }); + } +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts new file mode 100644 index 000000000..615050cf4 --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -0,0 +1,256 @@ +import { expect } from "chai"; +import { ZeroHash } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; + +import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; + +import { + computeTimestampAtSlot, + DATA_FORMAT_LIST, + deployVEBO, + initVEBO, + SECONDS_PER_FRAME, + SLOTS_PER_FRAME, +} from "test/deploy"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +describe("ValidatorsExitBusOracle.sol:happyPath", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + + let oracleVersion: bigint; + let exitRequests: ExitRequest[]; + let reportFields: ReportFields; + let reportItems: ReturnType; + let reportHash: string; + + let member1: HardhatEthersSigner; + let member2: HardhatEthersSigner; + let member3: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + const LAST_PROCESSING_REF_SLOT = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + requestsCount: number; + dataFormat: number; + data: string; + } + + const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + return ethers.keccak256(data); + }; + + const getValidatorsExitBusReportDataItems = (r: ReportFields) => { + return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; + }; + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); + }; + + before(async () => { + [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + + await deploy(); + }); + + const triggerConsensusOnHash = async (hash: string) => { + const { refSlot } = await consensus.getCurrentFrame(); + await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); + await consensus.connect(member3).submitReport(refSlot, hash, CONSENSUS_VERSION); + expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); + }; + + it("initially, consensus report is empty and is not being processed", async () => { + const report = await oracle.getConsensusReport(); + expect(report.hash).to.equal(ZeroHash); + + expect(report.processingDeadlineTime).to.equal(0); + expect(report.processingStarted).to.equal(false); + + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(ZeroHash); + expect(procState.processingDeadlineTime).to.equal(0); + expect(procState.dataSubmitted).to.equal(false); + expect(procState.dataFormat).to.equal(0); + expect(procState.requestsCount).to.equal(0); + expect(procState.requestsSubmitted).to.equal(0); + }); + + it("reference slot of the empty initial consensus report is set to the last processing slot passed to the initialize function", async () => { + const report = await oracle.getConsensusReport(); + expect(report.refSlot).to.equal(LAST_PROCESSING_REF_SLOT); + }); + + it("committee reaches consensus on a report hash", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + ]; + + reportFields = { + consensusVersion: CONSENSUS_VERSION, + refSlot: refSlot, + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + reportItems = getValidatorsExitBusReportDataItems(reportFields); + reportHash = calcValidatorsExitBusReportDataHash(reportItems); + + await triggerConsensusOnHash(reportHash); + }); + + it("oracle gets the report hash", async () => { + const report = await oracle.getConsensusReport(); + expect(report.hash).to.equal(reportHash); + expect(report.refSlot).to.equal(reportFields.refSlot); + expect(report.processingDeadlineTime).to.equal(computeTimestampAtSlot(report.refSlot + SLOTS_PER_FRAME)); + + expect(report.processingStarted).to.equal(false); + + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); + expect(procState.dataSubmitted).to.equal(false); + expect(procState.dataFormat).to.equal(0); + expect(procState.requestsCount).to.equal(0); + expect(procState.requestsSubmitted).to.equal(0); + }); + + it("some time passes", async () => { + await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); + }); + + it("non-member cannot submit the data", async () => { + await expect(oracle.connect(stranger).submitReportData(reportFields, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "SenderNotAllowed", + ); + }); + + it("the data cannot be submitted passing a different contract version", async () => { + await expect(oracle.connect(member1).submitReportData(reportFields, oracleVersion - 1n)) + .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") + .withArgs(oracleVersion, oracleVersion - 1n); + }); + + it("the data cannot be submitted passing a different consensus version", async () => { + const invalidReport = { ...reportFields, consensusVersion: CONSENSUS_VERSION + 1n }; + await expect(oracle.connect(member1).submitReportData(invalidReport, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnexpectedConsensusVersion") + .withArgs(CONSENSUS_VERSION, CONSENSUS_VERSION + 1n); + }); + + it("a data not matching the consensus hash cannot be submitted", async () => { + const invalidReport = { ...reportFields, requestsCount: reportFields.requestsCount + 1 }; + const invalidReportItems = getValidatorsExitBusReportDataItems(invalidReport); + const invalidReportHash = calcValidatorsExitBusReportDataHash(invalidReportItems); + + await expect(oracle.connect(member1).submitReportData(invalidReport, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnexpectedDataHash") + .withArgs(reportHash, invalidReportHash); + }); + + it("a committee member submits the report data, exit requests are emitted", async () => { + const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); + + await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); + expect((await oracle.getConsensusReport()).processingStarted).to.equal(true); + + const timestamp = await oracle.getTime(); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); + + it("reports are marked as processed", async () => { + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); + expect(procState.dataSubmitted).to.equal(true); + expect(procState.dataFormat).to.equal(DATA_FORMAT_LIST); + expect(procState.requestsCount).to.equal(exitRequests.length); + expect(procState.requestsSubmitted).to.equal(exitRequests.length); + }); + + it("last requested validator indices are updated", async () => { + const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); + const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); + + expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); + expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); + }); + + it("no data can be submitted for the same reference slot again", async () => { + await expect(oracle.connect(member2).submitReportData(reportFields, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "RefSlotAlreadyProcessing", + ); + }); +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts new file mode 100644 index 000000000..176233135 --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -0,0 +1,278 @@ +import { expect } from "chai"; +import { ContractTransactionReceipt, ZeroHash } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, OracleReportSanityChecker, ValidatorsExitBus__Harness } from "typechain-types"; + +import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; + +import { + computeTimestampAtSlot, + DATA_FORMAT_LIST, + deployVEBO, + initVEBO, + SECONDS_PER_FRAME, + SLOTS_PER_FRAME, +} from "test/deploy"; +import { Snapshot } from "test/suite"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; +const HASH_1 = "0x1111111111111111111111111111111111111111111111111111111111111111"; + +describe("ValidatorsExitBusOracle.sol:submitReportData", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + let oracleReportSanityChecker: OracleReportSanityChecker; + + let oracleVersion: bigint; + + let member1: HardhatEthersSigner; + let member2: HardhatEthersSigner; + let member3: HardhatEthersSigner; + + const LAST_PROCESSING_REF_SLOT = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + requestsCount: number; + dataFormat: number; + data: string; + } + + const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + return ethers.keccak256(data); + }; + + const getValidatorsExitBusReportDataItems = (r: ReportFields) => { + return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; + }; + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const triggerConsensusOnHash = async (hash: string) => { + const { refSlot } = await consensus.getCurrentFrame(); + await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); + await consensus.connect(member3).submitReport(refSlot, hash, CONSENSUS_VERSION); + expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); + }; + + const prepareReportAndSubmitHash = async ( + requests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }], + options = { reportFields: {} }, + ) => { + const { refSlot } = await consensus.getCurrentFrame(); + + const reportData = { + consensusVersion: CONSENSUS_VERSION, + dataFormat: DATA_FORMAT_LIST, + refSlot, + requestsCount: requests.length, + data: encodeExitRequestsDataList(requests), + ...options.reportFields, + }; + + const reportItems = getValidatorsExitBusReportDataItems(reportData); + const reportHash = calcValidatorsExitBusReportDataHash(reportItems); + + await triggerConsensusOnHash(reportHash); + + return { reportData, reportHash }; + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + oracleReportSanityChecker = deployed.oracleReportSanityChecker; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); + }; + + before(async () => { + [admin, member1, member2, member3] = await ethers.getSigners(); + + await deploy(); + }); + + context("discarded report prevents data submit", () => { + let reportData: ReportFields; + let reportHash: string; + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("report is discarded", async () => { + ({ reportData, reportHash } = await prepareReportAndSubmitHash()); + const { refSlot } = await consensus.getCurrentFrame(); + + // change of mind + const tx = await consensus.connect(member3).submitReport(refSlot, HASH_1, CONSENSUS_VERSION); + + await expect(tx).to.emit(oracle, "ReportDiscarded").withArgs(refSlot, reportHash); + }); + + it("processing state reverts to pre-report state ", async () => { + const state = await oracle.getProcessingState(); + expect(state.dataHash).to.equal(ZeroHash); + expect(state.dataSubmitted).to.equal(false); + expect(state.dataFormat).to.equal(0); + expect(state.requestsCount).to.equal(0); + expect(state.requestsSubmitted).to.equal(0); + }); + + it("reverts on trying to submit the discarded report", async () => { + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnexpectedDataHash") + .withArgs(ZeroHash, reportHash); + }); + }); + + context("_handleConsensusReportData", () => { + let originalState: string; + + beforeEach(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("enforces data format", () => { + it("dataFormat = 0 reverts", async () => { + const dataFormatUnsupported = 0; + const { reportData } = await prepareReportAndSubmitHash( + [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], + { reportFields: { dataFormat: dataFormatUnsupported } }, + ); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") + .withArgs(dataFormatUnsupported); + }); + + it("dataFormat = 2 reverts", async () => { + const dataFormatUnsupported = 2; + const { reportData } = await prepareReportAndSubmitHash( + [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], + { reportFields: { dataFormat: dataFormatUnsupported } }, + ); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") + .withArgs(dataFormatUnsupported); + }); + + it("dataFormat = 1 pass", async () => { + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + }); + }); + + context("enforces data length", () => { + it("reverts if there is more data than expected", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + const exitRequests = [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }]; + const { reportData } = await prepareReportAndSubmitHash(exitRequests, { + reportFields: { data: encodeExitRequestsDataList(exitRequests) + "aaaaaaaaaaaaaaaaaa", refSlot }, + }); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + }); + + it("reverts if there is less data than expected", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + const exitRequests = [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }]; + const data = encodeExitRequestsDataList(exitRequests); + + const { reportData } = await prepareReportAndSubmitHash(exitRequests, { + reportFields: { + data: data.slice(0, data.length - 18), + refSlot, + }, + }); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + }); + + it("pass if there is exact amount of data", async () => { + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + }); + }); + + context("invokes sanity check", () => { + // it("reverts if request limit is reached", async () => { + // const exitRequestsLimit = 1; + // await oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(exitRequestsLimit); + // const report = await prepareReportAndSubmitHash([ + // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + // ]); + // await assert.reverts( + // oracle.submitReportData(report, oracleVersion, { from: member1 }), + // `IncorrectNumberOfExitRequestsPerReport(${exitRequestsLimit})`, + // ); + // }); + // it("pass if requests amount equals to limit", async () => { + // const exitRequestsLimit = 1; + // await oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(exitRequestsLimit); + // const report = await prepareReportAndSubmitHash([ + // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + // ]); + // await oracle.submitReportData(report, oracleVersion, { from: member1 }); + // }); + }); + }); +}); From 8fa4443a1d542483fc96bcde163f0709d4f6bbe4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 10 Jan 2025 14:50:55 +0400 Subject: [PATCH 03/65] fix: submitReportData method tests --- ...r-exit-bus-oracle.submitReportData.test.ts | 481 ++++++++++++++++-- 1 file changed, 450 insertions(+), 31 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 176233135..5a79f8ab1 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { ContractTransactionReceipt, ZeroHash } from "ethers"; +import { ZeroHash } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -8,14 +8,7 @@ import { HashConsensus__Harness, OracleReportSanityChecker, ValidatorsExitBus__H import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; -import { - computeTimestampAtSlot, - DATA_FORMAT_LIST, - deployVEBO, - initVEBO, - SECONDS_PER_FRAME, - SLOTS_PER_FRAME, -} from "test/deploy"; +import { computeTimestampAtSlot, DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; import { Snapshot } from "test/suite"; const PUBKEYS = [ @@ -38,6 +31,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { let member1: HardhatEthersSigner; let member2: HardhatEthersSigner; let member3: HardhatEthersSigner; + let stranger: HardhatEthersSigner; const LAST_PROCESSING_REF_SLOT = 1; @@ -102,9 +96,13 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await triggerConsensusOnHash(reportHash); - return { reportData, reportHash }; + return { reportData, reportHash, reportItems }; }; + async function getLastRequestedValidatorIndex(moduleId: number, nodeOpId: number) { + return (await oracle.getLastRequestedValidatorIndices(moduleId, [nodeOpId]))[0]; + } + const deploy = async () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; @@ -127,7 +125,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }; before(async () => { - [admin, member1, member2, member3] = await ethers.getSigners(); + [admin, member1, member2, member3, stranger] = await ethers.getSigners(); await deploy(); }); @@ -253,26 +251,447 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); context("invokes sanity check", () => { - // it("reverts if request limit is reached", async () => { - // const exitRequestsLimit = 1; - // await oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(exitRequestsLimit); - // const report = await prepareReportAndSubmitHash([ - // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, - // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, - // ]); - // await assert.reverts( - // oracle.submitReportData(report, oracleVersion, { from: member1 }), - // `IncorrectNumberOfExitRequestsPerReport(${exitRequestsLimit})`, - // ); - // }); - // it("pass if requests amount equals to limit", async () => { - // const exitRequestsLimit = 1; - // await oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(exitRequestsLimit); - // const report = await prepareReportAndSubmitHash([ - // { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, - // ]); - // await oracle.submitReportData(report, oracleVersion, { from: member1 }); - // }); + before(async () => { + await oracleReportSanityChecker.grantRole( + await oracleReportSanityChecker.MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE(), + admin.address, + ); + }); + + it("reverts if request limit is reached", async () => { + const exitRequestsLimit = 1; + await oracleReportSanityChecker.connect(admin).setMaxExitRequestsPerOracleReport(exitRequestsLimit); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]); + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracleReportSanityChecker, "IncorrectNumberOfExitRequestsPerReport") + .withArgs(exitRequestsLimit); + }); + it("pass if requests amount equals to limit", async () => { + const exitRequestsLimit = 1; + await oracleReportSanityChecker.connect(admin).setMaxExitRequestsPerOracleReport(exitRequestsLimit); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + }); + }); + + context("validates data.requestsCount field with given data", () => { + it("reverts if requestsCount does not match with encoded data size", async () => { + const { reportData } = await prepareReportAndSubmitHash( + [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], + { reportFields: { requestsCount: 2 } }, + ); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "UnexpectedRequestsDataLength", + ); + }); + }); + + it("reverts if moduleId equals zero", async () => { + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 0, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsData", + ); + }); + + it("emits ValidatorExitRequest events", async () => { + const requests = [ + { moduleId: 4, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]; + const { reportData } = await prepareReportAndSubmitHash(requests); + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + const timestamp = await consensus.getTime(); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[0].moduleId, requests[0].nodeOpId, requests[0].valIndex, requests[0].valPubkey, timestamp); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); + }); + + it("updates processing state", async () => { + const storageBefore = await oracle.getDataProcessingState(); + expect(storageBefore.refSlot).to.equal(0); + expect(storageBefore.requestsCount).to.equal(0); + + expect(storageBefore.requestsProcessed).to.equal(0); + expect(storageBefore.dataFormat).to.equal(0); + + const { refSlot } = await consensus.getCurrentFrame(); + const requests = [ + { moduleId: 4, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]; + const { reportData } = await prepareReportAndSubmitHash(requests); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + + const storageAfter = await oracle.getDataProcessingState(); + expect(storageAfter.refSlot).to.equal(refSlot); + expect(storageAfter.requestsCount).to.equal(requests.length); + expect(storageAfter.requestsProcessed).to.equal(requests.length); + expect(storageAfter.dataFormat).to.equal(DATA_FORMAT_LIST); + }); + + it("updates total requests processed count", async () => { + let currentCount = 0; + const countStep0 = await oracle.getTotalRequestsProcessed(); + expect(countStep0).to.equal(currentCount); + + // Step 1 — process 1 item + const requestsStep1 = [{ moduleId: 3, nodeOpId: 1, valIndex: 2, valPubkey: PUBKEYS[1] }]; + const { reportData: reportStep1 } = await prepareReportAndSubmitHash(requestsStep1); + await oracle.connect(member1).submitReportData(reportStep1, oracleVersion); + const countStep1 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep1.length; + expect(countStep1).to.equal(currentCount); + + // Step 2 — process 2 items + await consensus.advanceTimeToNextFrameStart(); + const requestsStep2 = [ + { moduleId: 4, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]; + const { reportData: reportStep2 } = await prepareReportAndSubmitHash(requestsStep2); + await oracle.connect(member1).submitReportData(reportStep2, oracleVersion); + const countStep2 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep2.length; + expect(countStep2).to.equal(currentCount); + + // // Step 3 — process no items + await consensus.advanceTimeToNextFrameStart(); + const requestsStep3: ExitRequest[] = []; + const { reportData: reportStep3 } = await prepareReportAndSubmitHash(requestsStep3); + await oracle.connect(member1).submitReportData(reportStep3, oracleVersion); + const countStep3 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep3.length; + expect(countStep3).to.equal(currentCount); + }); + }); + + context(`requires validator indices for the same node operator to increase`, () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it(`requesting NO 5-3 to exit validator 0`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + expect(await getLastRequestedValidatorIndex(5, 3)).to.equal(0); + }); + + it(`cannot request NO 5-3 to exit validator 0 again`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") + .withArgs(5, 3, 0, 0); + }); + + it(`requesting NO 5-3 to exit validator 1`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[1] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion, { from: member1 }); + expect(await getLastRequestedValidatorIndex(5, 3)).to.equal(1); + }); + + it(`cannot request NO 5-3 to exit validator 1 again`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[1] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") + .withArgs(5, 3, 1, 1); + }); + + it(`cannot request NO 5-3 to exit validator 0 again`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") + .withArgs(5, 3, 1, 0); + }); + + it(`cannot request NO 5-3 to exit validator 1 again (multiple requests)`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[0] }, + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[0] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") + .withArgs(5, 3, 1, 1); + }); + + it(`cannot request NO 5-3 to exit validator 1 again (multiple requests, case 2)`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[3] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[4] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") + .withArgs(5, 3, 1, 1); + }); + + it(`cannot request NO 5-3 to exit validator 2 two times per request`, async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataSortOrder", + ); + }); + }); + + context(`only consensus member or SUBMIT_DATA_ROLE can submit report on unpaused contract`, () => { + let originalState: string; + + beforeEach(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + afterEach(async () => await Snapshot.restore(originalState)); + + it("reverts on stranger", async () => { + const { reportData } = await prepareReportAndSubmitHash(); + + await expect(oracle.connect(stranger).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "SenderNotAllowed", + ); + }); + + it("SUBMIT_DATA_ROLE is allowed", async () => { + oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), stranger, { from: admin }); + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash(); + await oracle.connect(stranger).submitReportData(reportData, oracleVersion); + }); + + it("consensus member is allowed", async () => { + expect(await consensus.getIsMember(member1)).to.equal(true); + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash(); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + }); + + it("reverts on paused contract", async () => { + await consensus.advanceTimeToNextFrameStart(); + const PAUSE_INFINITELY = await oracle.PAUSE_INFINITELY(); + await oracle.pauseFor(PAUSE_INFINITELY, { from: admin }); + const { reportData } = await prepareReportAndSubmitHash(); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "ResumedExpected", + ); + }); + }); + + context("invokes internal baseOracle checks", () => { + let originalState: string; + + beforeEach(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + afterEach(async () => await Snapshot.restore(originalState)); + + it(`reverts on contract version mismatch`, async () => { + const { reportData } = await prepareReportAndSubmitHash(); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion + 1n)) + .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") + .withArgs(oracleVersion, oracleVersion + 1n); + }); + + it("reverts on hash mismatch", async () => { + const requests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }]; + const { reportHash: actualReportHash } = await prepareReportAndSubmitHash(requests); + const newRequests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[1] }]; + + const { refSlot } = await consensus.getCurrentFrame(); + // change pubkey + const reportData = { + consensusVersion: CONSENSUS_VERSION, + dataFormat: DATA_FORMAT_LIST, + refSlot, + requestsCount: newRequests.length, + data: encodeExitRequestsDataList(newRequests), + }; + + const reportItems = getValidatorsExitBusReportDataItems(reportData); + const changedReportHash = calcValidatorsExitBusReportDataHash(reportItems); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "UnexpectedDataHash") + .withArgs(actualReportHash, changedReportHash); + }); + + it("reverts on processing deadline miss", async () => { + const { reportData } = await prepareReportAndSubmitHash(); + const deadline = (await oracle.getConsensusReport()).processingDeadlineTime.toString(10); + await consensus.advanceTimeToNextFrameStart(); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "ProcessingDeadlineMissed") + .withArgs(deadline); + }); + }); + + context("getTotalRequestsProcessed reflects report history", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + after(async () => await Snapshot.restore(originalState)); + + let requestCount = 0; + + it("should be zero at init", async () => { + requestCount = 0; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should increase after report", async () => { + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion, { from: member1 }); + requestCount += 1; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should double increase for two exits", async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[0] }, + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + requestCount += 2; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should not change on empty report", async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + }); + + context("getProcessingState reflects state change", () => { + let originalState: string; + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + after(async () => await Snapshot.restore(originalState)); + + let report: ReportFields; + let hash: string; + + it("has correct defaults on init", async () => { + const state = await oracle.getProcessingState(); + expect(Object.values(state)).to.deep.equal([ + (await consensus.getCurrentFrame()).refSlot, + 0, + ZeroHash, + false, + 0, + 0, + 0, + ]); + }); + + it("consensus report submitted", async () => { + ({ reportData: report, reportHash: hash } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[3] }, + ])); + const state = await oracle.getProcessingState(); + + expect(Object.values(state)).to.deep.equal([ + (await consensus.getCurrentFrame()).refSlot, + computeTimestampAtSlot((await consensus.getCurrentFrame()).reportProcessingDeadlineSlot), + hash, + false, + 0, + 0, + 0, + ]); + }); + + it("report is processed", async () => { + await oracle.connect(member1).submitReportData(report, oracleVersion); + const state = await oracle.getProcessingState(); + expect(Object.values(state)).to.deep.equal([ + (await consensus.getCurrentFrame()).refSlot, + computeTimestampAtSlot((await consensus.getCurrentFrame()).reportProcessingDeadlineSlot), + hash, + true, + DATA_FORMAT_LIST, + 2, + 2, + ]); + }); + + it("at next frame state resets", async () => { + await consensus.advanceTimeToNextFrameStart(); + const state = await oracle.getProcessingState(); + expect(Object.values(state)).to.deep.equal([ + (await consensus.getCurrentFrame()).refSlot, + 0, + ZeroHash, + false, + 0, + 0, + 0, + ]); }); }); }); From 2571fe191e6b4efe7efd981c98743b15dfec03f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:38:15 +0000 Subject: [PATCH 04/65] build(deps): bump undici from 5.28.4 to 5.28.5 Bumps [undici](https://github.com/nodejs/undici) from 5.28.4 to 5.28.5. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9ebdbbfd3..cdc600d4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11731,11 +11731,11 @@ __metadata: linkType: hard "undici@npm:^5.14.0": - version: 5.28.4 - resolution: "undici@npm:5.28.4" + version: 5.28.5 + resolution: "undici@npm:5.28.5" dependencies: "@fastify/busboy": "npm:^2.0.0" - checksum: 10c0/08d0f2596553aa0a54ca6e8e9c7f45aef7d042c60918564e3a142d449eda165a80196f6ef19ea2ef2e6446959e293095d8e40af1236f0d67223b06afac5ecad7 + checksum: 10c0/4dfaa13089fe4c0758f84ec0d34b257e58608e6be3aa540f493b9864b39e3fdcd0a1ace38e434fe79db55f833aa30bcfddd8d6cbe3e0982b0dcae8ec17b65e08 languageName: node linkType: hard From 23a4fa97fda413b8aab7cc1215a4d37f4c0a0020 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 30 Jan 2025 16:29:59 +0500 Subject: [PATCH 05/65] fix: remove unsafeWithdraw --- contracts/0.8.25/vaults/Delegation.sol | 2 +- contracts/0.8.25/vaults/Permissions.sol | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index a725eaec3..e544ff146 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -254,7 +254,7 @@ contract Delegation is Dashboard { if (_recipient == address(0)) revert ZeroArgument("_recipient"); if (_fee == 0) revert ZeroArgument("_fee"); - super._unsafeWithdraw(_recipient, _fee); + stakingVault().withdraw(_recipient, _fee); } /** diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index d2c7b31ea..dac80ccbd 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -118,7 +118,7 @@ abstract contract Permissions is AccessControlVoteable { } function _withdraw(address _recipient, uint256 _ether) internal virtual onlyRole(WITHDRAW_ROLE) { - _unsafeWithdraw(_recipient, _ether); + stakingVault().withdraw(_recipient, _ether); } function _mintShares(address _recipient, uint256 _shares) internal onlyRole(MINT_ROLE) { @@ -153,10 +153,6 @@ abstract contract Permissions is AccessControlVoteable { OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner); } - function _unsafeWithdraw(address _recipient, uint256 _ether) internal { - stakingVault().withdraw(_recipient, _ether); - } - /** * @notice Emitted when the contract is initialized */ From 19755bbcfebbee6d19751244693908bdea9ad8c8 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 30 Jan 2025 16:38:37 +0500 Subject: [PATCH 06/65] feat: move mass-role management to permissions --- contracts/0.8.25/vaults/Dashboard.sol | 36 ------------------------- contracts/0.8.25/vaults/Permissions.sol | 36 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index a00923153..c0d382c31 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -36,14 +36,6 @@ interface IWstETH is IERC20, IERC20Permit { * including funding, withdrawing, minting, burning, and rebalancing operations. */ contract Dashboard is Permissions { - /** - * @notice Struct containing an account and a role for granting/revoking roles. - */ - struct RoleAssignment { - address account; - bytes32 role; - } - /** * @notice Total basis points for fee calculations; equals to 100%. */ @@ -462,34 +454,6 @@ contract Dashboard is Permissions { _resumeBeaconChainDeposits(); } - // ==================== Role Management Functions ==================== - - /** - * @notice Mass-grants multiple roles to multiple accounts. - * @param _assignments An array of role assignments. - * @dev Performs the role admin checks internally. - */ - function grantRoles(RoleAssignment[] memory _assignments) external { - if (_assignments.length == 0) revert ZeroArgument("_assignments"); - - for (uint256 i = 0; i < _assignments.length; i++) { - grantRole(_assignments[i].role, _assignments[i].account); - } - } - - /** - * @notice Mass-revokes multiple roles from multiple accounts. - * @param _assignments An array of role assignments. - * @dev Performs the role admin checks internally. - */ - function revokeRoles(RoleAssignment[] memory _assignments) external { - if (_assignments.length == 0) revert ZeroArgument("_assignments"); - - for (uint256 i = 0; i < _assignments.length; i++) { - revokeRole(_assignments[i].role, _assignments[i].account); - } - } - // ==================== Internal Functions ==================== /** diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index dac80ccbd..2d6b33755 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -17,6 +17,14 @@ import {VaultHub} from "./VaultHub.sol"; * @notice Provides granular permissions for StakingVault operations. */ abstract contract Permissions is AccessControlVoteable { + /** + * @notice Struct containing an account and a role for granting/revoking roles. + */ + struct RoleAssignment { + address account; + bytes32 role; + } + /** * @notice Permission for funding the StakingVault. */ @@ -107,6 +115,34 @@ abstract contract Permissions is AccessControlVoteable { return IStakingVault(addr); } + // ==================== Role Management Functions ==================== + + /** + * @notice Mass-grants multiple roles to multiple accounts. + * @param _assignments An array of role assignments. + * @dev Performs the role admin checks internally. + */ + function grantRoles(RoleAssignment[] memory _assignments) external { + if (_assignments.length == 0) revert ZeroArgument("_assignments"); + + for (uint256 i = 0; i < _assignments.length; i++) { + grantRole(_assignments[i].role, _assignments[i].account); + } + } + + /** + * @notice Mass-revokes multiple roles from multiple accounts. + * @param _assignments An array of role assignments. + * @dev Performs the role admin checks internally. + */ + function revokeRoles(RoleAssignment[] memory _assignments) external { + if (_assignments.length == 0) revert ZeroArgument("_assignments"); + + for (uint256 i = 0; i < _assignments.length; i++) { + revokeRole(_assignments[i].role, _assignments[i].account); + } + } + function _votingCommittee() internal pure virtual returns (bytes32[] memory) { bytes32[] memory roles = new bytes32[](1); roles[0] = DEFAULT_ADMIN_ROLE; From fdb7a08c8a6106001b59b21cfff91162a09d689d Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 30 Jan 2025 16:40:05 +0500 Subject: [PATCH 07/65] fix: rename optional fund modifier --- contracts/0.8.25/vaults/Dashboard.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index c0d382c31..a9a869965 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -206,7 +206,7 @@ contract Dashboard is Permissions { /** * @notice Disconnects the staking vault from the vault hub. */ - function voluntaryDisconnect() external payable fundAndProceed { + function voluntaryDisconnect() external payable fundable { uint256 shares = vaultHub.vaultSocket(address(stakingVault())).sharesMinted; if (shares > 0) { @@ -267,7 +267,7 @@ contract Dashboard is Permissions { * @param _recipient Address of the recipient * @param _amountOfShares Amount of stETH shares to mint */ - function mintShares(address _recipient, uint256 _amountOfShares) external payable fundAndProceed { + function mintShares(address _recipient, uint256 _amountOfShares) external payable fundable { _mintShares(_recipient, _amountOfShares); } @@ -277,7 +277,7 @@ contract Dashboard is Permissions { * @param _recipient Address of the recipient * @param _amountOfStETH Amount of stETH to mint */ - function mintStETH(address _recipient, uint256 _amountOfStETH) external payable virtual fundAndProceed { + function mintStETH(address _recipient, uint256 _amountOfStETH) external payable virtual fundable { _mintShares(_recipient, STETH.getSharesByPooledEth(_amountOfStETH)); } @@ -286,7 +286,7 @@ contract Dashboard is Permissions { * @param _recipient Address of the recipient * @param _amountOfWstETH Amount of tokens to mint */ - function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable fundAndProceed { + function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable fundable { _mintShares(address(this), _amountOfWstETH); uint256 mintedStETH = STETH.getPooledEthBySharesRoundUp(_amountOfWstETH); @@ -399,7 +399,7 @@ contract Dashboard is Permissions { * @notice Rebalances the vault by transferring ether * @param _ether Amount of ether to rebalance */ - function rebalanceVault(uint256 _ether) external payable fundAndProceed { + function rebalanceVault(uint256 _ether) external payable fundable { _rebalanceVault(_ether); } @@ -459,7 +459,7 @@ contract Dashboard is Permissions { /** * @dev Modifier to fund the staking vault if msg.value > 0 */ - modifier fundAndProceed() { + modifier fundable() { if (msg.value > 0) { _fund(msg.value); } From 0ab9aa7d54329ecde43ca10d70189599dbf2d539 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 12:59:07 +0500 Subject: [PATCH 08/65] feat: hooray! renaming! --- .../AccessControlMutuallyConfirmable.sol | 151 ++++++++++++++++++ .../0.8.25/utils/AccessControlVoteable.sol | 150 ----------------- contracts/0.8.25/vaults/Dashboard.sol | 4 +- contracts/0.8.25/vaults/Delegation.sol | 47 +++--- contracts/0.8.25/vaults/Permissions.sol | 10 +- 5 files changed, 182 insertions(+), 180 deletions(-) create mode 100644 contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol delete mode 100644 contracts/0.8.25/utils/AccessControlVoteable.sol diff --git a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol new file mode 100644 index 000000000..f74534334 --- /dev/null +++ b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol"; + +/** + * @title AccessControlMutuallyConfirmable + * @author Lido + * @notice An extension of AccessControlEnumerable that allows exectuing functions by mutual confirmation. + * @dev This contract extends AccessControlEnumerable and adds a confirmation mechanism in the form of a modifier. + */ +abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { + /** + * @notice Tracks confirmations + * - callId: unique identifier for the call, derived as `keccak256(msg.data)` + * - role: role that confirmed the action + * - timestamp: timestamp of the confirmation. + */ + mapping(bytes32 callId => mapping(bytes32 role => uint256 timestamp)) public confirmations; + + /** + * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. + */ + uint256 public confirmLifetime; + + /** + * @dev Restricts execution of the function unless confirmed by all specified roles. + * Confirmation, in this context, is a call to the same function with the same arguments. + * + * The confirmation process works as follows: + * 1. When a role member calls the function: + * - Their confirmation is counted immediately + * - If not enough confirmations exist, their confirmation is recorded + * - If they're not a member of any of the specified roles, the call reverts + * + * 2. Confirmation counting: + * - Counts the current caller's confirmations if they're a member of any of the specified roles + * - Counts existing confirmations that are not expired, i.e. lifetime is not exceeded + * + * 3. Execution: + * - If all members of the specified roles have confirmed, executes the function + * - On successful execution, clears all confirmations for this call + * - If not enough confirmations, stores the current confirmations + * - Thus, if the caller has all the roles, the function is executed immediately + * + * 4. Gas Optimization: + * - Confirmations are stored in a deferred manner using a memory array + * - Confirmation storage writes only occur if the function cannot be executed immediately + * - This prevents unnecessary storage writes when all confirmations are present, + * because the confirmations are cleared anyway after the function is executed, + * - i.e. this optimization is beneficial for the deciding caller and + * saves 1 storage write for each role the deciding caller has + * + * @param _roles Array of role identifiers that must confirm the call in order to execute it + * + * @notice Confirmations past their lifetime are not counted and must be recast + * @notice Only members of the specified roles can submit confirmations + * @notice The order of confirmations does not matter + * + */ + modifier onlyMutuallyConfirmed(bytes32[] memory _roles) { + if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); + + bytes32 callId = keccak256(msg.data); + uint256 numberOfRoles = _roles.length; + uint256 confirmValidSince = block.timestamp - confirmLifetime; + uint256 numberOfConfirms = 0; + bool[] memory deferredConfirms = new bool[](numberOfRoles); + bool isRoleMember = false; + + for (uint256 i = 0; i < numberOfRoles; ++i) { + bytes32 role = _roles[i]; + + if (super.hasRole(role, msg.sender)) { + isRoleMember = true; + numberOfConfirms++; + deferredConfirms[i] = true; + + emit RoleMemberConfirmed(msg.sender, role, block.timestamp, msg.data); + } else if (confirmations[callId][role] >= confirmValidSince) { + numberOfConfirms++; + } + } + + if (!isRoleMember) revert SenderNotMember(); + + if (numberOfConfirms == numberOfRoles) { + for (uint256 i = 0; i < numberOfRoles; ++i) { + bytes32 role = _roles[i]; + delete confirmations[callId][role]; + } + _; + } else { + for (uint256 i = 0; i < numberOfRoles; ++i) { + if (deferredConfirms[i]) { + bytes32 role = _roles[i]; + confirmations[callId][role] = block.timestamp; + } + } + } + } + + /** + * @notice Sets the confirmation lifetime. + * Confirmation lifetime is a period during which the confirmation is counted. Once the period is over, + * the confirmation is considered expired, no longer counts and must be recasted for the confirmation to go through. + * @param _newConfirmLifetime The new confirmation lifetime in seconds. + */ + function _setConfirmLifetime(uint256 _newConfirmLifetime) internal { + if (_newConfirmLifetime == 0) revert ConfirmLifetimeCannotBeZero(); + + uint256 oldConfirmLifetime = confirmLifetime; + confirmLifetime = _newConfirmLifetime; + + emit ConfirmLifetimeSet(msg.sender, oldConfirmLifetime, _newConfirmLifetime); + } + + /** + * @dev Emitted when the confirmation lifetime is set. + * @param oldConfirmLifetime The old confirmation lifetime. + * @param newConfirmLifetime The new confirmation lifetime. + */ + event ConfirmLifetimeSet(address indexed sender, uint256 oldConfirmLifetime, uint256 newConfirmLifetime); + + /** + * @dev Emitted when a role member confirms. + * @param member The address of the confirming member. + * @param role The role of the confirming member. + * @param timestamp The timestamp of the confirmation. + * @param data The msg.data of the confirmation (selector + arguments). + */ + event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 timestamp, bytes data); + + /** + * @dev Thrown when attempting to set confirmation lifetime to zero. + */ + error ConfirmLifetimeCannotBeZero(); + + /** + * @dev Thrown when attempting to confirm when the confirmation lifetime is not set. + */ + error ConfirmLifetimeNotSet(); + + /** + * @dev Thrown when a caller without a required role attempts to confirm. + */ + error SenderNotMember(); +} diff --git a/contracts/0.8.25/utils/AccessControlVoteable.sol b/contracts/0.8.25/utils/AccessControlVoteable.sol deleted file mode 100644 index b078dea5b..000000000 --- a/contracts/0.8.25/utils/AccessControlVoteable.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -pragma solidity 0.8.25; - -import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol"; - -abstract contract AccessControlVoteable is AccessControlEnumerable { - /** - * @notice Tracks committee votes - * - callId: unique identifier for the call, derived as `keccak256(msg.data)` - * - role: role that voted - * - voteTimestamp: timestamp of the vote. - * The term "voting" refers to the entire voting process through which vote-restricted actions are performed. - * The term "vote" refers to a single individual vote cast by a committee member. - */ - mapping(bytes32 callId => mapping(bytes32 role => uint256 voteTimestamp)) public votings; - - /** - * @notice Vote lifetime in seconds; after this period, the vote expires and no longer counts. - */ - uint256 public voteLifetime; - - /** - * @dev Modifier that implements a mechanism for multi-role committee approval. - * Each unique function call (identified by msg.data: selector + arguments) requires - * approval from all committee role members within a specified time window. - * - * The voting process works as follows: - * 1. When a committee member calls the function: - * - Their vote is counted immediately - * - If not enough votes exist, their vote is recorded - * - If they're not a committee member, the call reverts - * - * 2. Vote counting: - * - Counts the current caller's votes if they're a committee member - * - Counts existing votes that are within the voting period - * - All votes must occur within the same voting period window - * - * 3. Execution: - * - If all committee members have voted within the period, executes the function - * - On successful execution, clears all voting state for this call - * - If not enough votes, stores the current votes - * - Thus, if the caller has all the roles, the function is executed immediately - * - * 4. Gas Optimization: - * - Votes are stored in a deferred manner using a memory array - * - Vote storage writes only occur if the function cannot be executed immediately - * - This prevents unnecessary storage writes when all votes are present, - * because the votes are cleared anyway after the function is executed, - * - i.e. this optimization is beneficial for the deciding caller and - * saves 1 storage write for each role the deciding caller has - * - * @param _committee Array of role identifiers that form the voting committee - * - * @notice Votes expire after the voting period and must be recast - * @notice All committee members must vote within the same voting period - * @notice Only committee members can initiate votes - * - * @custom:security-note Each unique function call (including parameters) requires its own set of votes - */ - modifier onlyIfVotedBy(bytes32[] memory _committee) { - if (voteLifetime == 0) revert VoteLifetimeNotSet(); - - bytes32 callId = keccak256(msg.data); - uint256 committeeSize = _committee.length; - uint256 votingStart = block.timestamp - voteLifetime; - uint256 voteTally = 0; - bool[] memory deferredVotes = new bool[](committeeSize); - bool isCommitteeMember = false; - - for (uint256 i = 0; i < committeeSize; ++i) { - bytes32 role = _committee[i]; - - if (super.hasRole(role, msg.sender)) { - isCommitteeMember = true; - voteTally++; - deferredVotes[i] = true; - - emit RoleMemberVoted(msg.sender, role, block.timestamp, msg.data); - } else if (votings[callId][role] >= votingStart) { - voteTally++; - } - } - - if (!isCommitteeMember) revert NotACommitteeMember(); - - if (voteTally == committeeSize) { - for (uint256 i = 0; i < committeeSize; ++i) { - bytes32 role = _committee[i]; - delete votings[callId][role]; - } - _; - } else { - for (uint256 i = 0; i < committeeSize; ++i) { - if (deferredVotes[i]) { - bytes32 role = _committee[i]; - votings[callId][role] = block.timestamp; - } - } - } - } - - /** - * @notice Sets the vote lifetime. - * Vote lifetime is a period during which the vote is counted. Once the period is over, - * the vote is considered expired, no longer counts and must be recasted for the voting to go through. - * @param _newVoteLifetime The new vote lifetime in seconds. - */ - function _setVoteLifetime(uint256 _newVoteLifetime) internal { - if (_newVoteLifetime == 0) revert VoteLifetimeCannotBeZero(); - - uint256 oldVoteLifetime = voteLifetime; - voteLifetime = _newVoteLifetime; - - emit VoteLifetimeSet(msg.sender, oldVoteLifetime, _newVoteLifetime); - } - - /** - * @dev Emitted when the vote lifetime is set. - * @param oldVoteLifetime The old vote lifetime. - * @param newVoteLifetime The new vote lifetime. - */ - event VoteLifetimeSet(address indexed sender, uint256 oldVoteLifetime, uint256 newVoteLifetime); - - /** - * @dev Emitted when a committee member votes. - * @param member The address of the voting member. - * @param role The role of the voting member. - * @param timestamp The timestamp of the vote. - * @param data The msg.data of the vote. - */ - event RoleMemberVoted(address indexed member, bytes32 indexed role, uint256 timestamp, bytes data); - - /** - * @dev Thrown when attempting to set vote lifetime to zero. - */ - error VoteLifetimeCannotBeZero(); - - /** - * @dev Thrown when attempting to vote when the vote lifetime is zero. - */ - error VoteLifetimeNotSet(); - - /** - * @dev Thrown when a caller without a required role attempts to vote. - */ - error NotACommitteeMember(); -} diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index a9a869965..908473ff7 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -99,8 +99,8 @@ contract Dashboard is Permissions { // ==================== View Functions ==================== - function votingCommittee() external pure returns (bytes32[] memory) { - return _votingCommittee(); + function confirmingRoles() external pure returns (bytes32[] memory) { + return _confirmingRoles(); } /** diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index e544ff146..26e942495 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -36,9 +36,9 @@ contract Delegation is Dashboard { * @notice Curator role: * - sets curator fee; * - claims curator fee; - * - votes on vote lifetime; - * - votes on node operator fee; - * - votes on ownership transfer; + * - confirms confirm lifetime; + * - confirms node operator fee; + * - confirms ownership transfer; * - pauses deposits to beacon chain; * - resumes deposits to beacon chain. */ @@ -46,9 +46,9 @@ contract Delegation is Dashboard { /** * @notice Node operator manager role: - * - votes on vote lifetime; - * - votes on node operator fee; - * - votes on ownership transfer; + * - confirms confirm lifetime; + * - confirms node operator fee; + * - confirms ownership transfer; * - assigns NODE_OPERATOR_FEE_CLAIMER_ROLE. */ bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("Vault.Delegation.NodeOperatorManagerRole"); @@ -92,7 +92,7 @@ contract Delegation is Dashboard { /** * @notice Initializes the contract: * - sets up the roles; - * - sets the vote lifetime to 7 days (can be changed later by CURATOR_ROLE and NODE_OPERATOR_MANAGER_ROLE). + * - sets the confirm lifetime to 7 days (can be changed later by CURATOR_ROLE and NODE_OPERATOR_MANAGER_ROLE). * @dev The msg.sender here is VaultFactory. The VaultFactory is temporarily granted * DEFAULT_ADMIN_ROLE AND NODE_OPERATOR_MANAGER_ROLE to be able to set initial fees and roles in VaultFactory. * All the roles are revoked from VaultFactory by the end of the initialization. @@ -152,13 +152,13 @@ contract Delegation is Dashboard { } /** - * @notice Sets the vote lifetime. - * Vote lifetime is a period during which the vote is counted. Once the period is over, - * the vote is considered expired, no longer counts and must be recasted for the voting to go through. - * @param _newVoteLifetime The new vote lifetime in seconds. + * @notice Sets the confirm lifetime. + * Confirm lifetime is a period during which the confirm is counted. Once the period is over, + * the confirm is considered expired, no longer counts and must be recasted. + * @param _newConfirmLifetime The new confirm lifetime in seconds. */ - function setVoteLifetime(uint256 _newVoteLifetime) external onlyIfVotedBy(_votingCommittee()) { - _setVoteLifetime(_newVoteLifetime); + function setConfirmLifetime(uint256 _newConfirmLifetime) external onlyMutuallyConfirmed(_confirmingRoles()) { + _setConfirmLifetime(_newConfirmLifetime); } /** @@ -181,11 +181,11 @@ contract Delegation is Dashboard { * @notice Sets the node operator fee. * The node operator fee is the percentage (in basis points) of node operator's share of the StakingVault rewards. * The node operator fee combined with the curator fee cannot exceed 100%. - * Note that the function reverts if the node operator fee is unclaimed and all the votes must be recasted to execute it again, - * which is why the deciding voter must make sure that `nodeOperatorUnclaimedFee()` is 0 before calling this function. + * Note that the function reverts if the node operator fee is unclaimed and all the confirms must be recasted to execute it again, + * which is why the deciding confirm must make sure that `nodeOperatorUnclaimedFee()` is 0 before calling this function. * @param _newNodeOperatorFeeBP The new node operator fee in basis points. */ - function setNodeOperatorFeeBP(uint256 _newNodeOperatorFeeBP) external onlyIfVotedBy(_votingCommittee()) { + function setNodeOperatorFeeBP(uint256 _newNodeOperatorFeeBP) external onlyMutuallyConfirmed(_confirmingRoles()) { if (_newNodeOperatorFeeBP + curatorFeeBP > MAX_FEE_BP) revert CombinedFeesExceed100Percent(); if (nodeOperatorUnclaimedFee() > 0) revert NodeOperatorFeeUnclaimed(); uint256 oldNodeOperatorFeeBP = nodeOperatorFeeBP; @@ -258,16 +258,17 @@ contract Delegation is Dashboard { } /** - * @notice Returns the committee that can: - * - change the vote lifetime; + * @notice Returns the roles that can: + * - change the confirm lifetime; + * - set the curator fee; * - set the node operator fee; * - transfer the ownership of the StakingVault. - * @return committee is an array of roles that form the voting committee. + * @return roles is an array of roles that form the confirming roles. */ - function _votingCommittee() internal pure override returns (bytes32[] memory committee) { - committee = new bytes32[](2); - committee[0] = CURATOR_ROLE; - committee[1] = NODE_OPERATOR_MANAGER_ROLE; + function _confirmingRoles() internal pure override returns (bytes32[] memory roles) { + roles = new bytes32[](2); + roles[0] = CURATOR_ROLE; + roles[1] = NODE_OPERATOR_MANAGER_ROLE; } /** diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 2d6b33755..553d5d39e 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.25; import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol"; -import {AccessControlVoteable} from "contracts/0.8.25/utils/AccessControlVoteable.sol"; +import {AccessControlMutuallyConfirmable} from "contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol"; import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; @@ -16,7 +16,7 @@ import {VaultHub} from "./VaultHub.sol"; * @author Lido * @notice Provides granular permissions for StakingVault operations. */ -abstract contract Permissions is AccessControlVoteable { +abstract contract Permissions is AccessControlMutuallyConfirmable { /** * @notice Struct containing an account and a role for granting/revoking roles. */ @@ -101,7 +101,7 @@ abstract contract Permissions is AccessControlVoteable { vaultHub = VaultHub(stakingVault().vaultHub()); _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setVoteLifetime(7 days); + _setConfirmLifetime(7 days); emit Initialized(); } @@ -143,7 +143,7 @@ abstract contract Permissions is AccessControlVoteable { } } - function _votingCommittee() internal pure virtual returns (bytes32[] memory) { + function _confirmingRoles() internal pure virtual returns (bytes32[] memory) { bytes32[] memory roles = new bytes32[](1); roles[0] = DEFAULT_ADMIN_ROLE; return roles; @@ -185,7 +185,7 @@ abstract contract Permissions is AccessControlVoteable { vaultHub.voluntaryDisconnect(address(stakingVault())); } - function _transferStakingVaultOwnership(address _newOwner) internal onlyIfVotedBy(_votingCommittee()) { + function _transferStakingVaultOwnership(address _newOwner) internal onlyMutuallyConfirmed(_confirmingRoles()) { OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner); } From 8c43b1329da2c739341dfcffee63582256243314 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:01:37 +0500 Subject: [PATCH 09/65] feat: update role ids --- contracts/0.8.25/vaults/Delegation.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index 26e942495..4c879e953 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -42,7 +42,7 @@ contract Delegation is Dashboard { * - pauses deposits to beacon chain; * - resumes deposits to beacon chain. */ - bytes32 public constant CURATOR_ROLE = keccak256("Vault.Delegation.CuratorRole"); + bytes32 public constant CURATOR_ROLE = keccak256("vaults.Delegation.CuratorRole"); /** * @notice Node operator manager role: @@ -51,13 +51,13 @@ contract Delegation is Dashboard { * - confirms ownership transfer; * - assigns NODE_OPERATOR_FEE_CLAIMER_ROLE. */ - bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("Vault.Delegation.NodeOperatorManagerRole"); + bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.Delegation.NodeOperatorManagerRole"); /** * @notice Node operator fee claimer role: * - claims node operator fee. */ - bytes32 public constant NODE_OPERATOR_FEE_CLAIMER_ROLE = keccak256("Vault.Delegation.NodeOperatorFeeClaimerRole"); + bytes32 public constant NODE_OPERATOR_FEE_CLAIMER_ROLE = keccak256("vaults.Delegation.NodeOperatorFeeClaimerRole"); /** * @notice Curator fee in basis points; combined with node operator fee cannot exceed 100%. From 3403a81d32a06f7f28985935f6a6fc4d049fe30d Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:02:46 +0500 Subject: [PATCH 10/65] fix(Permissions): update role ids --- contracts/0.8.25/vaults/Permissions.sol | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 553d5d39e..caa79ecea 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -28,49 +28,48 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { /** * @notice Permission for funding the StakingVault. */ - bytes32 public constant FUND_ROLE = keccak256("StakingVault.Permissions.Fund"); + bytes32 public constant FUND_ROLE = keccak256("vaults.Permissions.Fund"); /** * @notice Permission for withdrawing funds from the StakingVault. */ - bytes32 public constant WITHDRAW_ROLE = keccak256("StakingVault.Permissions.Withdraw"); + bytes32 public constant WITHDRAW_ROLE = keccak256("vaults.Permissions.Withdraw"); /** * @notice Permission for minting stETH shares backed by the StakingVault. */ - bytes32 public constant MINT_ROLE = keccak256("StakingVault.Permissions.Mint"); + bytes32 public constant MINT_ROLE = keccak256("vaults.Permissions.Mint"); /** * @notice Permission for burning stETH shares backed by the StakingVault. */ - bytes32 public constant BURN_ROLE = keccak256("StakingVault.Permissions.Burn"); + bytes32 public constant BURN_ROLE = keccak256("vaults.Permissions.Burn"); /** * @notice Permission for rebalancing the StakingVault. */ - bytes32 public constant REBALANCE_ROLE = keccak256("StakingVault.Permissions.Rebalance"); + bytes32 public constant REBALANCE_ROLE = keccak256("vaults.Permissions.Rebalance"); /** * @notice Permission for pausing beacon chain deposits on the StakingVault. */ - bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = - keccak256("StakingVault.Permissions.PauseBeaconChainDeposits"); + bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseBeaconChainDeposits"); /** * @notice Permission for resuming beacon chain deposits on the StakingVault. */ bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = - keccak256("StakingVault.Permissions.ResumeBeaconChainDeposits"); + keccak256("vaults.Permissions.ResumeBeaconChainDeposits"); /** * @notice Permission for requesting validator exit from the StakingVault. */ - bytes32 public constant REQUEST_VALIDATOR_EXIT_ROLE = keccak256("StakingVault.Permissions.RequestValidatorExit"); + bytes32 public constant REQUEST_VALIDATOR_EXIT_ROLE = keccak256("vaults.Permissions.RequestValidatorExit"); /** * @notice Permission for voluntary disconnecting the StakingVault. */ - bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("StakingVault.Permissions.VoluntaryDisconnect"); + bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("vaults.Permissions.VoluntaryDisconnect"); /** * @notice Address of the implementation contract From 18f184bee4112bbf71765fc4cef3d01ab72b73e4 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:05:29 +0500 Subject: [PATCH 11/65] refactor(Permissions): hide assembly in an internal func --- contracts/0.8.25/vaults/Permissions.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index caa79ecea..2c4a6073a 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -106,12 +106,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { } function stakingVault() public view returns (IStakingVault) { - bytes memory args = Clones.fetchCloneArgs(address(this)); - address addr; - assembly { - addr := mload(add(args, 32)) - } - return IStakingVault(addr); + return IStakingVault(_loadStakingVaultAddress()); } // ==================== Role Management Functions ==================== @@ -188,6 +183,13 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner); } + function _loadStakingVaultAddress() internal view returns (address addr) { + bytes memory args = Clones.fetchCloneArgs(address(this)); + assembly { + addr := mload(add(args, 32)) + } + } + /** * @notice Emitted when the contract is initialized */ From 98390095fb51061fcc21a33ea78029fe59d4816c Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:06:43 +0500 Subject: [PATCH 12/65] feat: log default admin in initialized event --- contracts/0.8.25/vaults/Permissions.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 2c4a6073a..6672b909c 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -102,7 +102,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { _setConfirmLifetime(7 days); - emit Initialized(); + emit Initialized(_defaultAdmin); } function stakingVault() public view returns (IStakingVault) { @@ -193,7 +193,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { /** * @notice Emitted when the contract is initialized */ - event Initialized(); + event Initialized(address _defaultAdmin); /** * @notice Error when direct calls to the implementation are forbidden From adeb088227113879cda8f1f7f266c1f28a14ffb4 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:15:45 +0500 Subject: [PATCH 13/65] feat: pass confirm lifetime as init param --- contracts/0.8.25/vaults/Dashboard.sol | 4 ++-- contracts/0.8.25/vaults/Delegation.sol | 4 ++-- contracts/0.8.25/vaults/Permissions.sol | 4 ++-- contracts/0.8.25/vaults/VaultFactory.sol | 3 ++- .../dashboard/contracts/VaultFactory__MockForDashboard.sol | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 908473ff7..abb47d3e6 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -89,12 +89,12 @@ contract Dashboard is Permissions { /** * @notice Initializes the contract with the default admin role */ - function initialize(address _defaultAdmin) external virtual { + function initialize(address _defaultAdmin, uint256 _confirmLifetime) external virtual { // reduces gas cost for `mintWsteth` // invariant: dashboard does not hold stETH on its balance STETH.approve(address(WSTETH), type(uint256).max); - _initialize(_defaultAdmin); + _initialize(_defaultAdmin, _confirmLifetime); } // ==================== View Functions ==================== diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index 4c879e953..ea6715415 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -97,8 +97,8 @@ contract Delegation is Dashboard { * DEFAULT_ADMIN_ROLE AND NODE_OPERATOR_MANAGER_ROLE to be able to set initial fees and roles in VaultFactory. * All the roles are revoked from VaultFactory by the end of the initialization. */ - function initialize(address _defaultAdmin) external override { - _initialize(_defaultAdmin); + function initialize(address _defaultAdmin, uint256 _confirmLifetime) external override { + _initialize(_defaultAdmin, _confirmLifetime); // the next line implies that the msg.sender is an operator // however, the msg.sender is the VaultFactory, and the role will be revoked diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 6672b909c..419e63428 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -91,7 +91,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { _SELF = address(this); } - function _initialize(address _defaultAdmin) internal { + function _initialize(address _defaultAdmin, uint256 _confirmLifetime) internal { if (initialized) revert AlreadyInitialized(); if (address(this) == _SELF) revert NonProxyCallsForbidden(); if (_defaultAdmin == address(0)) revert ZeroArgument("_defaultAdmin"); @@ -100,7 +100,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { vaultHub = VaultHub(stakingVault().vaultHub()); _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setConfirmLifetime(7 days); + _setConfirmLifetime(_confirmLifetime); emit Initialized(_defaultAdmin); } diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index b971e51f4..65b0c2bbb 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -26,6 +26,7 @@ struct DelegationConfig { address nodeOperatorFeeClaimer; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; + uint256 confirmLifetime; } contract VaultFactory { @@ -66,7 +67,7 @@ contract VaultFactory { ); // initialize Delegation - delegation.initialize(address(this)); + delegation.initialize(address(this), _delegationConfig.confirmLifetime); // setup roles delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), _delegationConfig.defaultAdmin); diff --git a/test/0.8.25/vaults/dashboard/contracts/VaultFactory__MockForDashboard.sol b/test/0.8.25/vaults/dashboard/contracts/VaultFactory__MockForDashboard.sol index 2404ca20d..caacda986 100644 --- a/test/0.8.25/vaults/dashboard/contracts/VaultFactory__MockForDashboard.sol +++ b/test/0.8.25/vaults/dashboard/contracts/VaultFactory__MockForDashboard.sol @@ -28,7 +28,7 @@ contract VaultFactory__MockForDashboard is UpgradeableBeacon { bytes memory immutableArgs = abi.encode(vault); dashboard = Dashboard(payable(Clones.cloneWithImmutableArgs(dashboardImpl, immutableArgs))); - dashboard.initialize(address(this)); + dashboard.initialize(address(this), 7 days); dashboard.grantRole(dashboard.DEFAULT_ADMIN_ROLE(), msg.sender); dashboard.grantRole(dashboard.FUND_ROLE(), msg.sender); dashboard.grantRole(dashboard.WITHDRAW_ROLE(), msg.sender); From 34c2e6243413837615498a1ab8e1b37a54e3eeb8 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 31 Jan 2025 13:38:50 +0500 Subject: [PATCH 14/65] feat: don't use msg.sender in init --- contracts/0.8.25/vaults/Delegation.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index ea6715415..feafb9cf9 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -103,7 +103,7 @@ contract Delegation is Dashboard { // the next line implies that the msg.sender is an operator // however, the msg.sender is the VaultFactory, and the role will be revoked // at the end of the initialization - _grantRole(NODE_OPERATOR_MANAGER_ROLE, msg.sender); + _grantRole(NODE_OPERATOR_MANAGER_ROLE, _defaultAdmin); _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_FEE_CLAIMER_ROLE, NODE_OPERATOR_MANAGER_ROLE); } From 038e7224afcc693c3528b8890c718af835863c62 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 3 Feb 2025 16:07:08 +0500 Subject: [PATCH 15/65] fix: modifier order --- contracts/0.8.25/vaults/Dashboard.sol | 66 ++++++++++++++------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index abb47d3e6..245423cda 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -323,38 +323,7 @@ contract Dashboard is Permissions { _burnWstETH(_amountOfWstETH); } - /** - * @dev Modifier to check if the permit is successful, and if not, check if the allowance is sufficient - */ - modifier safePermit( - address token, - address owner, - address spender, - PermitInput calldata permitInput - ) { - // Try permit() before allowance check to advance nonce if possible - try - IERC20Permit(token).permit( - owner, - spender, - permitInput.value, - permitInput.deadline, - permitInput.v, - permitInput.r, - permitInput.s - ) - { - _; - return; - } catch { - // Permit potentially got frontran. Continue anyways if allowance is sufficient. - if (IERC20(token).allowance(owner, spender) >= permitInput.value) { - _; - return; - } - } - revert InvalidPermit(token); - } + // TODO: move down /** * @notice Burns stETH tokens (in shares) backed by the vault from the sender using permit (with value in stETH). @@ -466,6 +435,39 @@ contract Dashboard is Permissions { _; } + /** + * @dev Modifier to check if the permit is successful, and if not, check if the allowance is sufficient + */ + modifier safePermit( + address token, + address owner, + address spender, + PermitInput calldata permitInput + ) { + // Try permit() before allowance check to advance nonce if possible + try + IERC20Permit(token).permit( + owner, + spender, + permitInput.value, + permitInput.deadline, + permitInput.v, + permitInput.r, + permitInput.s + ) + { + _; + return; + } catch { + // Permit potentially got frontran. Continue anyways if allowance is sufficient. + if (IERC20(token).allowance(owner, spender) >= permitInput.value) { + _; + return; + } + } + revert InvalidPermit(token); + } + /** /** From b16075caa3b4b40b41e658866a39b59e39eb7369 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 3 Feb 2025 16:07:18 +0500 Subject: [PATCH 16/65] test: permissions setup --- .../contracts/Permissions__Harness.sol | 56 +++++++ .../VaultFactory__MockPermissions.sol | 98 +++++++++++++ .../contracts/VaultHub__MockPermissions.sol | 10 ++ .../vaults/permissions/permissions.test.ts | 138 ++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol create mode 100644 test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol create mode 100644 test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol create mode 100644 test/0.8.25/vaults/permissions/permissions.test.ts diff --git a/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol new file mode 100644 index 000000000..d73cbb826 --- /dev/null +++ b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity ^0.8.0; + +import {Permissions} from "contracts/0.8.25/vaults/Permissions.sol"; + +contract Permissions__Harness is Permissions { + function initialize(address _defaultAdmin, uint256 _confirmLifetime) external { + _initialize(_defaultAdmin, _confirmLifetime); + } + + function confirmingRoles() external pure returns (bytes32[] memory) { + return _confirmingRoles(); + } + + function fund(uint256 _ether) external { + _fund(_ether); + } + + function withdraw(address _recipient, uint256 _ether) external { + _withdraw(_recipient, _ether); + } + + function mintShares(address _recipient, uint256 _shares) external { + _mintShares(_recipient, _shares); + } + + function burnShares(uint256 _shares) external { + _burnShares(_shares); + } + + function rebalanceVault(uint256 _ether) external { + _rebalanceVault(_ether); + } + + function pauseBeaconChainDeposits() external { + _pauseBeaconChainDeposits(); + } + + function resumeBeaconChainDeposits() external { + _resumeBeaconChainDeposits(); + } + + function requestValidatorExit(bytes calldata _pubkey) external { + _requestValidatorExit(_pubkey); + } + + function transferStakingVaultOwnership(address _newOwner) external { + _transferStakingVaultOwnership(_newOwner); + } + + function setConfirmLifetime(uint256 _newConfirmLifetime) external { + _setConfirmLifetime(_newConfirmLifetime); + } +} diff --git a/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol new file mode 100644 index 000000000..ba372a73c --- /dev/null +++ b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity ^0.8.0; + +import {BeaconProxy} from "@openzeppelin/contracts-v5.2/proxy/beacon/BeaconProxy.sol"; +import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol"; + +import {Permissions__Harness} from "./Permissions__Harness.sol"; +import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; + +struct PermissionsConfig { + address defaultAdmin; + address nodeOperator; + uint256 confirmLifetime; + address funder; + address withdrawer; + address minter; + address burner; + address rebalancer; + address depositPauser; + address depositResumer; + address exitRequester; + address disconnecter; +} + +contract VaultFactory__MockPermissions { + address public immutable BEACON; + address public immutable PERMISSIONS_IMPL; + + /// @param _beacon The address of the beacon contract + /// @param _permissionsImpl The address of the Permissions implementation + constructor(address _beacon, address _permissionsImpl) { + if (_beacon == address(0)) revert ZeroArgument("_beacon"); + if (_permissionsImpl == address(0)) revert ZeroArgument("_permissionsImpl"); + + BEACON = _beacon; + PERMISSIONS_IMPL = _permissionsImpl; + } + + /// @notice Creates a new StakingVault and Permissions contracts + /// @param _permissionsConfig The params of permissions initialization + /// @param _stakingVaultInitializerExtraParams The params of vault initialization + function createVaultWithPermissions( + PermissionsConfig calldata _permissionsConfig, + bytes calldata _stakingVaultInitializerExtraParams + ) external returns (IStakingVault vault, Permissions__Harness permissions) { + // create StakingVault + vault = IStakingVault(address(new BeaconProxy(BEACON, ""))); + + // create Permissions + bytes memory immutableArgs = abi.encode(vault); + permissions = Permissions__Harness(payable(Clones.cloneWithImmutableArgs(PERMISSIONS_IMPL, immutableArgs))); + + // initialize StakingVault + vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); + + // initialize Permissions + permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + + // setup roles + permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); + permissions.grantRole(permissions.FUND_ROLE(), _permissionsConfig.funder); + permissions.grantRole(permissions.WITHDRAW_ROLE(), _permissionsConfig.withdrawer); + permissions.grantRole(permissions.MINT_ROLE(), _permissionsConfig.minter); + permissions.grantRole(permissions.BURN_ROLE(), _permissionsConfig.burner); + permissions.grantRole(permissions.REBALANCE_ROLE(), _permissionsConfig.rebalancer); + permissions.grantRole(permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositPauser); + permissions.grantRole(permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositResumer); + permissions.grantRole(permissions.REQUEST_VALIDATOR_EXIT_ROLE(), _permissionsConfig.exitRequester); + permissions.grantRole(permissions.VOLUNTARY_DISCONNECT_ROLE(), _permissionsConfig.disconnecter); + + permissions.revokeRole(permissions.DEFAULT_ADMIN_ROLE(), address(this)); + + emit VaultCreated(address(permissions), address(vault)); + emit PermissionsCreated(_permissionsConfig.defaultAdmin, address(permissions)); + } + + /** + * @notice Event emitted on a Vault creation + * @param owner The address of the Vault owner + * @param vault The address of the created Vault + */ + event VaultCreated(address indexed owner, address indexed vault); + + /** + * @notice Event emitted on a Permissions creation + * @param admin The address of the Permissions admin + * @param permissions The address of the created Permissions + */ + event PermissionsCreated(address indexed admin, address indexed permissions); + + /** + * @notice Error thrown for when a given value cannot be zero + * @param argument Name of the argument + */ + error ZeroArgument(string argument); +} diff --git a/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol new file mode 100644 index 000000000..f68a3f5a3 --- /dev/null +++ b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity ^0.8.0; + +contract VaultHub__MockPermissions { + function hello() external pure returns (string memory) { + return "hello"; + } +} diff --git a/test/0.8.25/vaults/permissions/permissions.test.ts b/test/0.8.25/vaults/permissions/permissions.test.ts new file mode 100644 index 000000000..868ff179e --- /dev/null +++ b/test/0.8.25/vaults/permissions/permissions.test.ts @@ -0,0 +1,138 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + DepositContract__MockForStakingVault, + Permissions__Harness, + Permissions__Harness__factory, + StakingVault, + StakingVault__factory, + UpgradeableBeacon, + VaultFactory__MockPermissions, + VaultHub__MockPermissions, +} from "typechain-types"; +import { PermissionsConfigStruct } from "typechain-types/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions"; + +import { days, findEvents } from "lib"; + +describe("Permissions", () => { + let deployer: HardhatEthersSigner; + let defaultAdmin: HardhatEthersSigner; + let nodeOperator: HardhatEthersSigner; + let funder: HardhatEthersSigner; + let withdrawer: HardhatEthersSigner; + let minter: HardhatEthersSigner; + let burner: HardhatEthersSigner; + let rebalancer: HardhatEthersSigner; + let depositPauser: HardhatEthersSigner; + let depositResumer: HardhatEthersSigner; + let exitRequester: HardhatEthersSigner; + let disconnecter: HardhatEthersSigner; + + let depositContract: DepositContract__MockForStakingVault; + let permissionsImpl: Permissions__Harness; + let stakingVaultImpl: StakingVault; + let vaultHub: VaultHub__MockPermissions; + let beacon: UpgradeableBeacon; + let vaultFactory: VaultFactory__MockPermissions; + let stakingVault: StakingVault; + let permissions: Permissions__Harness; + + before(async () => { + [ + deployer, + defaultAdmin, + nodeOperator, + funder, + withdrawer, + minter, + burner, + rebalancer, + depositPauser, + depositResumer, + exitRequester, + disconnecter, + ] = await ethers.getSigners(); + + // 1. Deploy DepositContract + depositContract = await ethers.deployContract("DepositContract__MockForStakingVault"); + + // 2. Deploy VaultHub + vaultHub = await ethers.deployContract("VaultHub__MockPermissions"); + + // 3. Deploy StakingVault implementation + stakingVaultImpl = await ethers.deployContract("StakingVault", [vaultHub, depositContract]); + expect(await stakingVaultImpl.vaultHub()).to.equal(vaultHub); + expect(await stakingVaultImpl.depositContract()).to.equal(depositContract); + + // 4. Deploy Beacon and use StakingVault implementation as initial implementation + beacon = await ethers.deployContract("UpgradeableBeacon", [stakingVaultImpl, deployer]); + + // 5. Deploy Permissions implementation + permissionsImpl = await ethers.deployContract("Permissions__Harness"); + + // 6. Deploy VaultFactory and use Beacon and Permissions implementations + vaultFactory = await ethers.deployContract("VaultFactory__MockPermissions", [beacon, permissionsImpl]); + + // 7. Create StakingVault and Permissions proxies using VaultFactory + const vaultCreationTx = await vaultFactory.connect(deployer).createVaultWithPermissions( + { + defaultAdmin, + nodeOperator, + confirmLifetime: days(7n), + funder, + withdrawer, + minter, + burner, + rebalancer, + depositPauser, + depositResumer, + exitRequester, + disconnecter, + } as PermissionsConfigStruct, + "0x", + ); + const vaultCreationReceipt = await vaultCreationTx.wait(); + if (!vaultCreationReceipt) throw new Error("Vault creation failed"); + + // 8. Get StakingVault's proxy address from the event and wrap it in StakingVault interface + const vaultCreatedEvents = findEvents(vaultCreationReceipt, "VaultCreated"); + if (vaultCreatedEvents.length != 1) throw new Error("There should be exactly one VaultCreated event"); + const vaultCreatedEvent = vaultCreatedEvents[0]; + + stakingVault = StakingVault__factory.connect(vaultCreatedEvent.args.vault, defaultAdmin); + + // 9. Get Permissions' proxy address from the event and wrap it in Permissions interface + const permissionsCreatedEvents = findEvents(vaultCreationReceipt, "PermissionsCreated"); + if (permissionsCreatedEvents.length != 1) throw new Error("There should be exactly one PermissionsCreated event"); + const permissionsCreatedEvent = permissionsCreatedEvents[0]; + + permissions = Permissions__Harness__factory.connect(permissionsCreatedEvent.args.permissions, defaultAdmin); + + // 10. Check that StakingVault is initialized properly + expect(await stakingVault.owner()).to.equal(permissions); + expect(await stakingVault.nodeOperator()).to.equal(nodeOperator); + + // 11. Check events + expect(vaultCreatedEvent.args.owner).to.equal(permissions); + expect(permissionsCreatedEvent.args.admin).to.equal(defaultAdmin); + }); + + context("initial permissions", () => { + it("should have the correct roles", async () => { + await checkSoleMember(defaultAdmin, await permissions.DEFAULT_ADMIN_ROLE()); + await checkSoleMember(funder, await permissions.FUND_ROLE()); + await checkSoleMember(withdrawer, await permissions.WITHDRAW_ROLE()); + await checkSoleMember(minter, await permissions.MINT_ROLE()); + await checkSoleMember(burner, await permissions.BURN_ROLE()); + await checkSoleMember(rebalancer, await permissions.REBALANCE_ROLE()); + }); + }); + + async function checkSoleMember(account: HardhatEthersSigner, role: string) { + expect(await permissions.getRoleMemberCount(role)).to.equal(1); + expect(await permissions.getRoleMember(role, 0)).to.equal(account); + } +}); From 3e82d3a356838c881b3a1ed13608b22851d8e012 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 3 Feb 2025 18:00:00 +0500 Subject: [PATCH 17/65] feat: store expiry instead of cast timestamp --- .../0.8.25/utils/AccessControlMutuallyConfirmable.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol index f74534334..1ca3c8043 100644 --- a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol @@ -19,7 +19,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { * - role: role that confirmed the action * - timestamp: timestamp of the confirmation. */ - mapping(bytes32 callId => mapping(bytes32 role => uint256 timestamp)) public confirmations; + mapping(bytes32 callId => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; /** * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. @@ -66,7 +66,6 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { bytes32 callId = keccak256(msg.data); uint256 numberOfRoles = _roles.length; - uint256 confirmValidSince = block.timestamp - confirmLifetime; uint256 numberOfConfirms = 0; bool[] memory deferredConfirms = new bool[](numberOfRoles); bool isRoleMember = false; @@ -80,7 +79,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { deferredConfirms[i] = true; emit RoleMemberConfirmed(msg.sender, role, block.timestamp, msg.data); - } else if (confirmations[callId][role] >= confirmValidSince) { + } else if (confirmations[callId][role] >= block.timestamp) { numberOfConfirms++; } } @@ -97,7 +96,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { for (uint256 i = 0; i < numberOfRoles; ++i) { if (deferredConfirms[i]) { bytes32 role = _roles[i]; - confirmations[callId][role] = block.timestamp; + confirmations[callId][role] = block.timestamp + confirmLifetime; } } } From 62a8caa766a36141edb98d59b9dea693707a5592 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 3 Feb 2025 18:29:15 +0500 Subject: [PATCH 18/65] fix: use raw data for mapping key --- .../0.8.25/utils/AccessControlMutuallyConfirmable.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol index 1ca3c8043..7377f3746 100644 --- a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol @@ -19,7 +19,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { * - role: role that confirmed the action * - timestamp: timestamp of the confirmation. */ - mapping(bytes32 callId => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; + mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; /** * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. @@ -64,7 +64,6 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { modifier onlyMutuallyConfirmed(bytes32[] memory _roles) { if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); - bytes32 callId = keccak256(msg.data); uint256 numberOfRoles = _roles.length; uint256 numberOfConfirms = 0; bool[] memory deferredConfirms = new bool[](numberOfRoles); @@ -79,7 +78,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { deferredConfirms[i] = true; emit RoleMemberConfirmed(msg.sender, role, block.timestamp, msg.data); - } else if (confirmations[callId][role] >= block.timestamp) { + } else if (confirmations[msg.data][role] >= block.timestamp) { numberOfConfirms++; } } @@ -89,14 +88,14 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { if (numberOfConfirms == numberOfRoles) { for (uint256 i = 0; i < numberOfRoles; ++i) { bytes32 role = _roles[i]; - delete confirmations[callId][role]; + delete confirmations[msg.data][role]; } _; } else { for (uint256 i = 0; i < numberOfRoles; ++i) { if (deferredConfirms[i]) { bytes32 role = _roles[i]; - confirmations[callId][role] = block.timestamp + confirmLifetime; + confirmations[msg.data][role] = block.timestamp + confirmLifetime; } } } From b9dc9c3d8fd821b7df456a5a5f451680573d552a Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 3 Feb 2025 18:31:52 +0500 Subject: [PATCH 19/65] fix: revert if roles array empty --- contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol index 7377f3746..6f84110e5 100644 --- a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol @@ -62,6 +62,7 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { * */ modifier onlyMutuallyConfirmed(bytes32[] memory _roles) { + if (_roles.length == 0) revert ZeroConfirmingRoles(); if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); uint256 numberOfRoles = _roles.length; @@ -146,4 +147,9 @@ abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { * @dev Thrown when a caller without a required role attempts to confirm. */ error SenderNotMember(); + + /** + * @dev Thrown when the roles array is empty. + */ + error ZeroConfirmingRoles(); } From 0bfc88a1ecba28c60ebf0726c1e31ff7e11a373a Mon Sep 17 00:00:00 2001 From: DiRaiks Date: Mon, 10 Feb 2025 11:55:38 +0300 Subject: [PATCH 20/65] feat: override withdrawableEther in Delegation contract --- contracts/0.8.25/vaults/Dashboard.sol | 2 +- contracts/0.8.25/vaults/Delegation.sol | 12 ++++++ .../vaults/delegation/delegation.test.ts | 39 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index a00923153..cc64e48bf 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -192,7 +192,7 @@ contract Dashboard is Permissions { * @notice Returns the amount of ether that can be withdrawn from the staking vault. * @return The amount of ether that can be withdrawn. */ - function withdrawableEther() external view returns (uint256) { + function withdrawableEther() external view virtual returns (uint256) { return Math256.min(address(stakingVault()).balance, stakingVault().unlocked()); } diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index a725eaec3..32fbcb68e 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -4,6 +4,8 @@ // See contracts/COMPILERS.md pragma solidity 0.8.25; +import {Math256} from "contracts/common/lib/Math256.sol"; + import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {Dashboard} from "./Dashboard.sol"; @@ -151,6 +153,16 @@ contract Delegation is Dashboard { return reserved > valuation ? 0 : valuation - reserved; } + /** + * @notice Returns the amount of ether that can be withdrawn from the staking vault. + * @dev This is the amount of ether that is not locked in the StakingVault and not reserved for curator and node operator fees. + * @dev This method overrides the Dashboard's withdrawableEther() method + * @return The amount of ether that can be withdrawn. + */ + function withdrawableEther() external view override returns (uint256) { + return Math256.min(address(stakingVault()).balance, unreserved()); + } + /** * @notice Sets the vote lifetime. * Vote lifetime is a period during which the vote is counted. Once the period is over, diff --git a/test/0.8.25/vaults/delegation/delegation.test.ts b/test/0.8.25/vaults/delegation/delegation.test.ts index 7b4651a2b..b1e9383d6 100644 --- a/test/0.8.25/vaults/delegation/delegation.test.ts +++ b/test/0.8.25/vaults/delegation/delegation.test.ts @@ -362,6 +362,45 @@ describe("Delegation.sol", () => { }); }); + context("withdrawableEther", () => { + it("returns the correct amount", async () => { + const amount = ether("1"); + await delegation.connect(funder).fund({ value: amount }); + expect(await delegation.withdrawableEther()).to.equal(amount); + }); + + it("returns the correct amount when balance is less than unreserved", async () => { + const valuation = ether("3"); + const inOutDelta = 0n; + const locked = ether("2"); + + const amount = ether("1"); + await delegation.connect(funder).fund({ value: amount }); + await vault.connect(hubSigner).report(valuation, inOutDelta, locked); + + expect(await delegation.withdrawableEther()).to.equal(amount); + }); + + it("returns the correct amount when has fees", async () => { + const amount = ether("6"); + const valuation = ether("3"); + const inOutDelta = ether("1"); + const locked = ether("2"); + + const curatorFeeBP = 1000; // 10% + const operatorFeeBP = 1000; // 10% + await delegation.connect(vaultOwner).setCuratorFeeBP(curatorFeeBP); + await delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(operatorFeeBP); + + await delegation.connect(funder).fund({ value: amount }); + + await vault.connect(hubSigner).report(valuation, inOutDelta, locked); + const unreserved = await delegation.unreserved(); + + expect(await delegation.withdrawableEther()).to.equal(unreserved); + }); + }); + context("fund", () => { it("reverts if the caller is not a member of the staker role", async () => { await expect(delegation.connect(stranger).fund()).to.be.revertedWithCustomError( From e309222cdb6eddab1241f52e77da2a94af241779 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 11:34:45 +0000 Subject: [PATCH 21/65] test: fix new operators discrepancy --- lib/protocol/helpers/nor.ts | 14 ++++++--- lib/protocol/helpers/sdvt.ts | 14 ++++++--- lib/scratch.ts | 34 ++++++++++++++++++---- test/integration/accounting.integration.ts | 19 +++++++++--- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lib/protocol/helpers/nor.ts b/lib/protocol/helpers/nor.ts index c37cf5efa..a6ca84fa2 100644 --- a/lib/protocol/helpers/nor.ts +++ b/lib/protocol/helpers/nor.ts @@ -16,7 +16,7 @@ export const norEnsureOperators = async ( minOperatorsCount = MIN_OPS_COUNT, minOperatorKeysCount = MIN_OP_KEYS_COUNT, ) => { - await norEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); + const newOperatorsCount = await norEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); const { nor } = ctx.contracts; @@ -39,6 +39,8 @@ export const norEnsureOperators = async ( "Min operators count": minOperatorsCount, "Min keys count": minOperatorKeysCount, }); + + return newOperatorsCount; }; /** @@ -48,8 +50,8 @@ const norEnsureOperatorsHaveMinKeys = async ( ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT, minKeysCount = MIN_OP_KEYS_COUNT, -) => { - await norEnsureMinOperators(ctx, minOperatorsCount); +): Promise => { + const newOperatorsCount = await norEnsureMinOperators(ctx, minOperatorsCount); const { nor } = ctx.contracts; @@ -67,12 +69,14 @@ const norEnsureOperatorsHaveMinKeys = async ( expect(keysCountAfter).to.be.gte(minKeysCount); } + + return newOperatorsCount; }; /** * Fills the NOR with some operators in case there are not enough of them. */ -const norEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT) => { +const norEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT): Promise => { const { nor } = ctx.contracts; const before = await nor.getNodeOperatorsCount(); @@ -96,6 +100,8 @@ const norEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = M expect(after).to.equal(before + count); expect(after).to.be.gte(minOperatorsCount); + + return count; }; /** diff --git a/lib/protocol/helpers/sdvt.ts b/lib/protocol/helpers/sdvt.ts index 85b1981ac..593334f4a 100644 --- a/lib/protocol/helpers/sdvt.ts +++ b/lib/protocol/helpers/sdvt.ts @@ -21,7 +21,7 @@ export const sdvtEnsureOperators = async ( minOperatorsCount = MIN_OPS_COUNT, minOperatorKeysCount = MIN_OP_KEYS_COUNT, ) => { - await sdvtEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); + const newOperatorsCount = await sdvtEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); const { sdvt } = ctx.contracts; @@ -39,6 +39,8 @@ export const sdvtEnsureOperators = async ( expect(nodeOperatorAfter.totalVettedValidators).to.equal(nodeOperatorBefore.totalAddedValidators); } + + return newOperatorsCount; }; /** @@ -48,8 +50,8 @@ const sdvtEnsureOperatorsHaveMinKeys = async ( ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT, minKeysCount = MIN_OP_KEYS_COUNT, -) => { - await sdvtEnsureMinOperators(ctx, minOperatorsCount); +): Promise => { + const newOperatorsCount = await sdvtEnsureMinOperators(ctx, minOperatorsCount); const { sdvt } = ctx.contracts; @@ -74,12 +76,14 @@ const sdvtEnsureOperatorsHaveMinKeys = async ( "Min operators count": minOperatorsCount, "Min keys count": minKeysCount, }); + + return newOperatorsCount; }; /** * Fills the Simple DVT with some operators in case there are not enough of them. */ -const sdvtEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT) => { +const sdvtEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT): Promise => { const { sdvt } = ctx.contracts; const before = await sdvt.getNodeOperatorsCount(); @@ -110,6 +114,8 @@ const sdvtEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = "Min operators count": minOperatorsCount, "Operators count": after, }); + + return count; }; /** diff --git a/lib/scratch.ts b/lib/scratch.ts index e5d9751f6..4613f9d4a 100644 --- a/lib/scratch.ts +++ b/lib/scratch.ts @@ -6,6 +6,27 @@ import { ethers } from "hardhat"; import { log } from "./log"; import { resetStateFile } from "./state-file"; +class StepsFileNotFoundError extends Error { + constructor(filePath: string) { + super(`Steps file ${filePath} not found!`); + this.name = "StepsFileNotFoundError"; + } +} + +class MigrationFileNotFoundError extends Error { + constructor(filePath: string) { + super(`Migration file ${filePath} not found!`); + this.name = "MigrationFileNotFoundError"; + } +} + +class MigrationMainFunctionError extends Error { + constructor(filePath: string) { + super(`Migration file ${filePath} does not export a 'main' function!`); + this.name = "MigrationMainFunctionError"; + } +} + const deployedSteps: string[] = []; async function applySteps(steps: string[]) { @@ -35,8 +56,11 @@ export async function deployUpgrade(networkName: string): Promise { await applySteps(steps); } catch (error) { - log.error("Upgrade failed:", (error as Error).message); - log.warning("Upgrade steps not found, assuming the protocol is already deployed"); + if (error instanceof StepsFileNotFoundError) { + log.warning("Upgrade steps not found, assuming the protocol is already deployed"); + } else { + log.error("Upgrade failed:", (error as Error).message); + } } } @@ -55,7 +79,7 @@ type StepsFile = { export const loadSteps = (stepsFile: string): string[] => { const stepsPath = path.resolve(process.cwd(), `scripts/${stepsFile}`); if (!fs.existsSync(stepsPath)) { - throw new Error(`Steps file ${stepsPath} not found!`); + throw new StepsFileNotFoundError(stepsPath); } return (JSON.parse(fs.readFileSync(stepsPath, "utf8")) as StepsFile).steps; @@ -64,7 +88,7 @@ export const loadSteps = (stepsFile: string): string[] => { export const resolveMigrationFile = (step: string): string => { const migrationFile = path.resolve(process.cwd(), `scripts/${step}.ts`); if (!fs.existsSync(migrationFile)) { - throw new Error(`Migration file ${migrationFile} not found!`); + throw new MigrationFileNotFoundError(migrationFile); } return migrationFile; @@ -80,7 +104,7 @@ export async function applyMigrationScript(migrationFile: string): Promise const { main } = await import(fullPath); if (typeof main !== "function") { - throw new Error(`Migration file ${migrationFile} does not export a 'main' function!`); + throw new MigrationMainFunctionError(migrationFile); } try { diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index a64a82a50..e058d327e 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -49,17 +49,28 @@ describe("Accounting", () => { await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); - await norEnsureOperators(ctx, 3n, 5n); - await sdvtEnsureOperators(ctx, 3n, 5n); + const addedToNor = await norEnsureOperators(ctx, 3n, 5n); + const addedToSdvt = await sdvtEnsureOperators(ctx, 3n, 5n); // Deposit node operators const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); + const newOperators = addedToNor + addedToSdvt; + + if (newOperators) { + log.debug("New operators added", { + NOR: addedToNor, + SDVT: addedToSdvt, + }); + } else { + log("No new operators added"); + } + await report(ctx, { - clDiff: ether("32") * 6n, // 32 ETH * (3 + 3) validators - clAppearedValidators: 6n, + clDiff: ether("32") * newOperators, + clAppearedValidators: newOperators, excludeVaultsBalances: true, }); }); From d4bcbf367597d8cc344052f40a3345ae2fe8affd Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 12:32:52 +0000 Subject: [PATCH 22/65] feat: move common tests setup to provision part --- lib/protocol/helpers/index.ts | 2 +- lib/protocol/helpers/staking.ts | 47 ++++++++++++++++++- lib/protocol/helpers/withdrawal.ts | 16 +++---- lib/protocol/provision.ts | 16 ++++++- test/integration/accounting.integration.ts | 46 +----------------- test/integration/burn-shares.integration.ts | 6 +-- .../protocol-happy-path.integration.ts | 14 ++++-- .../integration/second-opinion.integration.ts | 15 +----- 8 files changed, 85 insertions(+), 77 deletions(-) diff --git a/lib/protocol/helpers/index.ts b/lib/protocol/helpers/index.ts index 66c854bbb..00aa6633c 100644 --- a/lib/protocol/helpers/index.ts +++ b/lib/protocol/helpers/index.ts @@ -1,4 +1,4 @@ -export { unpauseStaking, ensureStakeLimit } from "./staking"; +export { unpauseStaking, ensureStakeLimit, depositAndReportValidators } from "./staking"; export { unpauseWithdrawalQueue, finalizeWithdrawalQueue } from "./withdrawal"; diff --git a/lib/protocol/helpers/staking.ts b/lib/protocol/helpers/staking.ts index e482f3b54..82a7039e6 100644 --- a/lib/protocol/helpers/staking.ts +++ b/lib/protocol/helpers/staking.ts @@ -1,7 +1,18 @@ -import { ether, log, trace } from "lib"; +import { ZeroAddress } from "ethers"; + +import { certainAddress, ether, impersonate, log, trace } from "lib"; + +import { ZERO_HASH } from "test/deploy"; import { ProtocolContext } from "../types"; +import { report } from "./accounting"; + +const AMOUNT = ether("100"); +const MAX_DEPOSIT = 150n; +const CURATED_MODULE_ID = 1n; +const SIMPLE_DVT_MODULE_ID = 2n; + /** * Unpauses the staking contract. */ @@ -35,3 +46,37 @@ export const ensureStakeLimit = async (ctx: ProtocolContext) => { log.success("Staking limit set"); } }; + +export const depositAndReportValidators = async (ctx: ProtocolContext) => { + const { lido, depositSecurityModule } = ctx.contracts; + const ethHolder = await impersonate(certainAddress("provision:eht:whale"), ether("100000")); + + await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); + + // Deposit node operators + const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); + await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); + await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); + + const before = await lido.getBeaconStat(); + + log.debug("Validators on beacon chain before provisioning", { + depositedValidators: before.depositedValidators, + beaconValidators: before.beaconValidators, + beaconBalance: before.beaconBalance, + }); + + // Add new validators to beacon chain + await report(ctx, { + clDiff: ether("32") * before.depositedValidators, + clAppearedValidators: before.depositedValidators, + }); + + const after = await lido.getBeaconStat(); + + log.debug("Validators on beacon chain after provisioning", { + depositedValidators: after.depositedValidators, + beaconValidators: after.beaconValidators, + beaconBalance: after.beaconBalance, + }); +}; diff --git a/lib/protocol/helpers/withdrawal.ts b/lib/protocol/helpers/withdrawal.ts index 43315298b..329ed913d 100644 --- a/lib/protocol/helpers/withdrawal.ts +++ b/lib/protocol/helpers/withdrawal.ts @@ -1,9 +1,7 @@ import { expect } from "chai"; import { ZeroAddress } from "ethers"; -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { ether, log, trace, updateBalance } from "lib"; +import { certainAddress, ether, impersonate, log, trace } from "lib"; import { ProtocolContext } from "../types"; @@ -32,16 +30,13 @@ export const unpauseWithdrawalQueue = async (ctx: ProtocolContext) => { } }; -export const finalizeWithdrawalQueue = async ( - ctx: ProtocolContext, - stEthHolder: HardhatEthersSigner, - ethHolder: HardhatEthersSigner, -) => { +export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { const { lido, withdrawalQueue } = ctx.contracts; - await updateBalance(ethHolder.address, ether("1000000")); - await updateBalance(stEthHolder.address, ether("1000000")); + const seed = Math.random() * 1000000; + const ethHolder = await impersonate(certainAddress(`withdrawalQueue:eth:whale:${seed}`), ether("100000")); + const stEthHolder = await impersonate(certainAddress(`withdrawalQueue:stEth:whale:${seed}`), ether("100000")); const stEthHolderAmount = ether("10000"); // Here sendTransaction is used to validate native way of submitting ETH for stETH @@ -49,6 +44,7 @@ export const finalizeWithdrawalQueue = async ( await trace("stEthHolder.sendTransaction", tx); const stEthHolderBalance = await lido.balanceOf(stEthHolder.address); + expect(stEthHolderBalance).to.approximately(stEthHolderAmount, 10n, "stETH balance increased"); let lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 29a0d7681..76ce732b7 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -1,7 +1,11 @@ +import { log } from "lib"; + import { + depositAndReportValidators, ensureHashConsensusInitialEpoch, ensureOracleCommitteeMembers, ensureStakeLimit, + finalizeWithdrawalQueue, norEnsureOperators, sdvtEnsureOperators, unpauseStaking, @@ -9,20 +13,30 @@ import { } from "./helpers"; import { ProtocolContext } from "./types"; +let alreadyProvisioned = false; + /** * In order to make the protocol fully operational from scratch deploy, the additional steps are required: */ export const provision = async (ctx: ProtocolContext) => { + if (alreadyProvisioned) { + log.success("Already provisioned"); + return; + } + await ensureHashConsensusInitialEpoch(ctx); await ensureOracleCommitteeMembers(ctx, 5n); await unpauseStaking(ctx); - await unpauseWithdrawalQueue(ctx); await norEnsureOperators(ctx, 3n, 5n); await sdvtEnsureOperators(ctx, 3n, 5n); + await depositAndReportValidators(ctx); + await finalizeWithdrawalQueue(ctx); await ensureStakeLimit(ctx); + + alreadyProvisioned = true; }; diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index e058d327e..916314310 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -7,13 +7,7 @@ import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { ether, impersonate, log, ONE_GWEI, trace, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { - finalizeWithdrawalQueue, - getReportTimeElapsed, - norEnsureOperators, - report, - sdvtEnsureOperators, -} from "lib/protocol/helpers"; +import { getReportTimeElapsed, report } from "lib/protocol/helpers"; import { Snapshot } from "test/suite"; @@ -22,18 +16,11 @@ const LIMITER_PRECISION_BASE = BigInt(10 ** 9); const SHARE_RATE_PRECISION = BigInt(10 ** 27); const ONE_DAY = 86400n; const MAX_BASIS_POINTS = 10000n; -const AMOUNT = ether("100"); -const MAX_DEPOSIT = 150n; -const CURATED_MODULE_ID = 1n; -const SIMPLE_DVT_MODULE_ID = 2n; - -const ZERO_HASH = new Uint8Array(32).fill(0); describe("Accounting", () => { let ctx: ProtocolContext; let ethHolder: HardhatEthersSigner; - let stEthHolder: HardhatEthersSigner; let snapshot: string; let originalState: string; @@ -41,38 +28,9 @@ describe("Accounting", () => { before(async () => { ctx = await getProtocolContext(); - [stEthHolder, ethHolder] = await ethers.getSigners(); + [ethHolder] = await ethers.getSigners(); snapshot = await Snapshot.take(); - - const { lido, depositSecurityModule } = ctx.contracts; - - await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); - - const addedToNor = await norEnsureOperators(ctx, 3n, 5n); - const addedToSdvt = await sdvtEnsureOperators(ctx, 3n, 5n); - - // Deposit node operators - const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); - await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); - await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); - - const newOperators = addedToNor + addedToSdvt; - - if (newOperators) { - log.debug("New operators added", { - NOR: addedToNor, - SDVT: addedToSdvt, - }); - } else { - log("No new operators added"); - } - - await report(ctx, { - clDiff: ether("32") * newOperators, - clAppearedValidators: newOperators, - excludeVaultsBalances: true, - }); }); beforeEach(async () => (originalState = await Snapshot.take())); diff --git a/test/integration/burn-shares.integration.ts b/test/integration/burn-shares.integration.ts index 53dfa1ea3..0effa7dc5 100644 --- a/test/integration/burn-shares.integration.ts +++ b/test/integration/burn-shares.integration.ts @@ -14,8 +14,6 @@ describe("Burn Shares", () => { let ctx: ProtocolContext; let snapshot: string; - let ethHolder: HardhatEthersSigner; - let stEthHolder: HardhatEthersSigner; let stranger: HardhatEthersSigner; const amount = ether("1"); @@ -26,7 +24,7 @@ describe("Burn Shares", () => { before(async () => { ctx = await getProtocolContext(); - [stEthHolder, ethHolder, stranger] = await ethers.getSigners(); + [stranger] = await ethers.getSigners(); snapshot = await Snapshot.take(); }); @@ -38,7 +36,7 @@ describe("Burn Shares", () => { it("Should finalize withdrawal queue", async () => { const { withdrawalQueue } = ctx.contracts; - await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); + await finalizeWithdrawalQueue(ctx); const lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); const lastRequestId = await withdrawalQueue.getLastRequestId(); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 161c40b6a..590f38527 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -25,7 +25,6 @@ describe("Protocol Happy Path", () => { let ctx: ProtocolContext; let snapshot: string; - let ethHolder: HardhatEthersSigner; let stEthHolder: HardhatEthersSigner; let stranger: HardhatEthersSigner; @@ -35,7 +34,7 @@ describe("Protocol Happy Path", () => { before(async () => { ctx = await getProtocolContext(); - [stEthHolder, ethHolder, stranger] = await ethers.getSigners(); + [stEthHolder, stranger] = await ethers.getSigners(); snapshot = await Snapshot.take(); }); @@ -55,7 +54,16 @@ describe("Protocol Happy Path", () => { it("Should finalize withdrawal queue", async () => { const { lido, withdrawalQueue } = ctx.contracts; - await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); + const stEthHolderAmount = ether("10000"); + + // Deposit some eth + const tx = await lido.submit(ZeroAddress, { value: stEthHolderAmount }); + await trace("lido.submit", tx); + + const stEthHolderBalance = await lido.balanceOf(stEthHolder.address); + expect(stEthHolderBalance).to.approximately(stEthHolderAmount, 10n, "stETH balance increased"); + + await finalizeWithdrawalQueue(ctx); const lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); const lastRequestId = await withdrawalQueue.getLastRequestId(); diff --git a/test/integration/second-opinion.integration.ts b/test/integration/second-opinion.integration.ts index 75a7c0242..1e240fde0 100644 --- a/test/integration/second-opinion.integration.ts +++ b/test/integration/second-opinion.integration.ts @@ -1,13 +1,11 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - import { SecondOpinionOracle__Mock } from "typechain-types"; import { ether, impersonate, log, ONE_GWEI } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { finalizeWithdrawalQueue, norEnsureOperators, report, sdvtEnsureOperators } from "lib/protocol/helpers"; +import { report } from "lib/protocol/helpers"; import { bailOnFailure, Snapshot } from "test/suite"; @@ -26,9 +24,6 @@ function getDiffAmount(totalSupply: bigint): bigint { describe("Second opinion", () => { let ctx: ProtocolContext; - let ethHolder: HardhatEthersSigner; - let stEthHolder: HardhatEthersSigner; - let snapshot: string; let originalState: string; @@ -38,17 +33,10 @@ describe("Second opinion", () => { before(async () => { ctx = await getProtocolContext(); - [stEthHolder, ethHolder] = await ethers.getSigners(); - snapshot = await Snapshot.take(); const { lido, depositSecurityModule, oracleReportSanityChecker } = ctx.contracts; - await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); - - await norEnsureOperators(ctx, 3n, 5n); - await sdvtEnsureOperators(ctx, 3n, 5n); - const { chainId } = await ethers.provider.getNetwork(); // Sepolia-specific initialization if (chainId === 11155111n) { @@ -63,6 +51,7 @@ describe("Second opinion", () => { const adapterAddr = await ctx.contracts.stakingRouter.DEPOSIT_CONTRACT(); await bepoliaToken.connect(bepiloaSigner).transfer(adapterAddr, BEPOLIA_TO_TRANSFER); } + const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); From eef95235b2981a152b5078e52fb973ab358fa0aa Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 12:48:10 +0000 Subject: [PATCH 23/65] feat: simplify provisioning --- .github/workflows/analyse.yml | 2 +- lib/protocol/helpers/nor.ts | 10 ++++-- lib/protocol/helpers/sdvt.ts | 9 +++-- lib/protocol/helpers/staking.ts | 36 +++++++++---------- lib/protocol/helpers/withdrawal.ts | 11 ++---- lib/protocol/provision.ts | 3 +- .../protocol-happy-path.integration.ts | 10 +++--- 7 files changed, 38 insertions(+), 43 deletions(-) diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml index 016a2b748..3a4a625cb 100644 --- a/.github/workflows/analyse.yml +++ b/.github/workflows/analyse.yml @@ -40,7 +40,7 @@ jobs: - name: Run slither run: > - poetry run slither . --no-fail-pedantic --sarif results.sarif + poetry run slither . --no-fail-pedantic --sarif results.sarif - name: Check results.sarif presence id: results diff --git a/lib/protocol/helpers/nor.ts b/lib/protocol/helpers/nor.ts index a6ca84fa2..c5185d82a 100644 --- a/lib/protocol/helpers/nor.ts +++ b/lib/protocol/helpers/nor.ts @@ -5,6 +5,9 @@ import { certainAddress, log, trace } from "lib"; import { ProtocolContext, StakingModuleName } from "../types"; +import { depositAndReportValidators } from "./staking"; + +const NOR_MODULE_ID = 1n; const MIN_OPS_COUNT = 3n; const MIN_OP_KEYS_COUNT = 10n; @@ -16,10 +19,9 @@ export const norEnsureOperators = async ( minOperatorsCount = MIN_OPS_COUNT, minOperatorKeysCount = MIN_OP_KEYS_COUNT, ) => { - const newOperatorsCount = await norEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); - const { nor } = ctx.contracts; + const newOperatorsCount = await norEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); for (let operatorId = 0n; operatorId < minOperatorsCount; operatorId++) { const nodeOperatorBefore = await nor.getNodeOperator(operatorId, false); @@ -40,7 +42,9 @@ export const norEnsureOperators = async ( "Min keys count": minOperatorKeysCount, }); - return newOperatorsCount; + if (newOperatorsCount > 0) { + await depositAndReportValidators(ctx, NOR_MODULE_ID, newOperatorsCount); + } }; /** diff --git a/lib/protocol/helpers/sdvt.ts b/lib/protocol/helpers/sdvt.ts index 593334f4a..cc722e580 100644 --- a/lib/protocol/helpers/sdvt.ts +++ b/lib/protocol/helpers/sdvt.ts @@ -1,13 +1,14 @@ import { expect } from "chai"; import { randomBytes } from "ethers"; -import { impersonate, log, streccak, trace } from "lib"; +import { ether, impersonate, log, streccak, trace } from "lib"; -import { ether } from "../../units"; import { ProtocolContext } from "../types"; import { getOperatorManagerAddress, getOperatorName, getOperatorRewardAddress } from "./nor"; +import { depositAndReportValidators } from "./staking"; +const SDVT_MODULE_ID = 2n; const MIN_OPS_COUNT = 3n; const MIN_OP_KEYS_COUNT = 10n; @@ -40,7 +41,9 @@ export const sdvtEnsureOperators = async ( expect(nodeOperatorAfter.totalVettedValidators).to.equal(nodeOperatorBefore.totalAddedValidators); } - return newOperatorsCount; + if (newOperatorsCount > 0) { + await depositAndReportValidators(ctx, SDVT_MODULE_ID, newOperatorsCount); + } }; /** diff --git a/lib/protocol/helpers/staking.ts b/lib/protocol/helpers/staking.ts index 82a7039e6..39b8b9884 100644 --- a/lib/protocol/helpers/staking.ts +++ b/lib/protocol/helpers/staking.ts @@ -8,11 +8,6 @@ import { ProtocolContext } from "../types"; import { report } from "./accounting"; -const AMOUNT = ether("100"); -const MAX_DEPOSIT = 150n; -const CURATED_MODULE_ID = 1n; -const SIMPLE_DVT_MODULE_ID = 2n; - /** * Unpauses the staking contract. */ @@ -47,36 +42,37 @@ export const ensureStakeLimit = async (ctx: ProtocolContext) => { } }; -export const depositAndReportValidators = async (ctx: ProtocolContext) => { +export const depositAndReportValidators = async (ctx: ProtocolContext, moduleId: bigint, depositsCount: bigint) => { const { lido, depositSecurityModule } = ctx.contracts; - const ethHolder = await impersonate(certainAddress("provision:eht:whale"), ether("100000")); + const ethHolder = await impersonate(certainAddress("provision:eth:whale"), ether("100000")); await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); - // Deposit node operators - const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); - await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); - await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); + // Deposit validators + const dsmSigner = await impersonate(depositSecurityModule.address, ether("100000")); + await lido.connect(dsmSigner).deposit(depositsCount, moduleId, ZERO_HASH); const before = await lido.getBeaconStat(); log.debug("Validators on beacon chain before provisioning", { - depositedValidators: before.depositedValidators, - beaconValidators: before.beaconValidators, - beaconBalance: before.beaconBalance, + "Module ID to deposit": moduleId, + "Deposited": before.depositedValidators, + "Total": before.beaconValidators, + "Balance": before.beaconBalance, }); // Add new validators to beacon chain await report(ctx, { - clDiff: ether("32") * before.depositedValidators, - clAppearedValidators: before.depositedValidators, + clDiff: ether("32") * depositsCount, + clAppearedValidators: depositsCount, }); const after = await lido.getBeaconStat(); - log.debug("Validators on beacon chain after provisioning", { - depositedValidators: after.depositedValidators, - beaconValidators: after.beaconValidators, - beaconBalance: after.beaconBalance, + log.debug("Validators on beacon chain after depositing", { + "Module ID deposited": moduleId, + "Deposited": after.depositedValidators, + "Total": after.beaconValidators, + "Balance": after.beaconBalance, }); }; diff --git a/lib/protocol/helpers/withdrawal.ts b/lib/protocol/helpers/withdrawal.ts index 329ed913d..3066a8a73 100644 --- a/lib/protocol/helpers/withdrawal.ts +++ b/lib/protocol/helpers/withdrawal.ts @@ -1,4 +1,3 @@ -import { expect } from "chai"; import { ZeroAddress } from "ethers"; import { certainAddress, ether, impersonate, log, trace } from "lib"; @@ -33,20 +32,14 @@ export const unpauseWithdrawalQueue = async (ctx: ProtocolContext) => { export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { const { lido, withdrawalQueue } = ctx.contracts; - const seed = Math.random() * 1000000; - - const ethHolder = await impersonate(certainAddress(`withdrawalQueue:eth:whale:${seed}`), ether("100000")); - const stEthHolder = await impersonate(certainAddress(`withdrawalQueue:stEth:whale:${seed}`), ether("100000")); + const ethHolder = await impersonate(certainAddress("withdrawalQueue:eth:whale"), ether("100000")); + const stEthHolder = await impersonate(certainAddress("withdrawalQueue:stEth:whale"), ether("100000")); const stEthHolderAmount = ether("10000"); // Here sendTransaction is used to validate native way of submitting ETH for stETH const tx = await stEthHolder.sendTransaction({ to: lido.address, value: stEthHolderAmount }); await trace("stEthHolder.sendTransaction", tx); - const stEthHolderBalance = await lido.balanceOf(stEthHolder.address); - - expect(stEthHolderBalance).to.approximately(stEthHolderAmount, 10n, "stETH balance increased"); - let lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); let lastRequestId = await withdrawalQueue.getLastRequestId(); diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 76ce732b7..e22e1ca75 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -1,7 +1,6 @@ import { log } from "lib"; import { - depositAndReportValidators, ensureHashConsensusInitialEpoch, ensureOracleCommitteeMembers, ensureStakeLimit, @@ -33,7 +32,7 @@ export const provision = async (ctx: ProtocolContext) => { await norEnsureOperators(ctx, 3n, 5n); await sdvtEnsureOperators(ctx, 3n, 5n); - await depositAndReportValidators(ctx); + await finalizeWithdrawalQueue(ctx); await ensureStakeLimit(ctx); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 590f38527..1a5ec6e30 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -30,6 +30,7 @@ describe("Protocol Happy Path", () => { let uncountedStETHShares: bigint; let amountWithRewards: bigint; + let depositCount: bigint; before(async () => { ctx = await getProtocolContext(); @@ -220,7 +221,7 @@ describe("Protocol Happy Path", () => { const dsmSigner = await impersonate(depositSecurityModule.address, ether("100")); const stakingModules = await stakingRouter.getStakingModules(); - let depositCount = 0n; + depositCount = 0n; let expectedBufferedEtherAfterDeposit = bufferedEtherBeforeDeposit; for (const module of stakingModules) { const depositTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, module.id, ZERO_HASH); @@ -294,11 +295,10 @@ describe("Protocol Happy Path", () => { const treasuryBalanceBeforeRebase = await lido.sharesOf(treasuryAddress); - // Stranger deposited 100 ETH, enough to deposit 3 validators, need to reflect this in the report - // 0.01 ETH is added to the clDiff to simulate some rewards + // 0.001 – to simulate rewards const reportData: Partial = { - clDiff: ether("96.01"), - clAppearedValidators: 3n, + clDiff: ether("32") * depositCount + ether("0.001"), + clAppearedValidators: depositCount, }; const { reportTx, extraDataTx } = (await report(ctx, reportData)) as { From 46e4593b1bb0bf388dc589a0e76142af5c6ae8a4 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 13:18:52 +0000 Subject: [PATCH 24/65] fix: tests --- test/integration/protocol-happy-path.integration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 1a5ec6e30..3e034d702 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -55,10 +55,10 @@ describe("Protocol Happy Path", () => { it("Should finalize withdrawal queue", async () => { const { lido, withdrawalQueue } = ctx.contracts; - const stEthHolderAmount = ether("10000"); + const stEthHolderAmount = ether("1000"); // Deposit some eth - const tx = await lido.submit(ZeroAddress, { value: stEthHolderAmount }); + const tx = await lido.connect(stEthHolder).submit(ZeroAddress, { value: stEthHolderAmount }); await trace("lido.submit", tx); const stEthHolderBalance = await lido.balanceOf(stEthHolder.address); From f492007e48a3e598eaf1d16725c3d7cf1c2c5871 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 13:26:18 +0000 Subject: [PATCH 25/65] feat: add mainnet integration tests to schedule --- .github/workflows/tests-integration-mainnet.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-integration-mainnet.yml b/.github/workflows/tests-integration-mainnet.yml index dfbb6d082..1bad570d1 100644 --- a/.github/workflows/tests-integration-mainnet.yml +++ b/.github/workflows/tests-integration-mainnet.yml @@ -1,6 +1,9 @@ name: Integration Tests -on: [push] +on: + push: + schedule: + - cron: "0 10 */2 * *" jobs: test_hardhat_integration_fork: From 7dd0aa477c09535db2d292cf801036645e717118 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 15:11:15 +0100 Subject: [PATCH 26/65] chore: update CODEOWNERS --- .github/CODEOWNERS | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8f8d20609..6350fb009 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,3 @@ # CODEOWNERS: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners - -# Any PR to `master` branch with changes to production contracts notifies the protocol team -/contracts/ @lidofinance/lido-eth-protocol - -# Any PR to `master` branch with changes to GitHub workflows notifies the workflow review team -/.github/workflows/ @lidofinance/review-gh-workflows +* @lidofinance/lido-eth-protocol +.github @lidofinance/review-gh-workflows From fce5c97a8165c473ed3bc347f7808465342b0526 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Tue, 11 Feb 2025 13:59:11 +0500 Subject: [PATCH 27/65] fix: rename access control confirmable --- ...olMutuallyConfirmable.sol => AccessControlConfirmable.sol} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename contracts/0.8.25/utils/{AccessControlMutuallyConfirmable.sol => AccessControlConfirmable.sol} (98%) diff --git a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol similarity index 98% rename from contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol rename to contracts/0.8.25/utils/AccessControlConfirmable.sol index 6f84110e5..328aede81 100644 --- a/contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -7,12 +7,12 @@ pragma solidity 0.8.25; import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol"; /** - * @title AccessControlMutuallyConfirmable + * @title AccessControlConfirmable * @author Lido * @notice An extension of AccessControlEnumerable that allows exectuing functions by mutual confirmation. * @dev This contract extends AccessControlEnumerable and adds a confirmation mechanism in the form of a modifier. */ -abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { +abstract contract AccessControlConfirmable is AccessControlEnumerable { /** * @notice Tracks confirmations * - callId: unique identifier for the call, derived as `keccak256(msg.data)` From 667791a9ae794d79f9ce688b7ff7cc47680373ad Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 11 Feb 2025 11:26:17 +0000 Subject: [PATCH 28/65] fix: negative rebase --- package.json | 6 +-- ...base.ts => negative-rebase.integration.ts} | 42 ++++--------------- 2 files changed, 11 insertions(+), 37 deletions(-) rename test/integration/{negative-rebase.ts => negative-rebase.integration.ts} (75%) diff --git a/package.json b/package.json index 3e42dd7e1..d802c899b 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "test:integration": "hardhat test test/integration/**/*.ts", "test:integration:trace": "hardhat test test/integration/**/*.ts --trace --disabletracer", "test:integration:fulltrace": "hardhat test test/integration/**/*.ts --fulltrace --disabletracer", - "test:integration:scratch": "INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts", - "test:integration:scratch:trace": "INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --trace --disabletracer", - "test:integration:scratch:fulltrace": "INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --fulltrace --disabletracer", + "test:integration:scratch": "HARDHAT_FORKING_URL= INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts", + "test:integration:scratch:trace": "HARDHAT_FORKING_URL= INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --trace --disabletracer", + "test:integration:scratch:fulltrace": "HARDHAT_FORKING_URL= INTEGRATION_WITH_CSM=off INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --fulltrace --disabletracer", "test:integration:fork:local": "hardhat test test/integration/**/*.ts --network local", "test:integration:fork:mainnet": "hardhat test test/integration/**/*.ts --network mainnet-fork", "test:integration:fork:mainnet:custom": "hardhat test --network mainnet-fork", diff --git a/test/integration/negative-rebase.ts b/test/integration/negative-rebase.integration.ts similarity index 75% rename from test/integration/negative-rebase.ts rename to test/integration/negative-rebase.integration.ts index 367485ef2..e28e4062b 100644 --- a/test/integration/negative-rebase.ts +++ b/test/integration/negative-rebase.integration.ts @@ -4,11 +4,9 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { ether, impersonate } from "lib"; +import { ether } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { report } from "lib/protocol/helpers/accounting"; -import { norEnsureOperators } from "lib/protocol/helpers/nor"; -import { finalizeWithdrawalQueue } from "lib/protocol/helpers/withdrawal"; +import { report } from "lib/protocol/helpers"; import { Snapshot } from "test/suite"; @@ -16,16 +14,17 @@ describe("Negative rebase", () => { let ctx: ProtocolContext; let beforeSnapshot: string; let beforeEachSnapshot: string; - let ethHolder, stEthHolder: HardhatEthersSigner; + let ethHolder: HardhatEthersSigner; before(async () => { beforeSnapshot = await Snapshot.take(); ctx = await getProtocolContext(); - [ethHolder, stEthHolder] = await ethers.getSigners(); + [ethHolder] = await ethers.getSigners(); await setBalance(ethHolder.address, ether("1000000")); const network = await ethers.provider.getNetwork(); - console.log("network", network.name); + + // In case of sepolia network, transfer some BEPOLIA tokens to the adapter contract if (network.name == "sepolia" || network.name == "sepolia-fork") { const sepoliaDepositContractAddress = "0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D"; const bepoliaWhaleHolder = "0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134"; @@ -37,35 +36,11 @@ describe("Negative rebase", () => { const adapterAddr = await ctx.contracts.stakingRouter.DEPOSIT_CONTRACT(); await bepoliaToken.connect(bepiloaSigner).transfer(adapterAddr, BEPOLIA_TO_TRANSFER); } - const beaconStat = await ctx.contracts.lido.getBeaconStat(); - if (beaconStat.beaconValidators == 0n) { - const MAX_DEPOSIT = 150n; - const CURATED_MODULE_ID = 1n; - const ZERO_HASH = new Uint8Array(32).fill(0); - const { lido, depositSecurityModule } = ctx.contracts; - - await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); - - await norEnsureOperators(ctx, 3n, 5n); - - const dsmSigner = await impersonate(depositSecurityModule.address, ether("100")); - await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); - - await report(ctx, { - clDiff: ether("32") * 3n, - clAppearedValidators: 3n, - excludeVaultsBalances: true, - }); - } }); - after(async () => { - await Snapshot.restore(beforeSnapshot); - }); + after(async () => await Snapshot.restore(beforeSnapshot)); - beforeEach(async () => { - beforeEachSnapshot = await Snapshot.take(); - }); + beforeEach(async () => (beforeEachSnapshot = await Snapshot.take())); afterEach(async () => await Snapshot.restore(beforeEachSnapshot)); @@ -104,7 +79,6 @@ describe("Negative rebase", () => { expect(lastReportData.totalExitedValidators).to.be.equal(lastExitedTotal + 2n); expect(beforeLastReportData.totalExitedValidators).to.be.equal(lastExitedTotal); - }); it("Should store correctly many negative rebases", async () => { From 7e1f108567e532345c01e0a2d81da28e6ffacbf6 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Tue, 11 Feb 2025 19:10:49 +0500 Subject: [PATCH 29/65] fix: tailing renaming --- contracts/0.8.25/utils/AccessControlConfirmable.sol | 2 +- contracts/0.8.25/vaults/Delegation.sol | 4 ++-- contracts/0.8.25/vaults/Permissions.sol | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index 328aede81..ea1036f55 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -61,7 +61,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * @notice The order of confirmations does not matter * */ - modifier onlyMutuallyConfirmed(bytes32[] memory _roles) { + modifier onlyConfirmed(bytes32[] memory _roles) { if (_roles.length == 0) revert ZeroConfirmingRoles(); if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index feafb9cf9..abe159ec4 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -157,7 +157,7 @@ contract Delegation is Dashboard { * the confirm is considered expired, no longer counts and must be recasted. * @param _newConfirmLifetime The new confirm lifetime in seconds. */ - function setConfirmLifetime(uint256 _newConfirmLifetime) external onlyMutuallyConfirmed(_confirmingRoles()) { + function setConfirmLifetime(uint256 _newConfirmLifetime) external onlyConfirmed(_confirmingRoles()) { _setConfirmLifetime(_newConfirmLifetime); } @@ -185,7 +185,7 @@ contract Delegation is Dashboard { * which is why the deciding confirm must make sure that `nodeOperatorUnclaimedFee()` is 0 before calling this function. * @param _newNodeOperatorFeeBP The new node operator fee in basis points. */ - function setNodeOperatorFeeBP(uint256 _newNodeOperatorFeeBP) external onlyMutuallyConfirmed(_confirmingRoles()) { + function setNodeOperatorFeeBP(uint256 _newNodeOperatorFeeBP) external onlyConfirmed(_confirmingRoles()) { if (_newNodeOperatorFeeBP + curatorFeeBP > MAX_FEE_BP) revert CombinedFeesExceed100Percent(); if (nodeOperatorUnclaimedFee() > 0) revert NodeOperatorFeeUnclaimed(); uint256 oldNodeOperatorFeeBP = nodeOperatorFeeBP; diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 419e63428..d4f9e1328 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.25; import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol"; -import {AccessControlMutuallyConfirmable} from "contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol"; +import {AccessControlConfirmable} from "contracts/0.8.25/utils/AccessControlConfirmable.sol"; import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; @@ -16,7 +16,7 @@ import {VaultHub} from "./VaultHub.sol"; * @author Lido * @notice Provides granular permissions for StakingVault operations. */ -abstract contract Permissions is AccessControlMutuallyConfirmable { +abstract contract Permissions is AccessControlConfirmable { /** * @notice Struct containing an account and a role for granting/revoking roles. */ @@ -179,7 +179,7 @@ abstract contract Permissions is AccessControlMutuallyConfirmable { vaultHub.voluntaryDisconnect(address(stakingVault())); } - function _transferStakingVaultOwnership(address _newOwner) internal onlyMutuallyConfirmed(_confirmingRoles()) { + function _transferStakingVaultOwnership(address _newOwner) internal onlyConfirmed(_confirmingRoles()) { OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner); } From a4bd6b8af45e77e0b6e3737f544b1605286e53dd Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 11 Feb 2025 18:19:39 +0000 Subject: [PATCH 30/65] feat: hardhat based logs --- lib/deploy.ts | 11 +- lib/index.ts | 2 - lib/log.ts | 23 --- lib/protocol/helpers/accounting.ts | 19 +- lib/protocol/helpers/nor.ts | 13 +- lib/protocol/helpers/sdvt.ts | 16 +- lib/protocol/helpers/staking.ts | 8 +- lib/protocol/helpers/withdrawal.ts | 18 +- lib/transaction.ts | 42 ---- lib/type.ts | 15 -- tasks/index.ts | 7 +- tasks/logger.ts | 181 ++++++++++++++++++ test/0.8.9/lidoLocator.test.ts | 3 +- test/integration/accounting.integration.ts | 23 +-- test/integration/burn-shares.integration.ts | 11 +- .../protocol-happy-path.integration.ts | 29 +-- 16 files changed, 231 insertions(+), 190 deletions(-) delete mode 100644 lib/transaction.ts delete mode 100644 lib/type.ts create mode 100644 tasks/logger.ts diff --git a/lib/deploy.ts b/lib/deploy.ts index 1b9a1626a..24aa589c6 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -5,7 +5,7 @@ import { FactoryOptions } from "hardhat/types"; import { LidoLocator } from "typechain-types"; import { addContractHelperFields, DeployedContract, getContractPath, loadContract, LoadedContract } from "lib/contract"; -import { ConvertibleToString, cy, gr, log, yl } from "lib/log"; +import { ConvertibleToString, cy, log, yl } from "lib/log"; import { incrementGasUsed, Sk, updateObjectInState } from "lib/state-file"; const GAS_PRIORITY_FEE = process.env.GAS_PRIORITY_FEE || null; @@ -36,15 +36,11 @@ export async function makeTx( log.withArguments(`Call: ${yl(contract.name)}[${cy(contract.address)}].${yl(funcName)}`, args); const tx = await contract.getFunction(funcName)(...args, txParams); - log(` Transaction: ${tx.hash} (nonce ${yl(tx.nonce)})...`); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; incrementGasUsed(gasUsed, withStateFile); - log(` Executed (gas used: ${yl(gasUsed)})`); - log.emptyLine(); - return receipt; } @@ -80,8 +76,6 @@ async function deployContractType2( throw new Error(`Failed to send the deployment transaction for ${artifactName}`); } - log(` Transaction: ${tx.hash} (nonce ${yl(tx.nonce)})`); - const receipt = await tx.wait(); if (!receipt) { throw new Error(`Failed to wait till the transaction ${tx.hash} execution!`); @@ -92,9 +86,6 @@ async function deployContractType2( (contract as DeployedContract).deploymentGasUsed = gasUsed; (contract as DeployedContract).deploymentTx = tx.hash; - log(` Deployed: ${gr(receipt.contractAddress!)} (gas used: ${yl(gasUsed)})`); - log.emptyLine(); - await addContractHelperFields(contract, artifactName); return contract as DeployedContract; diff --git a/lib/index.ts b/lib/index.ts index f1df50e7f..a2dde748d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -21,6 +21,4 @@ export * from "./signing-keys"; export * from "./state-file"; export * from "./string"; export * from "./time"; -export * from "./transaction"; -export * from "./type"; export * from "./units"; diff --git a/lib/log.ts b/lib/log.ts index 1291fafba..7e053e632 100644 --- a/lib/log.ts +++ b/lib/log.ts @@ -1,8 +1,6 @@ import chalk from "chalk"; import path from "path"; -import { TraceableTransaction } from "./type"; - // @ts-expect-error TS2339: Property 'toJSON' does not exist on type 'BigInt'. BigInt.prototype.toJSON = function () { return this.toString(); @@ -127,24 +125,3 @@ log.debug = (title: string, records: Record) => { Object.keys(records).forEach((label) => _record(` ${label}`, records[label])); log.emptyLine(); }; - -log.traceTransaction = (name: string, tx: TraceableTransaction) => { - const value = tx.value === "0.0" ? "" : `Value: ${yl(tx.value)} ETH`; - const from = `From: ${yl(tx.from)}`; - const to = `To: ${yl(tx.to)}`; - const gasPrice = `Gas price: ${yl(tx.gasPrice)} gwei`; - const gasLimit = `Gas limit: ${yl(tx.gasLimit)}`; - const gasUsed = `Gas used: ${yl(tx.gasUsed)} (${yl(tx.gasUsedPercent)})`; - const block = `Block: ${yl(tx.blockNumber)}`; - const nonce = `Nonce: ${yl(tx.nonce)}`; - - const color = tx.status ? gr : rd; - const status = `${color(name)} ${color(tx.status ? "confirmed" : "failed")}`; - - log(`Transaction sent:`, yl(tx.hash)); - log(` ${from} ${to} ${value}`); - log(` ${gasPrice} ${gasLimit} ${gasUsed}`); - log(` ${block} ${nonce}`); - log(` ${status}`); - log.emptyLine(); -}; diff --git a/lib/protocol/helpers/accounting.ts b/lib/protocol/helpers/accounting.ts index 7ff51943c..d07b33591 100644 --- a/lib/protocol/helpers/accounting.ts +++ b/lib/protocol/helpers/accounting.ts @@ -18,7 +18,6 @@ import { log, ONE_GWEI, streccak, - trace, } from "lib"; import { ProtocolContext } from "../types"; @@ -447,7 +446,7 @@ export const handleOracleReport = async ( "El Rewards Vault Balance": formatEther(elRewardsVaultBalance), }); - const handleReportTx = await lido.connect(accountingOracleAccount).handleOracleReport( + await lido.connect(accountingOracleAccount).handleOracleReport( reportTimestamp, 1n * 24n * 60n * 60n, // 1 day beaconValidators, @@ -458,8 +457,6 @@ export const handleOracleReport = async ( [], 0n, ); - - await trace("lido.handleOracleReport", handleReportTx); } catch (error) { log.error("Error", (error as Error).message ?? "Unknown error during oracle report simulation"); expect(error).to.be.undefined; @@ -629,8 +626,6 @@ export const submitReport = async ( const reportTx = await accountingOracle.connect(submitter).submitReportData(data, oracleVersion); - await trace("accountingOracle.submitReportData", reportTx); - log.debug("Pushed oracle report main data", { "Ref slot": refSlot, "Consensus version": consensusVersion, @@ -640,10 +635,8 @@ export const submitReport = async ( let extraDataTx: ContractTransactionResponse; if (extraDataFormat) { extraDataTx = await accountingOracle.connect(submitter).submitReportExtraDataList(extraDataList); - await trace("accountingOracle.submitReportExtraDataList", extraDataTx); } else { extraDataTx = await accountingOracle.connect(submitter).submitReportExtraDataEmpty(); - await trace("accountingOracle.submitReportExtraDataEmpty", extraDataTx); } const state = await accountingOracle.getProcessingState(); @@ -712,8 +705,7 @@ export const ensureOracleCommitteeMembers = async (ctx: ProtocolContext, minMemb log.warning(`Adding oracle committee member ${count}`); const address = getOracleCommitteeMemberAddress(count); - const addTx = await hashConsensus.connect(agentSigner).addMember(address, minMembersCount); - await trace("hashConsensus.addMember", addTx); + await hashConsensus.connect(agentSigner).addMember(address, minMembersCount); addresses.push(address); @@ -745,9 +737,7 @@ export const ensureHashConsensusInitialEpoch = async (ctx: ProtocolContext) => { const updatedInitialEpoch = (latestBlockTimestamp - genesisTime) / (slotsPerEpoch * secondsPerSlot); const agentSigner = await ctx.getSigner("agent"); - - const tx = await hashConsensus.connect(agentSigner).updateInitialEpoch(updatedInitialEpoch); - await trace("hashConsensus.updateInitialEpoch", tx); + await hashConsensus.connect(agentSigner).updateInitialEpoch(updatedInitialEpoch); log.success("Hash consensus epoch initialized"); } @@ -784,8 +774,7 @@ const reachConsensus = async ( submitter = member; } - const tx = await hashConsensus.connect(member).submitReport(refSlot, reportHash, consensusVersion); - await trace("hashConsensus.submitReport", tx); + await hashConsensus.connect(member).submitReport(refSlot, reportHash, consensusVersion); } const { consensusReport } = await hashConsensus.getConsensusState(); diff --git a/lib/protocol/helpers/nor.ts b/lib/protocol/helpers/nor.ts index c5185d82a..eb86f3a0f 100644 --- a/lib/protocol/helpers/nor.ts +++ b/lib/protocol/helpers/nor.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { randomBytes } from "ethers"; -import { certainAddress, log, trace } from "lib"; +import { certainAddress, log } from "lib"; import { ProtocolContext, StakingModuleName } from "../types"; @@ -126,9 +126,7 @@ export const norAddNodeOperator = async ( log.warning(`Adding fake NOR operator ${operatorId}`); const agentSigner = await ctx.getSigner("agent"); - - const addTx = await nor.connect(agentSigner).addNodeOperator(name, rewardAddress); - await trace("nodeOperatorRegistry.addNodeOperator", addTx); + await nor.connect(agentSigner).addNodeOperator(name, rewardAddress); log.debug("Added NOR fake operator", { "Operator ID": operatorId, @@ -160,7 +158,7 @@ export const norAddOperatorKeys = async ( const votingSigner = await ctx.getSigner("voting"); - const addKeysTx = await nor + await nor .connect(votingSigner) .addSigningKeys( operatorId, @@ -168,7 +166,6 @@ export const norAddOperatorKeys = async ( randomBytes(Number(keysToAdd * PUBKEY_LENGTH)), randomBytes(Number(keysToAdd * SIGNATURE_LENGTH)), ); - await trace("nodeOperatorRegistry.addSigningKeys", addKeysTx); const totalKeysAfter = await nor.getTotalSigningKeyCount(operatorId); const unusedKeysAfter = await nor.getUnusedSigningKeyCount(operatorId); @@ -204,9 +201,7 @@ const norSetOperatorStakingLimit = async ( log.warning(`Setting NOR operator ${operatorId} staking limit`); const votingSigner = await ctx.getSigner("voting"); - - const setLimitTx = await nor.connect(votingSigner).setNodeOperatorStakingLimit(operatorId, limit); - await trace("nodeOperatorRegistry.setNodeOperatorStakingLimit", setLimitTx); + await nor.connect(votingSigner).setNodeOperatorStakingLimit(operatorId, limit); log.success(`Set NOR operator ${operatorId} staking limit`); }; diff --git a/lib/protocol/helpers/sdvt.ts b/lib/protocol/helpers/sdvt.ts index cc722e580..35b8fd96f 100644 --- a/lib/protocol/helpers/sdvt.ts +++ b/lib/protocol/helpers/sdvt.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { randomBytes } from "ethers"; -import { ether, impersonate, log, streccak, trace } from "lib"; +import { ether, impersonate, log, streccak } from "lib"; import { ProtocolContext } from "../types"; @@ -138,17 +138,14 @@ const sdvtAddNodeOperator = async ( const easyTrackExecutor = await ctx.getSigner("easyTrack"); - const addTx = await sdvt.connect(easyTrackExecutor).addNodeOperator(name, rewardAddress); - await trace("simpleDVT.addNodeOperator", addTx); - - const grantPermissionTx = await acl.connect(easyTrackExecutor).grantPermissionP( + await sdvt.connect(easyTrackExecutor).addNodeOperator(name, rewardAddress); + await acl.connect(easyTrackExecutor).grantPermissionP( managerAddress, sdvt.address, MANAGE_SIGNING_KEYS_ROLE, // See https://legacy-docs.aragon.org/developers/tools/aragonos/reference-aragonos-3#parameter-interpretation for details [1 << (240 + Number(operatorId))], ); - await trace("acl.grantPermissionP", grantPermissionTx); log.debug("Added SDVT fake operator", { "Operator ID": operatorId, @@ -176,8 +173,7 @@ const sdvtAddNodeOperatorKeys = async ( const { rewardAddress } = await sdvt.getNodeOperator(operatorId, false); const actor = await impersonate(rewardAddress, ether("100")); - - const addKeysTx = await sdvt + await sdvt .connect(actor) .addSigningKeys( operatorId, @@ -185,7 +181,6 @@ const sdvtAddNodeOperatorKeys = async ( randomBytes(Number(keysToAdd * PUBKEY_LENGTH)), randomBytes(Number(keysToAdd * SIGNATURE_LENGTH)), ); - await trace("simpleDVT.addSigningKeys", addKeysTx); const totalKeysAfter = await sdvt.getTotalSigningKeyCount(operatorId); const unusedKeysAfter = await sdvt.getUnusedSigningKeyCount(operatorId); @@ -218,6 +213,5 @@ const sdvtSetOperatorStakingLimit = async ( const easyTrackExecutor = await ctx.getSigner("easyTrack"); - const setLimitTx = await sdvt.connect(easyTrackExecutor).setNodeOperatorStakingLimit(operatorId, limit); - await trace("simpleDVT.setNodeOperatorStakingLimit", setLimitTx); + await sdvt.connect(easyTrackExecutor).setNodeOperatorStakingLimit(operatorId, limit); }; diff --git a/lib/protocol/helpers/staking.ts b/lib/protocol/helpers/staking.ts index 39b8b9884..4d7a2874a 100644 --- a/lib/protocol/helpers/staking.ts +++ b/lib/protocol/helpers/staking.ts @@ -1,6 +1,6 @@ import { ZeroAddress } from "ethers"; -import { certainAddress, ether, impersonate, log, trace } from "lib"; +import { certainAddress, ether, impersonate, log } from "lib"; import { ZERO_HASH } from "test/deploy"; @@ -17,8 +17,7 @@ export const unpauseStaking = async (ctx: ProtocolContext) => { log.warning("Unpausing staking contract"); const votingSigner = await ctx.getSigner("voting"); - const tx = await lido.connect(votingSigner).resume(); - await trace("lido.resume", tx); + await lido.connect(votingSigner).resume(); log.success("Staking contract unpaused"); } @@ -35,8 +34,7 @@ export const ensureStakeLimit = async (ctx: ProtocolContext) => { const stakeLimitIncreasePerBlock = ether("20"); // this is an arbitrary value const votingSigner = await ctx.getSigner("voting"); - const tx = await lido.connect(votingSigner).setStakingLimit(maxStakeLimit, stakeLimitIncreasePerBlock); - await trace("lido.setStakingLimit", tx); + await lido.connect(votingSigner).setStakingLimit(maxStakeLimit, stakeLimitIncreasePerBlock); log.success("Staking limit set"); } diff --git a/lib/protocol/helpers/withdrawal.ts b/lib/protocol/helpers/withdrawal.ts index 3066a8a73..4f360238c 100644 --- a/lib/protocol/helpers/withdrawal.ts +++ b/lib/protocol/helpers/withdrawal.ts @@ -1,6 +1,6 @@ import { ZeroAddress } from "ethers"; -import { certainAddress, ether, impersonate, log, trace } from "lib"; +import { certainAddress, ether, impersonate, log } from "lib"; import { ProtocolContext } from "../types"; @@ -19,10 +19,7 @@ export const unpauseWithdrawalQueue = async (ctx: ProtocolContext) => { const agentSignerAddress = await agentSigner.getAddress(); await withdrawalQueue.connect(agentSigner).grantRole(resumeRole, agentSignerAddress); - - const tx = await withdrawalQueue.connect(agentSigner).resume(); - await trace("withdrawalQueue.resume", tx); - + await withdrawalQueue.connect(agentSigner).resume(); await withdrawalQueue.connect(agentSigner).revokeRole(resumeRole, agentSignerAddress); log.success("Unpaused withdrawal queue contract"); @@ -37,8 +34,7 @@ export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { const stEthHolderAmount = ether("10000"); // Here sendTransaction is used to validate native way of submitting ETH for stETH - const tx = await stEthHolder.sendTransaction({ to: lido.address, value: stEthHolderAmount }); - await trace("stEthHolder.sendTransaction", tx); + await stEthHolder.sendTransaction({ to: lido.address, value: stEthHolderAmount }); let lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); let lastRequestId = await withdrawalQueue.getLastRequestId(); @@ -54,14 +50,10 @@ export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { "Last request ID": lastRequestId, }); - const submitTx = await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); - - await trace("lido.submit", submitTx); + await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); } - const submitTx = await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); - - await trace("lido.submit", submitTx); + await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); log.success("Finalized withdrawal queue"); }; diff --git a/lib/transaction.ts b/lib/transaction.ts deleted file mode 100644 index 0160a7f39..000000000 --- a/lib/transaction.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - ContractTransactionReceipt, - ContractTransactionResponse, - TransactionReceipt, - TransactionResponse, -} from "ethers"; -import hre, { ethers } from "hardhat"; - -import { log } from "lib"; - -type Transaction = TransactionResponse | ContractTransactionResponse; -type Receipt = TransactionReceipt | ContractTransactionReceipt; - -export const trace = async (name: string, tx: Transaction) => { - const receipt = await tx.wait(); - - if (!receipt) { - log.error("Failed to trace transaction: no receipt!"); - throw new Error(`Failed to trace transaction for ${name}: no receipt!`); - } - - const network = await tx.provider.getNetwork(); - const config = hre.config.networks[network.name]; - const blockGasLimit = "blockGasLimit" in config ? config.blockGasLimit : 30_000_000; - const gasUsedPercent = (Number(receipt.gasUsed) / blockGasLimit) * 100; - - log.traceTransaction(name, { - from: tx.from, - to: tx.to ?? `New contract @ ${receipt.contractAddress}`, - value: ethers.formatEther(tx.value), - gasUsed: ethers.formatUnits(receipt.gasUsed, "wei"), - gasPrice: ethers.formatUnits(receipt.gasPrice, "gwei"), - gasUsedPercent: `${gasUsedPercent.toFixed(2)}%`, - gasLimit: blockGasLimit.toString(), - nonce: tx.nonce, - blockNumber: receipt.blockNumber, - hash: receipt.hash, - status: !!receipt.status, - }); - - return receipt as T; -}; diff --git a/lib/type.ts b/lib/type.ts deleted file mode 100644 index 1660da4ea..000000000 --- a/lib/type.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type ArrayToUnion = A[number]; - -export type TraceableTransaction = { - from: string; - to: string; - value: string; - gasUsed: string; - gasPrice: string; - gasLimit: string; - gasUsedPercent: string; - nonce: number; - blockNumber: number; - hash: string; - status: boolean; -}; diff --git a/tasks/index.ts b/tasks/index.ts index 04b17d7c9..570db57d5 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -1,3 +1,4 @@ -export * from "./verify-contracts"; -export * from "./extract-abis"; -export * from "./solidity-get-source"; +import "./logger"; +import "./solidity-get-source"; +import "./extract-abis"; +import "./verify-contracts"; diff --git a/tasks/logger.ts b/tasks/logger.ts new file mode 100644 index 000000000..087c2385f --- /dev/null +++ b/tasks/logger.ts @@ -0,0 +1,181 @@ +import "hardhat/types/runtime"; +import chalk from "chalk"; +import { formatUnits, Interface, TransactionReceipt, TransactionResponse } from "ethers"; +import { extendEnvironment } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +// Custom errors +class NoReceiptError extends Error { + constructor() { + super("Transaction receipt not found"); + } +} + +// Types +interface FunctionDetails { + name?: string; + functionName?: string; +} + +enum TransactionType { + CONTRACT_DEPLOYMENT = "Contract deployment", + ETH_TRANSFER = "ETH transfer", + CONTRACT_CALL = "Contract call", +} + +// Constants +const DEFAULT_BLOCK_GAS_LIMIT = 30_000_000; +const FUNCTION_SIGNATURE_LENGTH = 10; + +// Cache for contract interfaces and function details +const interfaceCache = new Map(); +const functionDetailsCache = new Map(); + +// Helper functions +function formatGasUsage(gasUsed: bigint, blockGasLimit: number): string { + const gasUsedPercent = (Number(gasUsed) * 100) / blockGasLimit; + return `${gasUsed} (${gasUsedPercent.toFixed(2)}%)`; +} + +function formatTransactionLines( + tx: TransactionResponse, + receipt: TransactionReceipt, + txType: string, + name: string | undefined, + functionName: string | undefined, + blockGasLimit: number, + gasPrice: string, +): string[] { + const lines = [ + `Transaction sent: ${chalk.yellow(receipt.hash)}`, + ` From: ${chalk.cyan(tx.from)} To: ${chalk.cyan(tx.to || receipt.contractAddress)}`, + ` Gas price: ${chalk.yellow(gasPrice)} gwei Gas limit: ${chalk.yellow(blockGasLimit)} Gas used: ${chalk.yellow(formatGasUsage(receipt.gasUsed, blockGasLimit))}`, + ` Block: ${chalk.yellow(receipt.blockNumber)} Nonce: ${chalk.yellow(tx.nonce)}`, + ]; + + const color = receipt.status ? chalk.green : chalk.red; + const status = receipt.status ? "confirmed" : "failed"; + + if (txType === TransactionType.CONTRACT_DEPLOYMENT) { + lines.push(` Contract address: ${chalk.cyan(receipt.contractAddress)}`); + lines.push(` ${color(name || "Contract deployment")} ${color(status)}`); + } else if (txType === TransactionType.ETH_TRANSFER) { + lines.push(` ETH transfer: ${chalk.cyan(tx.value)}`); + lines.push(` ${color("ETH transfer")} ${color(status)}`); + } else { + const txName = name && functionName ? `${name}.${functionName}` : functionName || "Contract call"; + lines.push(` ${color(txName)} ${color(status)}`); + } + + return lines; +} + +extendEnvironment((hre: HardhatRuntimeEnvironment) => { + const originalSendTransaction = hre.ethers.provider.send; + + // Wrap the provider's send method to intercept transactions + hre.ethers.provider.send = async function (method: string, params: unknown[]) { + const result = await originalSendTransaction.apply(this, [method, params]); + + // Only log eth_sendTransaction and eth_sendRawTransaction calls + if (method === "eth_sendTransaction" || method === "eth_sendRawTransaction") { + const tx = (await this.getTransaction(result)) as TransactionResponse; + await logTransaction(tx); + } + + return result; + }; + + async function getFunctionDetails(tx: TransactionResponse): Promise { + if (!tx.data || tx.data === "0x" || !tx.to) return {}; + + // Check cache first + const cacheKey = `${tx.to}-${tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH)}`; + if (functionDetailsCache.has(cacheKey)) { + return functionDetailsCache.get(cacheKey)!; + } + + try { + // Try to get contract name and function name from all available artifacts + const allArtifacts = await hre.artifacts.getAllFullyQualifiedNames(); + + for (const artifactName of allArtifacts) { + try { + let iface: Interface; + + // Check interface cache + if (interfaceCache.has(artifactName)) { + iface = interfaceCache.get(artifactName)!; + } else { + const artifact = await hre.artifacts.readArtifact(artifactName); + iface = new Interface(artifact.abi); + interfaceCache.set(artifactName, iface); + } + + const result = iface.parseTransaction({ data: tx.data }); + + if (result) { + const details = { + name: artifactName.split(":").pop() || "", + functionName: result.name, + }; + functionDetailsCache.set(cacheKey, details); + return details; + } + } catch { + continue; // Skip artifacts that can't be parsed + } + } + } catch (error) { + console.warn("Error getting function details:", error); + } + + // Cache and return function signature if we can't decode + const details = { + functionName: tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH), + }; + functionDetailsCache.set(cacheKey, details); + return details; + } + + async function logTransaction(tx: TransactionResponse): Promise { + const receipt = await tx.wait(); + if (!receipt) { + throw new NoReceiptError(); + } + + try { + const network = await tx.provider.getNetwork(); + const config = hre.config.networks[network.name]; + const blockGasLimit = "blockGasLimit" in config ? config.blockGasLimit : DEFAULT_BLOCK_GAS_LIMIT; + + const txType = await getTxType(tx, receipt); + const { name, functionName } = await getFunctionDetails(tx); + const gasPrice = formatUnits(receipt.gasPrice || 0n, "gwei"); + + const lines = formatTransactionLines(tx, receipt, txType, name, functionName, blockGasLimit, gasPrice); + + lines.forEach((line) => console.log(line)); + + return receipt; + } catch (error) { + console.error("Error logging transaction:", error); + return receipt; + } + } + + async function getTxType(tx: TransactionResponse, receipt: TransactionReceipt): Promise { + if (receipt.contractAddress) { + return TransactionType.CONTRACT_DEPLOYMENT; + } + + if (!tx.data || tx.data === "0x") { + return TransactionType.ETH_TRANSFER; + } + + const { name, functionName } = await getFunctionDetails(tx); + return name && functionName ? `${name}.${functionName}` : functionName || TransactionType.CONTRACT_CALL; + } + + return logTransaction; +}); diff --git a/test/0.8.9/lidoLocator.test.ts b/test/0.8.9/lidoLocator.test.ts index f970de0c0..711869b43 100644 --- a/test/0.8.9/lidoLocator.test.ts +++ b/test/0.8.9/lidoLocator.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { LidoLocator } from "typechain-types"; -import { ArrayToUnion, randomAddress } from "lib"; +import { randomAddress } from "lib"; const services = [ "accountingOracle", @@ -23,6 +23,7 @@ const services = [ "oracleDaemonConfig", ] as const; +type ArrayToUnion = A[number]; type Service = ArrayToUnion; type Config = Record; diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index 916314310..03d22a5f4 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { ether, impersonate, log, ONE_GWEI, trace, updateBalance } from "lib"; +import { ether, impersonate, log, ONE_GWEI, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { getReportTimeElapsed, report } from "lib/protocol/helpers"; @@ -103,8 +103,7 @@ describe("Accounting", () => { const { lido, wstETH } = ctx.contracts; if (!(await lido.sharesOf(wstETH.address))) { const wstEthSigner = await impersonate(wstETH.address, ether("10001")); - const submitTx = await lido.connect(wstEthSigner).submit(ZeroAddress, { value: ether("10000") }); - await trace("lido.submit", submitTx); + await lido.connect(wstEthSigner).submit(ZeroAddress, { value: ether("10000") }); } } @@ -116,8 +115,7 @@ describe("Accounting", () => { while ((await withdrawalQueue.getLastRequestId()) != (await withdrawalQueue.getLastFinalizedRequestId())) { await report(ctx); - const submitTx = await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); - await trace("lido.submit", submitTx); + await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); } } @@ -742,8 +740,7 @@ describe("Accounting", () => { const stethOfShares = await lido.getPooledEthByShares(sharesLimit); const wstEthSigner = await impersonate(wstETH.address, ether("1")); - const approveTx = await lido.connect(wstEthSigner).approve(burner.address, stethOfShares); - await trace("lido.approve", approveTx); + await lido.connect(wstEthSigner).approve(burner.address, stethOfShares); const coverShares = sharesLimit / 3n; const noCoverShares = sharesLimit - sharesLimit / 3n; @@ -751,7 +748,7 @@ describe("Accounting", () => { const lidoSigner = await impersonate(lido.address); const burnTx = await burner.connect(lidoSigner).requestBurnShares(wstETH.address, noCoverShares); - const burnTxReceipt = await trace("burner.requestBurnShares", burnTx); + const burnTxReceipt = (await burnTx.wait()) as ContractTransactionReceipt; const sharesBurntEvent = getFirstEvent(burnTxReceipt, "StETHBurnRequested"); expect(sharesBurntEvent.args.amountOfShares).to.equal(noCoverShares, "StETHBurnRequested: amountOfShares mismatch"); @@ -762,10 +759,7 @@ describe("Accounting", () => { ); const burnForCoverTx = await burner.connect(lidoSigner).requestBurnSharesForCover(wstETH.address, coverShares); - const burnForCoverTxReceipt = await trace( - "burner.requestBurnSharesForCover", - burnForCoverTx, - ); + const burnForCoverTxReceipt = (await burnForCoverTx.wait()) as ContractTransactionReceipt; const sharesBurntForCoverEvent = getFirstEvent(burnForCoverTxReceipt, "StETHBurnRequested"); expect(sharesBurntForCoverEvent.args.amountOfShares).to.equal(coverShares); @@ -819,8 +813,7 @@ describe("Accounting", () => { const stethOfShares = await lido.getPooledEthByShares(limitWithExcess); const wstEthSigner = await impersonate(wstETH.address, ether("1")); - const approveTx = await lido.connect(wstEthSigner).approve(burner.address, stethOfShares); - await trace("lido.approve", approveTx); + await lido.connect(wstEthSigner).approve(burner.address, stethOfShares); const coverShares = limit / 3n; const noCoverShares = limit - limit / 3n + excess; @@ -828,7 +821,7 @@ describe("Accounting", () => { const lidoSigner = await impersonate(lido.address); const burnTx = await burner.connect(lidoSigner).requestBurnShares(wstETH.address, noCoverShares); - const burnTxReceipt = await trace("burner.requestBurnShares", burnTx); + const burnTxReceipt = (await burnTx.wait()) as ContractTransactionReceipt; const sharesBurntEvent = getFirstEvent(burnTxReceipt, "StETHBurnRequested"); expect(sharesBurntEvent.args.amountOfShares).to.equal(noCoverShares, "StETHBurnRequested: amountOfShares mismatch"); diff --git a/test/integration/burn-shares.integration.ts b/test/integration/burn-shares.integration.ts index 0effa7dc5..ade3c1830 100644 --- a/test/integration/burn-shares.integration.ts +++ b/test/integration/burn-shares.integration.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { ether, impersonate, log, trace } from "lib"; +import { ether, impersonate, log } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { finalizeWithdrawalQueue, handleOracleReport } from "lib/protocol/helpers"; @@ -47,8 +47,7 @@ describe("Burn Shares", () => { it("Should allow stranger to submit ETH", async () => { const { lido } = ctx.contracts; - const submitTx = await lido.connect(stranger).submit(ZeroAddress, { value: amount }); - await trace("lido.submit", submitTx); + await lido.connect(stranger).submit(ZeroAddress, { value: amount }); const stEthBefore = await lido.balanceOf(stranger.address); expect(stEthBefore).to.be.approximately(amount, 10n, "Incorrect stETH balance after submit"); @@ -74,12 +73,10 @@ describe("Burn Shares", () => { it("Should burn shares after report", async () => { const { lido, burner } = ctx.contracts; - const approveTx = await lido.connect(stranger).approve(burner.address, ether("1000000")); - await trace("lido.approve", approveTx); + await lido.connect(stranger).approve(burner.address, ether("1000000")); const lidoSigner = await impersonate(lido.address); - const burnTx = await burner.connect(lidoSigner).requestBurnSharesForCover(stranger, sharesToBurn); - await trace("burner.requestBurnSharesForCover", burnTx); + await burner.connect(lidoSigner).requestBurnSharesForCover(stranger, sharesToBurn); const { beaconValidators, beaconBalance } = await lido.getBeaconStat(); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 3e034d702..5b32f8783 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { batch, ether, impersonate, log, trace, updateBalance } from "lib"; +import { batch, ether, impersonate, log, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { finalizeWithdrawalQueue, @@ -58,8 +58,7 @@ describe("Protocol Happy Path", () => { const stEthHolderAmount = ether("1000"); // Deposit some eth - const tx = await lido.connect(stEthHolder).submit(ZeroAddress, { value: stEthHolderAmount }); - await trace("lido.submit", tx); + await lido.connect(stEthHolder).submit(ZeroAddress, { value: stEthHolderAmount }); const stEthHolderBalance = await lido.balanceOf(stEthHolder.address); expect(stEthHolderBalance).to.approximately(stEthHolderAmount, 10n, "stETH balance increased"); @@ -73,11 +72,8 @@ describe("Protocol Happy Path", () => { uncountedStETHShares = await lido.sharesOf(withdrawalQueue.address); // Added to facilitate the burner transfers - const approveTx = await lido.connect(stEthHolder).approve(withdrawalQueue.address, 1000n); - await trace("lido.approve", approveTx); - - const requestWithdrawalsTx = await withdrawalQueue.connect(stEthHolder).requestWithdrawals([1000n], stEthHolder); - await trace("withdrawalQueue.requestWithdrawals", requestWithdrawalsTx); + await lido.connect(stEthHolder).approve(withdrawalQueue.address, 1000n); + await withdrawalQueue.connect(stEthHolder).requestWithdrawals([1000n], stEthHolder); expect(lastFinalizedRequestId).to.equal(lastRequestId); }); @@ -129,7 +125,7 @@ describe("Protocol Happy Path", () => { }); const tx = await lido.connect(stranger).submit(ZeroAddress, { value: AMOUNT }); - const receipt = await trace("lido.submit", tx); + const receipt = (await tx.wait()) as ContractTransactionReceipt; expect(receipt).not.to.be.null; @@ -225,7 +221,7 @@ describe("Protocol Happy Path", () => { let expectedBufferedEtherAfterDeposit = bufferedEtherBeforeDeposit; for (const module of stakingModules) { const depositTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, module.id, ZERO_HASH); - const depositReceipt = await trace(`lido.deposit (${module.name})`, depositTx); + const depositReceipt = (await depositTx.wait()) as ContractTransactionReceipt; const unbufferedEvent = ctx.getEvents(depositReceipt, "Unbuffered")[0]; const unbufferedAmount = unbufferedEvent?.args[0] || 0n; const deposits = unbufferedAmount / ether("32"); @@ -428,7 +424,7 @@ describe("Protocol Happy Path", () => { amountWithRewards = balanceBeforeRequest.stETH; const approveTx = await lido.connect(stranger).approve(withdrawalQueue.address, amountWithRewards); - const approveTxReceipt = await trace("lido.approve", approveTx); + const approveTxReceipt = (await approveTx.wait()) as ContractTransactionReceipt; const approveEvent = ctx.getEvents(approveTxReceipt, "Approval")[0]; @@ -444,11 +440,7 @@ describe("Protocol Happy Path", () => { const lastRequestIdBefore = await withdrawalQueue.getLastRequestId(); const withdrawalTx = await withdrawalQueue.connect(stranger).requestWithdrawals([amountWithRewards], stranger); - const withdrawalTxReceipt = await trace( - "withdrawalQueue.requestWithdrawals", - withdrawalTx, - ); - + const withdrawalTxReceipt = (await withdrawalTx.wait()) as ContractTransactionReceipt; const withdrawalEvent = ctx.getEvents(withdrawalTxReceipt, "WithdrawalRequested")[0]; expect(withdrawalEvent?.args.toObject()).to.deep.include( @@ -594,12 +586,11 @@ describe("Protocol Happy Path", () => { expect(claimableEtherBeforeClaim).to.equal(amountWithRewards, "Claimable ether before claim"); const claimTx = await withdrawalQueue.connect(stranger).claimWithdrawals([requestId], hints); - const claimTxReceipt = await trace("withdrawalQueue.claimWithdrawals", claimTx); + const claimTxReceipt = (await claimTx.wait()) as ContractTransactionReceipt; + const claimEvent = ctx.getEvents(claimTxReceipt, "WithdrawalClaimed")[0]; const spentGas = claimTxReceipt.gasUsed * claimTxReceipt.gasPrice; - const claimEvent = ctx.getEvents(claimTxReceipt, "WithdrawalClaimed")[0]; - expect(claimEvent?.args.toObject()).to.deep.include( { requestId, From 3155741c70e0b19811ba77cba1f1c86c9b00bdb6 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 11 Feb 2025 18:36:53 +0000 Subject: [PATCH 31/65] chore: cleanup logs --- lib/protocol/helpers/accounting.ts | 20 ++++++++++--- lib/protocol/helpers/nor.ts | 19 ++++++++++--- lib/protocol/helpers/sdvt.ts | 31 ++++++++++----------- lib/protocol/helpers/staking.ts | 11 ++++---- lib/protocol/helpers/withdrawal.ts | 2 -- lib/protocol/provision.ts | 2 ++ tasks/logger.ts | 5 ++++ test/integration/burn-shares.integration.ts | 13 +-------- 8 files changed, 59 insertions(+), 44 deletions(-) diff --git a/lib/protocol/helpers/accounting.ts b/lib/protocol/helpers/accounting.ts index d07b33591..e8a497a77 100644 --- a/lib/protocol/helpers/accounting.ts +++ b/lib/protocol/helpers/accounting.ts @@ -490,7 +490,13 @@ const getFinalizationBatches = async ( const MAX_REQUESTS_PER_CALL = 1000n; if (availableEth === 0n) { - log.warning("No available ether to request withdrawals"); + log.debug("No available ether to request withdrawals", { + "Share rate": shareRate, + "Available eth": formatEther(availableEth), + "Limited withdrawal vault balance": formatEther(limitedWithdrawalVaultBalance), + "Limited el rewards vault balance": formatEther(limitedElRewardsVaultBalance), + "Reserved buffer": formatEther(reservedBuffer), + }); return []; } @@ -702,9 +708,13 @@ export const ensureOracleCommitteeMembers = async (ctx: ProtocolContext, minMemb let count = addresses.length; while (addresses.length < minMembersCount) { - log.warning(`Adding oracle committee member ${count}`); - const address = getOracleCommitteeMemberAddress(count); + + log.debug(`Adding oracle committee member ${count}`, { + "Min members count": minMembersCount, + "Address": address, + }); + await hashConsensus.connect(agentSigner).addMember(address, minMembersCount); addresses.push(address); @@ -730,7 +740,9 @@ export const ensureHashConsensusInitialEpoch = async (ctx: ProtocolContext) => { const { initialEpoch } = await hashConsensus.getFrameConfig(); if (initialEpoch === HASH_CONSENSUS_FAR_FUTURE_EPOCH) { - log.warning("Initializing hash consensus epoch..."); + log.debug("Initializing hash consensus epoch...", { + "Initial epoch": initialEpoch, + }); const latestBlockTimestamp = await getCurrentBlockTimestamp(); const { genesisTime, secondsPerSlot, slotsPerEpoch } = await hashConsensus.getChainConfig(); diff --git a/lib/protocol/helpers/nor.ts b/lib/protocol/helpers/nor.ts index eb86f3a0f..c350d7011 100644 --- a/lib/protocol/helpers/nor.ts +++ b/lib/protocol/helpers/nor.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { randomBytes } from "ethers"; +import { ethers, randomBytes } from "ethers"; import { certainAddress, log } from "lib"; @@ -123,7 +123,12 @@ export const norAddNodeOperator = async ( const { nor } = ctx.contracts; const { operatorId, name, rewardAddress, managerAddress } = params; - log.warning(`Adding fake NOR operator ${operatorId}`); + log.debug(`Adding fake NOR operator ${operatorId}`, { + "Operator ID": operatorId, + "Name": name, + "Reward address": rewardAddress, + "Manager address": managerAddress, + }); const agentSigner = await ctx.getSigner("agent"); await nor.connect(agentSigner).addNodeOperator(name, rewardAddress); @@ -151,7 +156,10 @@ export const norAddOperatorKeys = async ( const { nor } = ctx.contracts; const { operatorId, keysToAdd } = params; - log.warning(`Adding fake keys to NOR operator ${operatorId}`); + log.debug(`Adding fake keys to NOR operator ${operatorId}`, { + "Operator ID": operatorId, + "Keys to add": keysToAdd, + }); const totalKeysBefore = await nor.getTotalSigningKeyCount(operatorId); const unusedKeysBefore = await nor.getUnusedSigningKeyCount(operatorId); @@ -198,7 +206,10 @@ const norSetOperatorStakingLimit = async ( const { nor } = ctx.contracts; const { operatorId, limit } = params; - log.warning(`Setting NOR operator ${operatorId} staking limit`); + log.debug(`Setting NOR operator ${operatorId} staking limit`, { + "Operator ID": operatorId, + "Limit": ethers.formatEther(limit), + }); const votingSigner = await ctx.getSigner("voting"); await nor.connect(votingSigner).setNodeOperatorStakingLimit(operatorId, limit); diff --git a/lib/protocol/helpers/sdvt.ts b/lib/protocol/helpers/sdvt.ts index 35b8fd96f..7dc99dcd4 100644 --- a/lib/protocol/helpers/sdvt.ts +++ b/lib/protocol/helpers/sdvt.ts @@ -62,7 +62,10 @@ const sdvtEnsureOperatorsHaveMinKeys = async ( const unusedKeysCount = await sdvt.getUnusedSigningKeyCount(operatorId); if (unusedKeysCount < minKeysCount) { - log.warning(`Adding SDVT fake keys to operator ${operatorId}`); + log.debug(`Adding SDVT fake keys to operator ${operatorId}`, { + "Unused keys count": unusedKeysCount, + "Min keys count": minKeysCount, + }); await sdvtAddNodeOperatorKeys(ctx, { operatorId, @@ -102,7 +105,12 @@ const sdvtEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = managerAddress: getOperatorManagerAddress("sdvt", operatorId), }; - log.warning(`Adding SDVT fake operator ${operatorId}`); + log.debug(`Adding SDVT fake operator ${operatorId}`, { + "Operator ID": operatorId, + "Name": operator.name, + "Reward address": operator.rewardAddress, + "Manager address": operator.managerAddress, + }); await sdvtAddNodeOperator(ctx, operator); count++; @@ -147,12 +155,7 @@ const sdvtAddNodeOperator = async ( [1 << (240 + Number(operatorId))], ); - log.debug("Added SDVT fake operator", { - "Operator ID": operatorId, - "Name": name, - "Reward address": rewardAddress, - "Manager address": managerAddress, - }); + log.success(`Added fake SDVT operator ${operatorId}`); }; /** @@ -188,14 +191,7 @@ const sdvtAddNodeOperatorKeys = async ( expect(totalKeysAfter).to.equal(totalKeysBefore + keysToAdd); expect(unusedKeysAfter).to.equal(unusedKeysBefore + keysToAdd); - log.debug("Added SDVT fake signing keys", { - "Operator ID": operatorId, - "Keys to add": keysToAdd, - "Total keys before": totalKeysBefore, - "Total keys after": totalKeysAfter, - "Unused keys before": unusedKeysBefore, - "Unused keys after": unusedKeysAfter, - }); + log.success(`Added fake keys to SDVT operator ${operatorId}`); }; /** @@ -212,6 +208,7 @@ const sdvtSetOperatorStakingLimit = async ( const { operatorId, limit } = params; const easyTrackExecutor = await ctx.getSigner("easyTrack"); - await sdvt.connect(easyTrackExecutor).setNodeOperatorStakingLimit(operatorId, limit); + + log.success(`Set SDVT operator ${operatorId} staking limit`); }; diff --git a/lib/protocol/helpers/staking.ts b/lib/protocol/helpers/staking.ts index 4d7a2874a..03422bef4 100644 --- a/lib/protocol/helpers/staking.ts +++ b/lib/protocol/helpers/staking.ts @@ -1,4 +1,4 @@ -import { ZeroAddress } from "ethers"; +import { ethers, ZeroAddress } from "ethers"; import { certainAddress, ether, impersonate, log } from "lib"; @@ -14,8 +14,6 @@ import { report } from "./accounting"; export const unpauseStaking = async (ctx: ProtocolContext) => { const { lido } = ctx.contracts; if (await lido.isStakingPaused()) { - log.warning("Unpausing staking contract"); - const votingSigner = await ctx.getSigner("voting"); await lido.connect(votingSigner).resume(); @@ -28,11 +26,14 @@ export const ensureStakeLimit = async (ctx: ProtocolContext) => { const stakeLimitInfo = await lido.getStakeLimitFullInfo(); if (!stakeLimitInfo.isStakingLimitSet) { - log.warning("Setting staking limit"); - const maxStakeLimit = ether("150000"); const stakeLimitIncreasePerBlock = ether("20"); // this is an arbitrary value + log.debug("Setting staking limit", { + "Max stake limit": ethers.formatEther(maxStakeLimit), + "Stake limit increase per block": ethers.formatEther(stakeLimitIncreasePerBlock), + }); + const votingSigner = await ctx.getSigner("voting"); await lido.connect(votingSigner).setStakingLimit(maxStakeLimit, stakeLimitIncreasePerBlock); diff --git a/lib/protocol/helpers/withdrawal.ts b/lib/protocol/helpers/withdrawal.ts index 4f360238c..eb10e630b 100644 --- a/lib/protocol/helpers/withdrawal.ts +++ b/lib/protocol/helpers/withdrawal.ts @@ -12,8 +12,6 @@ import { report } from "./accounting"; export const unpauseWithdrawalQueue = async (ctx: ProtocolContext) => { const { withdrawalQueue } = ctx.contracts; if (await withdrawalQueue.isPaused()) { - log.warning("Unpausing withdrawal queue contract"); - const resumeRole = await withdrawalQueue.RESUME_ROLE(); const agentSigner = await ctx.getSigner("agent"); const agentSignerAddress = await agentSigner.getAddress(); diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index e22e1ca75..9457ba39a 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -38,4 +38,6 @@ export const provision = async (ctx: ProtocolContext) => { await ensureStakeLimit(ctx); alreadyProvisioned = true; + + log.success("Provisioned"); }; diff --git a/tasks/logger.ts b/tasks/logger.ts index 087c2385f..ccb3a360e 100644 --- a/tasks/logger.ts +++ b/tasks/logger.ts @@ -4,6 +4,8 @@ import { formatUnits, Interface, TransactionReceipt, TransactionResponse } from import { extendEnvironment } from "hardhat/config"; import { HardhatRuntimeEnvironment } from "hardhat/types"; +const LOG_LEVEL = process.env.LOG_LEVEL || "info"; + // Custom errors class NoReceiptError extends Error { constructor() { @@ -71,6 +73,8 @@ function formatTransactionLines( } extendEnvironment((hre: HardhatRuntimeEnvironment) => { + if (LOG_LEVEL != "debug" && LOG_LEVEL != "all") return; + const originalSendTransaction = hre.ethers.provider.send; // Wrap the provider's send method to intercept transactions @@ -156,6 +160,7 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { const lines = formatTransactionLines(tx, receipt, txType, name, functionName, blockGasLimit, gasPrice); lines.forEach((line) => console.log(line)); + console.log(); return receipt; } catch (error) { diff --git a/test/integration/burn-shares.integration.ts b/test/integration/burn-shares.integration.ts index ade3c1830..8b670d35b 100644 --- a/test/integration/burn-shares.integration.ts +++ b/test/integration/burn-shares.integration.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { ether, impersonate, log } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { finalizeWithdrawalQueue, handleOracleReport } from "lib/protocol/helpers"; +import { handleOracleReport } from "lib/protocol/helpers"; import { bailOnFailure, Snapshot } from "test/suite"; @@ -33,17 +33,6 @@ describe("Burn Shares", () => { after(async () => await Snapshot.restore(snapshot)); - it("Should finalize withdrawal queue", async () => { - const { withdrawalQueue } = ctx.contracts; - - await finalizeWithdrawalQueue(ctx); - - const lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); - const lastRequestId = await withdrawalQueue.getLastRequestId(); - - expect(lastFinalizedRequestId).to.equal(lastRequestId); - }); - it("Should allow stranger to submit ETH", async () => { const { lido } = ctx.contracts; From 268854531141267cf716475d27ee73933af40e93 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:45:25 +0500 Subject: [PATCH 32/65] feat: add more role granularity --- contracts/0.8.25/vaults/Delegation.sol | 56 ++++++++++-------------- contracts/0.8.25/vaults/VaultFactory.sol | 27 +++++++----- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index abe159ec4..f6b331ef7 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -10,21 +10,6 @@ import {Dashboard} from "./Dashboard.sol"; /** * @title Delegation * @notice This contract is a contract-owner of StakingVault and includes an additional delegation layer. - * - * The delegation hierarchy is as follows: - * - DEFAULT_ADMIN_ROLE is the underlying owner of StakingVault; - * - NODE_OPERATOR_MANAGER_ROLE is the node operator manager of StakingVault; and itself is the role admin, - * and the DEFAULT_ADMIN_ROLE cannot assign NODE_OPERATOR_MANAGER_ROLE; - * - NODE_OPERATOR_FEE_CLAIMER_ROLE is the role that can claim node operator fee; is assigned by NODE_OPERATOR_MANAGER_ROLE; - * - * Additionally, the following roles are assigned by DEFAULT_ADMIN_ROLE: - * - CURATOR_ROLE is the curator of StakingVault and perfoms some operations on behalf of DEFAULT_ADMIN_ROLE; - * - FUND_WITHDRAW_ROLE funds and withdraws from the StakingVault; - * - MINT_BURN_ROLE mints and burns shares of stETH backed by the StakingVault; - * - * The curator and node operator have their respective fees. - * The feeBP is the percentage (in basis points) of the StakingVault rewards. - * The unclaimed fee is the amount of ether that is owed to the curator or node operator based on the feeBP. */ contract Delegation is Dashboard { /** @@ -33,31 +18,33 @@ contract Delegation is Dashboard { uint256 private constant MAX_FEE_BP = TOTAL_BASIS_POINTS; /** - * @notice Curator role: - * - sets curator fee; - * - claims curator fee; - * - confirms confirm lifetime; - * - confirms node operator fee; - * - confirms ownership transfer; - * - pauses deposits to beacon chain; - * - resumes deposits to beacon chain. + * @notice Sets curator fee. + */ + bytes32 public constant CURATOR_FEE_SET_ROLE = keccak256("vaults.Delegation.CuratorFeeSetRole"); + + /** + * @notice Claims curator fee. */ - bytes32 public constant CURATOR_ROLE = keccak256("vaults.Delegation.CuratorRole"); + bytes32 public constant CURATOR_FEE_CLAIM_ROLE = keccak256("vaults.Delegation.CuratorFeeClaimRole"); /** * @notice Node operator manager role: * - confirms confirm lifetime; - * - confirms node operator fee; * - confirms ownership transfer; - * - assigns NODE_OPERATOR_FEE_CLAIMER_ROLE. + * - assigns NODE_OPERATOR_FEE_CONFIRM_ROLE; + * - assigns NODE_OPERATOR_FEE_CLAIM_ROLE. */ bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.Delegation.NodeOperatorManagerRole"); /** - * @notice Node operator fee claimer role: - * - claims node operator fee. + * @notice Confirms node operator fee. + */ + bytes32 public constant NODE_OPERATOR_FEE_CONFIRM_ROLE = keccak256("vaults.Delegation.NodeOperatorFeeConfirmRole"); + + /** + * @notice Claims node operator fee. */ - bytes32 public constant NODE_OPERATOR_FEE_CLAIMER_ROLE = keccak256("vaults.Delegation.NodeOperatorFeeClaimerRole"); + bytes32 public constant NODE_OPERATOR_FEE_CLAIM_ROLE = keccak256("vaults.Delegation.NodeOperatorFeeClaimRole"); /** * @notice Curator fee in basis points; combined with node operator fee cannot exceed 100%. @@ -105,7 +92,8 @@ contract Delegation is Dashboard { // at the end of the initialization _grantRole(NODE_OPERATOR_MANAGER_ROLE, _defaultAdmin); _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE); - _setRoleAdmin(NODE_OPERATOR_FEE_CLAIMER_ROLE, NODE_OPERATOR_MANAGER_ROLE); + _setRoleAdmin(NODE_OPERATOR_FEE_CONFIRM_ROLE, NODE_OPERATOR_MANAGER_ROLE); + _setRoleAdmin(NODE_OPERATOR_FEE_CLAIM_ROLE, NODE_OPERATOR_MANAGER_ROLE); } /** @@ -168,7 +156,7 @@ contract Delegation is Dashboard { * The function will revert if the curator fee is unclaimed. * @param _newCuratorFeeBP The new curator fee in basis points. */ - function setCuratorFeeBP(uint256 _newCuratorFeeBP) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setCuratorFeeBP(uint256 _newCuratorFeeBP) external onlyRole(CURATOR_FEE_SET_ROLE) { if (_newCuratorFeeBP + nodeOperatorFeeBP > MAX_FEE_BP) revert CombinedFeesExceed100Percent(); if (curatorUnclaimedFee() > 0) revert CuratorFeeUnclaimed(); uint256 oldCuratorFeeBP = curatorFeeBP; @@ -198,7 +186,7 @@ contract Delegation is Dashboard { * @notice Claims the curator fee. * @param _recipient The address to which the curator fee will be sent. */ - function claimCuratorFee(address _recipient) external onlyRole(CURATOR_ROLE) { + function claimCuratorFee(address _recipient) external onlyRole(CURATOR_FEE_CLAIM_ROLE) { uint256 fee = curatorUnclaimedFee(); curatorFeeClaimedReport = stakingVault().latestReport(); _claimFee(_recipient, fee); @@ -210,7 +198,7 @@ contract Delegation is Dashboard { * although NODE_OPERATOR_MANAGER_ROLE is the admin role for NODE_OPERATOR_FEE_CLAIMER_ROLE. * @param _recipient The address to which the node operator fee will be sent. */ - function claimNodeOperatorFee(address _recipient) external onlyRole(NODE_OPERATOR_FEE_CLAIMER_ROLE) { + function claimNodeOperatorFee(address _recipient) external onlyRole(NODE_OPERATOR_FEE_CLAIM_ROLE) { uint256 fee = nodeOperatorUnclaimedFee(); nodeOperatorFeeClaimedReport = stakingVault().latestReport(); _claimFee(_recipient, fee); @@ -267,7 +255,7 @@ contract Delegation is Dashboard { */ function _confirmingRoles() internal pure override returns (bytes32[] memory roles) { roles = new bytes32[](2); - roles[0] = CURATOR_ROLE; + roles[0] = DEFAULT_ADMIN_ROLE; roles[1] = NODE_OPERATOR_MANAGER_ROLE; } diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 65b0c2bbb..209a4ea4f 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -21,9 +21,11 @@ struct DelegationConfig { address depositResumer; address exitRequester; address disconnecter; - address curator; + address curatorFeeSetter; + address curatorFeeClaimer; address nodeOperatorManager; - address nodeOperatorFeeClaimer; + address nodeOperatorFeeConfirm; + address nodeOperatorFeeClaim; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; uint256 confirmLifetime; @@ -50,8 +52,6 @@ contract VaultFactory { DelegationConfig calldata _delegationConfig, bytes calldata _stakingVaultInitializerExtraParams ) external returns (IStakingVault vault, Delegation delegation) { - if (_delegationConfig.curator == address(0)) revert ZeroArgument("curator"); - // create StakingVault vault = IStakingVault(address(new BeaconProxy(BEACON, ""))); @@ -69,7 +69,8 @@ contract VaultFactory { // initialize Delegation delegation.initialize(address(this), _delegationConfig.confirmLifetime); - // setup roles + // setup roles from config + // basic permissions to the staking vault delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), _delegationConfig.defaultAdmin); delegation.grantRole(delegation.FUND_ROLE(), _delegationConfig.funder); delegation.grantRole(delegation.WITHDRAW_ROLE(), _delegationConfig.withdrawer); @@ -80,20 +81,24 @@ contract VaultFactory { delegation.grantRole(delegation.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _delegationConfig.depositResumer); delegation.grantRole(delegation.REQUEST_VALIDATOR_EXIT_ROLE(), _delegationConfig.exitRequester); delegation.grantRole(delegation.VOLUNTARY_DISCONNECT_ROLE(), _delegationConfig.disconnecter); - delegation.grantRole(delegation.CURATOR_ROLE(), _delegationConfig.curator); + // delegation roles + delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), _delegationConfig.curatorFeeSetter); + delegation.grantRole(delegation.CURATOR_FEE_CLAIM_ROLE(), _delegationConfig.curatorFeeClaimer); delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), _delegationConfig.nodeOperatorManager); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIMER_ROLE(), _delegationConfig.nodeOperatorFeeClaimer); + delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), _delegationConfig.nodeOperatorFeeClaim); + delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), _delegationConfig.nodeOperatorFeeConfirm); - // grant temporary roles to factory - delegation.grantRole(delegation.CURATOR_ROLE(), address(this)); - delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), address(this)); + // grant temporary roles to factory for setting fees + delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); + delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), address(this)); // set fees delegation.setCuratorFeeBP(_delegationConfig.curatorFeeBP); delegation.setNodeOperatorFeeBP(_delegationConfig.nodeOperatorFeeBP); // revoke temporary roles from factory - delegation.revokeRole(delegation.CURATOR_ROLE(), address(this)); + delegation.revokeRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); + delegation.revokeRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), address(this)); delegation.revokeRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), address(this)); delegation.revokeRole(delegation.DEFAULT_ADMIN_ROLE(), address(this)); From 927703ff1c88c5fdeaa9a93d6b357503418d59d9 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:50:30 +0500 Subject: [PATCH 33/65] feat: add getter for timestamp --- contracts/0.8.25/utils/AccessControlConfirmable.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index ea1036f55..4ec521085 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -26,6 +26,16 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { */ uint256 public confirmLifetime; + /** + * @notice Returns the expiry timestamp of the confirmation for a given call and role. + * @param _callData The call data of the function. + * @param _role The role that confirmed the call. + * @return The expiry timestamp of the confirmation. + */ + function confirmationExpiryTimestamp(bytes calldata _callData, bytes32 _role) public view returns (uint256) { + return confirmations[_callData][_role]; + } + /** * @dev Restricts execution of the function unless confirmed by all specified roles. * Confirmation, in this context, is a call to the same function with the same arguments. From 7b382286a50f59382b1b3de024e46383cab950d5 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:53:46 +0500 Subject: [PATCH 34/65] fix: add a behavior comment --- contracts/0.8.25/vaults/Permissions.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index d4f9e1328..f90b30689 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -115,6 +115,7 @@ abstract contract Permissions is AccessControlConfirmable { * @notice Mass-grants multiple roles to multiple accounts. * @param _assignments An array of role assignments. * @dev Performs the role admin checks internally. + * @dev If an account is already a member of a role, doesn't revert, emits no events. */ function grantRoles(RoleAssignment[] memory _assignments) external { if (_assignments.length == 0) revert ZeroArgument("_assignments"); From 214af41cc6ea0f3d6d9879f4829658b4bec06123 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:57:08 +0500 Subject: [PATCH 35/65] fix: shorten name to fit the line --- contracts/0.8.25/vaults/Permissions.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index f90b30689..99b8952eb 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -53,13 +53,12 @@ abstract contract Permissions is AccessControlConfirmable { /** * @notice Permission for pausing beacon chain deposits on the StakingVault. */ - bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseBeaconChainDeposits"); + bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseDeposits"); /** * @notice Permission for resuming beacon chain deposits on the StakingVault. */ - bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = - keccak256("vaults.Permissions.ResumeBeaconChainDeposits"); + bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.ResumeDeposits"); /** * @notice Permission for requesting validator exit from the StakingVault. From 6bb0fcf9b59dba1f53ccc4317e173d856cb60226 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:58:57 +0500 Subject: [PATCH 36/65] feat: add comment --- contracts/0.8.25/vaults/Permissions.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 99b8952eb..2e7671892 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -104,6 +104,10 @@ abstract contract Permissions is AccessControlConfirmable { emit Initialized(_defaultAdmin); } + /** + * @notice Returns the address of the underlying StakingVault. + * @return The address of the StakingVault. + */ function stakingVault() public view returns (IStakingVault) { return IStakingVault(_loadStakingVaultAddress()); } From fae844323ee9c3ac44b5cb3890eba1b6ee13cbaa Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 12:59:14 +0500 Subject: [PATCH 37/65] feat: add behavior comment --- contracts/0.8.25/vaults/Permissions.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 2e7671892..1f13d3dc0 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -132,6 +132,7 @@ abstract contract Permissions is AccessControlConfirmable { * @notice Mass-revokes multiple roles from multiple accounts. * @param _assignments An array of role assignments. * @dev Performs the role admin checks internally. + * @dev If an account is not a member of a role, doesn't revert, emits no events. */ function revokeRoles(RoleAssignment[] memory _assignments) external { if (_assignments.length == 0) revert ZeroArgument("_assignments"); From d4a71e24bab8603f60dea58616caefd53d694a14 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Wed, 12 Feb 2025 08:23:01 +0000 Subject: [PATCH 38/65] chore: refactor --- tasks/logger.ts | 254 +++++++++++++++++++++--------------------------- 1 file changed, 113 insertions(+), 141 deletions(-) diff --git a/tasks/logger.ts b/tasks/logger.ts index ccb3a360e..ecd0c75e8 100644 --- a/tasks/logger.ts +++ b/tasks/logger.ts @@ -2,22 +2,14 @@ import "hardhat/types/runtime"; import chalk from "chalk"; import { formatUnits, Interface, TransactionReceipt, TransactionResponse } from "ethers"; import { extendEnvironment } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { HardhatNetworkConfig, HardhatRuntimeEnvironment } from "hardhat/types"; const LOG_LEVEL = process.env.LOG_LEVEL || "info"; +const DEFAULT_BLOCK_GAS_LIMIT = 30_000_000; +const FUNCTION_SIGNATURE_LENGTH = 10; -// Custom errors -class NoReceiptError extends Error { - constructor() { - super("Transaction receipt not found"); - } -} - -// Types -interface FunctionDetails { - name?: string; - functionName?: string; -} +const interfaceCache = new Map(); +const callCache = new Map(); enum TransactionType { CONTRACT_DEPLOYMENT = "Contract deployment", @@ -25,162 +17,142 @@ enum TransactionType { CONTRACT_CALL = "Contract call", } -// Constants -const DEFAULT_BLOCK_GAS_LIMIT = 30_000_000; -const FUNCTION_SIGNATURE_LENGTH = 10; - -// Cache for contract interfaces and function details -const interfaceCache = new Map(); -const functionDetailsCache = new Map(); +type Call = { + contract: string; + function: string; +}; -// Helper functions -function formatGasUsage(gasUsed: bigint, blockGasLimit: number): string { - const gasUsedPercent = (Number(gasUsed) * 100) / blockGasLimit; - return `${gasUsed} (${gasUsedPercent.toFixed(2)}%)`; -} - -function formatTransactionLines( +function outputTransaction( tx: TransactionResponse, + txType: TransactionType, receipt: TransactionReceipt, - txType: string, - name: string | undefined, - functionName: string | undefined, - blockGasLimit: number, + call: Call, + gasLimit: number, gasPrice: string, -): string[] { - const lines = [ - `Transaction sent: ${chalk.yellow(receipt.hash)}`, - ` From: ${chalk.cyan(tx.from)} To: ${chalk.cyan(tx.to || receipt.contractAddress)}`, - ` Gas price: ${chalk.yellow(gasPrice)} gwei Gas limit: ${chalk.yellow(blockGasLimit)} Gas used: ${chalk.yellow(formatGasUsage(receipt.gasUsed, blockGasLimit))}`, - ` Block: ${chalk.yellow(receipt.blockNumber)} Nonce: ${chalk.yellow(tx.nonce)}`, - ]; - - const color = receipt.status ? chalk.green : chalk.red; - const status = receipt.status ? "confirmed" : "failed"; +): void { + const gasUsedPercent = (Number(receipt.gasUsed) * 100) / gasLimit; + + const txHash = chalk.yellow(receipt.hash); + const txFrom = chalk.cyan(tx.from); + const txTo = chalk.cyan(tx.to || receipt.contractAddress); + const txGasPrice = chalk.yellow(gasPrice); + const txGasLimit = chalk.yellow(gasLimit); + const txGasUsed = chalk.yellow(`${receipt.gasUsed} (${gasUsedPercent.toFixed(2)}%)`); + const txBlock = chalk.yellow(receipt.blockNumber); + const txNonce = chalk.yellow(tx.nonce); + const txStatus = receipt.status ? chalk.green("confirmed") : chalk.red("failed"); + const txContract = chalk.cyan(call.contract || "Contract deployment"); + const txFunction = chalk.cyan(call.function || ""); + const txCall = `${txContract}.${txFunction}`; + + console.log(`Transaction sent: ${txHash}`); + console.log(` From: ${txFrom} To: ${txTo}`); + console.log(` Gas price: ${txGasPrice} gwei Gas limit: ${txGasLimit} Gas used: ${txGasUsed}`); + console.log(` Block: ${txBlock} Nonce: ${txNonce}`); if (txType === TransactionType.CONTRACT_DEPLOYMENT) { - lines.push(` Contract address: ${chalk.cyan(receipt.contractAddress)}`); - lines.push(` ${color(name || "Contract deployment")} ${color(status)}`); + console.log(` Contract deployed: ${chalk.cyan(receipt.contractAddress)}`); } else if (txType === TransactionType.ETH_TRANSFER) { - lines.push(` ETH transfer: ${chalk.cyan(tx.value)}`); - lines.push(` ${color("ETH transfer")} ${color(status)}`); + console.log(` ETH transfer: ${chalk.yellow(tx.value)}`); } else { - const txName = name && functionName ? `${name}.${functionName}` : functionName || "Contract call"; - lines.push(` ${color(txName)} ${color(status)}`); + console.log(` ${txCall} ${txStatus}`); } - - return lines; + console.log(); } -extendEnvironment((hre: HardhatRuntimeEnvironment) => { - if (LOG_LEVEL != "debug" && LOG_LEVEL != "all") return; +// Transaction Processing +async function getCall(tx: TransactionResponse, hre: HardhatRuntimeEnvironment): Promise { + if (!tx.data || tx.data === "0x" || !tx.to) return { contract: "", function: "" }; - const originalSendTransaction = hre.ethers.provider.send; + const cacheKey = `${tx.to}-${tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH)}`; + if (callCache.has(cacheKey)) { + return callCache.get(cacheKey)!; + } - // Wrap the provider's send method to intercept transactions - hre.ethers.provider.send = async function (method: string, params: unknown[]) { - const result = await originalSendTransaction.apply(this, [method, params]); + try { + const call = await extractCallDetails(tx, hre); + callCache.set(cacheKey, call); + return call; + } catch (error) { + console.warn("Error getting call details:", error); + const fallbackCall = { contract: tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH), function: "" }; + callCache.set(cacheKey, fallbackCall); + return fallbackCall; + } +} - // Only log eth_sendTransaction and eth_sendRawTransaction calls - if (method === "eth_sendTransaction" || method === "eth_sendRawTransaction") { - const tx = (await this.getTransaction(result)) as TransactionResponse; - await logTransaction(tx); +async function extractCallDetails(tx: TransactionResponse, hre: HardhatRuntimeEnvironment): Promise { + try { + const artifacts = await hre.artifacts.getAllFullyQualifiedNames(); + for (const name of artifacts) { + const iface = await getOrCreateInterface(name, hre); + const result = iface.parseTransaction({ data: tx.data }); + if (result) { + return { + contract: name.split(":").pop() || "", + function: result.name || "", + }; + } } + } catch { + // Ignore errors and return empty call + } - return result; - }; + return { contract: "", function: "" }; +} - async function getFunctionDetails(tx: TransactionResponse): Promise { - if (!tx.data || tx.data === "0x" || !tx.to) return {}; +async function getOrCreateInterface(artifactName: string, hre: HardhatRuntimeEnvironment) { + if (interfaceCache.has(artifactName)) { + return interfaceCache.get(artifactName)!; + } - // Check cache first - const cacheKey = `${tx.to}-${tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH)}`; - if (functionDetailsCache.has(cacheKey)) { - return functionDetailsCache.get(cacheKey)!; - } + const artifact = await hre.artifacts.readArtifact(artifactName); + const iface = new Interface(artifact.abi); + interfaceCache.set(artifactName, iface); + return iface; +} - try { - // Try to get contract name and function name from all available artifacts - const allArtifacts = await hre.artifacts.getAllFullyQualifiedNames(); - - for (const artifactName of allArtifacts) { - try { - let iface: Interface; - - // Check interface cache - if (interfaceCache.has(artifactName)) { - iface = interfaceCache.get(artifactName)!; - } else { - const artifact = await hre.artifacts.readArtifact(artifactName); - iface = new Interface(artifact.abi); - interfaceCache.set(artifactName, iface); - } - - const result = iface.parseTransaction({ data: tx.data }); - - if (result) { - const details = { - name: artifactName.split(":").pop() || "", - functionName: result.name, - }; - functionDetailsCache.set(cacheKey, details); - return details; - } - } catch { - continue; // Skip artifacts that can't be parsed - } - } - } catch (error) { - console.warn("Error getting function details:", error); - } +async function getTxType(tx: TransactionResponse, receipt: TransactionReceipt): Promise { + if (receipt.contractAddress) return TransactionType.CONTRACT_DEPLOYMENT; + if (!tx.data || tx.data === "0x") return TransactionType.ETH_TRANSFER; + return TransactionType.CONTRACT_CALL; +} - // Cache and return function signature if we can't decode - const details = { - functionName: tx.data.slice(0, FUNCTION_SIGNATURE_LENGTH), - }; - functionDetailsCache.set(cacheKey, details); - return details; - } +async function logTransaction(tx: TransactionResponse, hre: HardhatRuntimeEnvironment) { + const receipt = await tx.wait(); + if (!receipt) throw new Error("Transaction receipt not found"); - async function logTransaction(tx: TransactionResponse): Promise { - const receipt = await tx.wait(); - if (!receipt) { - throw new NoReceiptError(); - } + try { + const network = await tx.provider.getNetwork(); + const config = hre.config.networks[network.name] as HardhatNetworkConfig; + const gasLimit = config.blockGasLimit ?? DEFAULT_BLOCK_GAS_LIMIT; - try { - const network = await tx.provider.getNetwork(); - const config = hre.config.networks[network.name]; - const blockGasLimit = "blockGasLimit" in config ? config.blockGasLimit : DEFAULT_BLOCK_GAS_LIMIT; + const txType = await getTxType(tx, receipt); + const call = await getCall(tx, hre); + const gasPrice = formatUnits(receipt.gasPrice || 0n, "gwei"); - const txType = await getTxType(tx, receipt); - const { name, functionName } = await getFunctionDetails(tx); - const gasPrice = formatUnits(receipt.gasPrice || 0n, "gwei"); + outputTransaction(tx, txType, receipt, call, gasLimit, gasPrice); - const lines = formatTransactionLines(tx, receipt, txType, name, functionName, blockGasLimit, gasPrice); + return receipt; + } catch (error) { + console.error("Error logging transaction:", error); + return receipt; + } +} - lines.forEach((line) => console.log(line)); - console.log(); +extendEnvironment((hre: HardhatRuntimeEnvironment) => { + if (LOG_LEVEL != "debug" && LOG_LEVEL != "all") return; - return receipt; - } catch (error) { - console.error("Error logging transaction:", error); - return receipt; - } - } + const originalSendTransaction = hre.ethers.provider.send; - async function getTxType(tx: TransactionResponse, receipt: TransactionReceipt): Promise { - if (receipt.contractAddress) { - return TransactionType.CONTRACT_DEPLOYMENT; - } + hre.ethers.provider.send = async function (method: string, params: unknown[]) { + const result = await originalSendTransaction.apply(this, [method, params]); - if (!tx.data || tx.data === "0x") { - return TransactionType.ETH_TRANSFER; + if (method === "eth_sendTransaction" || method === "eth_sendRawTransaction") { + const tx = (await this.getTransaction(result)) as TransactionResponse; + await logTransaction(tx, hre); } - const { name, functionName } = await getFunctionDetails(tx); - return name && functionName ? `${name}.${functionName}` : functionName || TransactionType.CONTRACT_CALL; - } - - return logTransaction; + return result; + }; }); From f627871fac3fae40eda4c28158c99dd925b4d34c Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 13:46:47 +0500 Subject: [PATCH 39/65] feat(Permissions): cover with comments --- contracts/0.8.25/vaults/Permissions.sol | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 1f13d3dc0..c94357c63 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -142,52 +142,107 @@ abstract contract Permissions is AccessControlConfirmable { } } + /** + * @dev Returns an array of roles that need to confirm the call + * used for the `onlyConfirmed` modifier. + * At this level, only the DEFAULT_ADMIN_ROLE is needed to confirm the call + * but in inherited contracts, the function can be overridden to add more roles, + * which are introduced further in the inheritance chain. + * @return The roles that need to confirm the call. + */ function _confirmingRoles() internal pure virtual returns (bytes32[] memory) { bytes32[] memory roles = new bytes32[](1); roles[0] = DEFAULT_ADMIN_ROLE; return roles; } + /** + * @dev Checks the FUND_ROLE and funds the StakingVault. + * @param _ether The amount of ether to fund the StakingVault with. + */ function _fund(uint256 _ether) internal onlyRole(FUND_ROLE) { stakingVault().fund{value: _ether}(); } + /** + * @dev Checks the WITHDRAW_ROLE and withdraws funds from the StakingVault. + * @param _recipient The address to withdraw the funds to. + * @param _ether The amount of ether to withdraw from the StakingVault. + * @dev The zero checks for recipient and ether are performed in the StakingVault contract. + */ function _withdraw(address _recipient, uint256 _ether) internal virtual onlyRole(WITHDRAW_ROLE) { stakingVault().withdraw(_recipient, _ether); } + /** + * @dev Checks the MINT_ROLE and mints shares backed by the StakingVault. + * @param _recipient The address to mint the shares to. + * @param _shares The amount of shares to mint. + * @dev The zero checks for parameters are performed in the VaultHub contract. + */ function _mintShares(address _recipient, uint256 _shares) internal onlyRole(MINT_ROLE) { vaultHub.mintSharesBackedByVault(address(stakingVault()), _recipient, _shares); } + /** + * @dev Checks the BURN_ROLE and burns shares backed by the StakingVault. + * @param _shares The amount of shares to burn. + * @dev The zero check for parameters is performed in the VaultHub contract. + */ function _burnShares(uint256 _shares) internal onlyRole(BURN_ROLE) { vaultHub.burnSharesBackedByVault(address(stakingVault()), _shares); } + /** + * @dev Checks the REBALANCE_ROLE and rebalances the StakingVault. + * @param _ether The amount of ether to rebalance the StakingVault with. + * @dev The zero check for parameters is performed in the StakingVault contract. + */ function _rebalanceVault(uint256 _ether) internal onlyRole(REBALANCE_ROLE) { stakingVault().rebalance(_ether); } + /** + * @dev Checks the PAUSE_BEACON_CHAIN_DEPOSITS_ROLE and pauses beacon chain deposits on the StakingVault. + */ function _pauseBeaconChainDeposits() internal onlyRole(PAUSE_BEACON_CHAIN_DEPOSITS_ROLE) { stakingVault().pauseBeaconChainDeposits(); } + /** + * @dev Checks the RESUME_BEACON_CHAIN_DEPOSITS_ROLE and resumes beacon chain deposits on the StakingVault. + */ function _resumeBeaconChainDeposits() internal onlyRole(RESUME_BEACON_CHAIN_DEPOSITS_ROLE) { stakingVault().resumeBeaconChainDeposits(); } + /** + * @dev Checks the REQUEST_VALIDATOR_EXIT_ROLE and requests validator exit on the StakingVault. + * @param _pubkey The public key of the validator to request exit for. + */ function _requestValidatorExit(bytes calldata _pubkey) internal onlyRole(REQUEST_VALIDATOR_EXIT_ROLE) { stakingVault().requestValidatorExit(_pubkey); } + /** + * @dev Checks the VOLUNTARY_DISCONNECT_ROLE and voluntarily disconnects the StakingVault. + */ function _voluntaryDisconnect() internal onlyRole(VOLUNTARY_DISCONNECT_ROLE) { vaultHub.voluntaryDisconnect(address(stakingVault())); } + /** + * @dev Checks the DEFAULT_ADMIN_ROLE and transfers the StakingVault ownership. + * @param _newOwner The address to transfer the StakingVault ownership to. + */ function _transferStakingVaultOwnership(address _newOwner) internal onlyConfirmed(_confirmingRoles()) { OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner); } + /** + * @dev Loads the address of the underlying StakingVault. + * @return addr The address of the StakingVault. + */ function _loadStakingVaultAddress() internal view returns (address addr) { bytes memory args = Clones.fetchCloneArgs(address(this)); assembly { From 5d905492eca18ac4a2c74c32082ad5d02fd57036 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 13:53:25 +0500 Subject: [PATCH 40/65] fix: update some naming --- contracts/0.8.25/vaults/Dashboard.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 245423cda..1fdfd9d97 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -30,9 +30,9 @@ interface IWstETH is IERC20, IERC20Permit { /** * @title Dashboard - * @notice This contract is meant to be used as the owner of `StakingVault`. - * This contract improves the vault UX by bundling all functions from the vault and vault hub - * in this single contract. It provides administrative functions for managing the staking vault, + * @notice This contract is a UX-layer for `StakingVault`. + * This contract improves the vault UX by bundling all functions from the StakingVault and VaultHub + * in this single contract. It provides administrative functions for managing the StakingVault, * including funding, withdrawing, minting, burning, and rebalancing operations. */ contract Dashboard is Permissions { @@ -99,6 +99,10 @@ contract Dashboard is Permissions { // ==================== View Functions ==================== + /** + * @notice Returns the roles that need to confirm multi-role operations. + * @return The roles that need to confirm the call. + */ function confirmingRoles() external pure returns (bytes32[] memory) { return _confirmingRoles(); } @@ -147,7 +151,7 @@ contract Dashboard is Permissions { * @notice Returns the treasury fee basis points. * @return The treasury fee in basis points as a uint16. */ - function treasuryFee() external view returns (uint16) { + function treasuryFeeBP() external view returns (uint16) { return vaultSocket().treasuryFeeBP; } From 9e6ad2163e7dc13d368931e7088d9953c8a0000f Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 17:49:35 +0500 Subject: [PATCH 41/65] fix(VaultFactory): role names --- contracts/0.8.25/vaults/VaultFactory.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 209a4ea4f..bb85c2d51 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -24,8 +24,8 @@ struct DelegationConfig { address curatorFeeSetter; address curatorFeeClaimer; address nodeOperatorManager; - address nodeOperatorFeeConfirm; - address nodeOperatorFeeClaim; + address nodeOperatorFeeConfirmer; + address nodeOperatorFeeClaimer; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; uint256 confirmLifetime; @@ -85,8 +85,8 @@ contract VaultFactory { delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), _delegationConfig.curatorFeeSetter); delegation.grantRole(delegation.CURATOR_FEE_CLAIM_ROLE(), _delegationConfig.curatorFeeClaimer); delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), _delegationConfig.nodeOperatorManager); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), _delegationConfig.nodeOperatorFeeClaim); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), _delegationConfig.nodeOperatorFeeConfirm); + delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), _delegationConfig.nodeOperatorFeeClaimer); + delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), _delegationConfig.nodeOperatorFeeConfirmer); // grant temporary roles to factory for setting fees delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); From 48ddb15bf6619b8473964cce00a0e26bbd95cfa1 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 17:50:07 +0500 Subject: [PATCH 42/65] fix(ACLConfirmable): log expiry timestamp --- contracts/0.8.25/utils/AccessControlConfirmable.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index 4ec521085..36786f143 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -79,6 +79,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { uint256 numberOfConfirms = 0; bool[] memory deferredConfirms = new bool[](numberOfRoles); bool isRoleMember = false; + uint256 expiryTimestamp = block.timestamp + confirmLifetime; for (uint256 i = 0; i < numberOfRoles; ++i) { bytes32 role = _roles[i]; @@ -88,7 +89,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { numberOfConfirms++; deferredConfirms[i] = true; - emit RoleMemberConfirmed(msg.sender, role, block.timestamp, msg.data); + emit RoleMemberConfirmed(msg.sender, role, expiryTimestamp, msg.data); } else if (confirmations[msg.data][role] >= block.timestamp) { numberOfConfirms++; } @@ -106,7 +107,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { for (uint256 i = 0; i < numberOfRoles; ++i) { if (deferredConfirms[i]) { bytes32 role = _roles[i]; - confirmations[msg.data][role] = block.timestamp + confirmLifetime; + confirmations[msg.data][role] = expiryTimestamp; } } } @@ -138,10 +139,10 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * @dev Emitted when a role member confirms. * @param member The address of the confirming member. * @param role The role of the confirming member. - * @param timestamp The timestamp of the confirmation. + * @param expiryTimestamp The timestamp of the confirmation. * @param data The msg.data of the confirmation (selector + arguments). */ - event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 timestamp, bytes data); + event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 expiryTimestamp, bytes data); /** * @dev Thrown when attempting to set confirmation lifetime to zero. From d5b5f0b825bc1f50e4e4066fff90749bec98d036 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 12 Feb 2025 17:51:05 +0500 Subject: [PATCH 43/65] fix(tests): update delegation tests --- .../vaults/delegation/delegation.test.ts | 199 ++++++++++-------- 1 file changed, 108 insertions(+), 91 deletions(-) diff --git a/test/0.8.25/vaults/delegation/delegation.test.ts b/test/0.8.25/vaults/delegation/delegation.test.ts index 7b4651a2b..97eab50fc 100644 --- a/test/0.8.25/vaults/delegation/delegation.test.ts +++ b/test/0.8.25/vaults/delegation/delegation.test.ts @@ -36,9 +36,12 @@ describe("Delegation.sol", () => { let depositResumer: HardhatEthersSigner; let exitRequester: HardhatEthersSigner; let disconnecter: HardhatEthersSigner; - let curator: HardhatEthersSigner; + let curatorFeeSetter: HardhatEthersSigner; + let curatorFeeClaimer: HardhatEthersSigner; let nodeOperatorManager: HardhatEthersSigner; + let nodeOperatorFeeConfirmer: HardhatEthersSigner; let nodeOperatorFeeClaimer: HardhatEthersSigner; + let stranger: HardhatEthersSigner; let beaconOwner: HardhatEthersSigner; let hubSigner: HardhatEthersSigner; @@ -72,8 +75,10 @@ describe("Delegation.sol", () => { depositResumer, exitRequester, disconnecter, - curator, + curatorFeeSetter, + curatorFeeClaimer, nodeOperatorManager, + nodeOperatorFeeConfirmer, nodeOperatorFeeClaimer, stranger, beaconOwner, @@ -114,11 +119,14 @@ describe("Delegation.sol", () => { depositResumer, exitRequester, disconnecter, - curator, + curatorFeeSetter, + curatorFeeClaimer, nodeOperatorManager, + nodeOperatorFeeConfirmer, nodeOperatorFeeClaimer, curatorFeeBP: 0n, nodeOperatorFeeBP: 0n, + confirmLifetime: days(7n), }, "0x", ); @@ -173,13 +181,16 @@ describe("Delegation.sol", () => { context("initialize", () => { it("reverts if already initialized", async () => { - await expect(delegation.initialize(vaultOwner)).to.be.revertedWithCustomError(delegation, "AlreadyInitialized"); + await expect(delegation.initialize(vaultOwner, days(7n))).to.be.revertedWithCustomError( + delegation, + "AlreadyInitialized", + ); }); it("reverts if called on the implementation", async () => { const delegation_ = await ethers.deployContract("Delegation", [weth, lidoLocator]); - await expect(delegation_.initialize(vaultOwner)).to.be.revertedWithCustomError( + await expect(delegation_.initialize(vaultOwner, days(7n))).to.be.revertedWithCustomError( delegation_, "NonProxyCallsForbidden", ); @@ -204,9 +215,11 @@ describe("Delegation.sol", () => { await assertSoleMember(depositResumer, await delegation.RESUME_BEACON_CHAIN_DEPOSITS_ROLE()); await assertSoleMember(exitRequester, await delegation.REQUEST_VALIDATOR_EXIT_ROLE()); await assertSoleMember(disconnecter, await delegation.VOLUNTARY_DISCONNECT_ROLE()); - await assertSoleMember(curator, await delegation.CURATOR_ROLE()); + await assertSoleMember(curatorFeeSetter, await delegation.CURATOR_FEE_SET_ROLE()); + await assertSoleMember(curatorFeeClaimer, await delegation.CURATOR_FEE_CLAIM_ROLE()); await assertSoleMember(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE()); - await assertSoleMember(nodeOperatorFeeClaimer, await delegation.NODE_OPERATOR_FEE_CLAIMER_ROLE()); + await assertSoleMember(nodeOperatorFeeConfirmer, await delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE()); + await assertSoleMember(nodeOperatorFeeClaimer, await delegation.NODE_OPERATOR_FEE_CLAIM_ROLE()); expect(await delegation.curatorFeeBP()).to.equal(0n); expect(await delegation.nodeOperatorFeeBP()).to.equal(0n); @@ -217,41 +230,41 @@ describe("Delegation.sol", () => { }); }); - context("votingCommittee", () => { + context("confirmingRoles", () => { it("returns the correct roles", async () => { - expect(await delegation.votingCommittee()).to.deep.equal([ - await delegation.CURATOR_ROLE(), + expect(await delegation.confirmingRoles()).to.deep.equal([ + await delegation.DEFAULT_ADMIN_ROLE(), await delegation.NODE_OPERATOR_MANAGER_ROLE(), ]); }); }); - context("setVoteLifetime", () => { - it("reverts if the caller is not a member of the vote lifetime committee", async () => { - await expect(delegation.connect(stranger).setVoteLifetime(days(10n))).to.be.revertedWithCustomError( + context("setConfirmLifetime", () => { + it("reverts if the caller is not a member of the confirm lifetime committee", async () => { + await expect(delegation.connect(stranger).setConfirmLifetime(days(10n))).to.be.revertedWithCustomError( delegation, - "NotACommitteeMember", + "SenderNotMember", ); }); - it("sets the new vote lifetime", async () => { - const oldVoteLifetime = await delegation.voteLifetime(); - const newVoteLifetime = days(10n); - const msgData = delegation.interface.encodeFunctionData("setVoteLifetime", [newVoteLifetime]); - let voteTimestamp = await getNextBlockTimestamp(); + it("sets the new confirm lifetime", async () => { + const oldConfirmLifetime = await delegation.confirmLifetime(); + const newConfirmLifetime = days(10n); + const msgData = delegation.interface.encodeFunctionData("setConfirmLifetime", [newConfirmLifetime]); + let confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); - await expect(delegation.connect(curator).setVoteLifetime(newVoteLifetime)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(curator, await delegation.CURATOR_ROLE(), voteTimestamp, msgData); + await expect(delegation.connect(vaultOwner).setConfirmLifetime(newConfirmLifetime)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), confirmTimestamp, msgData); - voteTimestamp = await getNextBlockTimestamp(); - await expect(delegation.connect(nodeOperatorManager).setVoteLifetime(newVoteLifetime)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), voteTimestamp, msgData) - .and.to.emit(delegation, "VoteLifetimeSet") - .withArgs(nodeOperatorManager, oldVoteLifetime, newVoteLifetime); + confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + await expect(delegation.connect(nodeOperatorManager).setConfirmLifetime(newConfirmLifetime)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), confirmTimestamp, msgData) + .and.to.emit(delegation, "ConfirmLifetimeSet") + .withArgs(nodeOperatorManager, oldConfirmLifetime, newConfirmLifetime); - expect(await delegation.voteLifetime()).to.equal(newVoteLifetime); + expect(await delegation.confirmLifetime()).to.equal(newConfirmLifetime); }); }); @@ -259,25 +272,25 @@ describe("Delegation.sol", () => { it("reverts if the caller is not a member of the curator due claim role", async () => { await expect(delegation.connect(stranger).claimCuratorFee(stranger)) .to.be.revertedWithCustomError(delegation, "AccessControlUnauthorizedAccount") - .withArgs(stranger, await delegation.CURATOR_ROLE()); + .withArgs(stranger, await delegation.CURATOR_FEE_CLAIM_ROLE()); }); it("reverts if the recipient is the zero address", async () => { - await expect(delegation.connect(curator).claimCuratorFee(ethers.ZeroAddress)) + await expect(delegation.connect(curatorFeeClaimer).claimCuratorFee(ethers.ZeroAddress)) .to.be.revertedWithCustomError(delegation, "ZeroArgument") .withArgs("_recipient"); }); - it("reverts if the due is zero", async () => { + it("reverts if the fee is zero", async () => { expect(await delegation.curatorUnclaimedFee()).to.equal(0n); - await expect(delegation.connect(curator).claimCuratorFee(stranger)) + await expect(delegation.connect(curatorFeeClaimer).claimCuratorFee(stranger)) .to.be.revertedWithCustomError(delegation, "ZeroArgument") .withArgs("_fee"); }); - it("claims the due", async () => { + it("claims the fee", async () => { const curatorFee = 10_00n; // 10% - await delegation.connect(vaultOwner).setCuratorFeeBP(curatorFee); + await delegation.connect(curatorFeeSetter).setCuratorFeeBP(curatorFee); expect(await delegation.curatorFeeBP()).to.equal(curatorFee); const rewards = ether("1"); @@ -292,7 +305,7 @@ describe("Delegation.sol", () => { expect(await ethers.provider.getBalance(vault)).to.equal(rewards); expect(await ethers.provider.getBalance(recipient)).to.equal(0n); - await expect(delegation.connect(curator).claimCuratorFee(recipient)) + await expect(delegation.connect(curatorFeeClaimer).claimCuratorFee(recipient)) .to.emit(vault, "Withdrawn") .withArgs(delegation, recipient, expectedDue); expect(await ethers.provider.getBalance(recipient)).to.equal(expectedDue); @@ -324,7 +337,7 @@ describe("Delegation.sol", () => { it("claims the due", async () => { const operatorFee = 10_00n; // 10% await delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(operatorFee); - await delegation.connect(curator).setNodeOperatorFeeBP(operatorFee); + await delegation.connect(vaultOwner).setNodeOperatorFeeBP(operatorFee); expect(await delegation.nodeOperatorFeeBP()).to.equal(operatorFee); const rewards = ether("1"); @@ -509,13 +522,13 @@ describe("Delegation.sol", () => { it("reverts if caller is not curator", async () => { await expect(delegation.connect(stranger).setCuratorFeeBP(1000n)) .to.be.revertedWithCustomError(delegation, "AccessControlUnauthorizedAccount") - .withArgs(stranger, await delegation.DEFAULT_ADMIN_ROLE()); + .withArgs(stranger, await delegation.CURATOR_FEE_SET_ROLE()); }); it("reverts if curator fee is not zero", async () => { // set the curator fee to 5% const newCuratorFee = 500n; - await delegation.connect(vaultOwner).setCuratorFeeBP(newCuratorFee); + await delegation.connect(curatorFeeSetter).setCuratorFeeBP(newCuratorFee); expect(await delegation.curatorFeeBP()).to.equal(newCuratorFee); // bring rewards @@ -526,14 +539,14 @@ describe("Delegation.sol", () => { expect(await delegation.curatorUnclaimedFee()).to.equal((totalRewards * newCuratorFee) / BP_BASE); // attempt to change the performance fee to 6% - await expect(delegation.connect(vaultOwner).setCuratorFeeBP(600n)).to.be.revertedWithCustomError( + await expect(delegation.connect(curatorFeeSetter).setCuratorFeeBP(600n)).to.be.revertedWithCustomError( delegation, "CuratorFeeUnclaimed", ); }); it("reverts if new fee is greater than max fee", async () => { - await expect(delegation.connect(vaultOwner).setCuratorFeeBP(MAX_FEE + 1n)).to.be.revertedWithCustomError( + await expect(delegation.connect(curatorFeeSetter).setCuratorFeeBP(MAX_FEE + 1n)).to.be.revertedWithCustomError( delegation, "CombinedFeesExceed100Percent", ); @@ -541,7 +554,7 @@ describe("Delegation.sol", () => { it("sets the curator fee", async () => { const newCuratorFee = 1000n; - await delegation.connect(vaultOwner).setCuratorFeeBP(newCuratorFee); + await delegation.connect(curatorFeeSetter).setCuratorFeeBP(newCuratorFee); expect(await delegation.curatorFeeBP()).to.equal(newCuratorFee); }); }); @@ -549,7 +562,7 @@ describe("Delegation.sol", () => { context("setOperatorFee", () => { it("reverts if new fee is greater than max fee", async () => { const invalidFee = MAX_FEE + 1n; - await delegation.connect(curator).setNodeOperatorFeeBP(invalidFee); + await delegation.connect(vaultOwner).setNodeOperatorFeeBP(invalidFee); await expect( delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(invalidFee), @@ -559,7 +572,7 @@ describe("Delegation.sol", () => { it("reverts if performance due is not zero", async () => { // set the performance fee to 5% const newOperatorFee = 500n; - await delegation.connect(curator).setNodeOperatorFeeBP(newOperatorFee); + await delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee); await delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee); expect(await delegation.nodeOperatorFeeBP()).to.equal(newOperatorFee); @@ -571,39 +584,41 @@ describe("Delegation.sol", () => { expect(await delegation.nodeOperatorUnclaimedFee()).to.equal((totalRewards * newOperatorFee) / BP_BASE); // attempt to change the performance fee to 6% - await delegation.connect(curator).setNodeOperatorFeeBP(600n); + await delegation.connect(vaultOwner).setNodeOperatorFeeBP(600n); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(600n)).to.be.revertedWithCustomError( delegation, "NodeOperatorFeeUnclaimed", ); }); - it("requires both curator and operator to set the operator fee and emits the RoleMemberVoted event", async () => { + it("requires both default admin and operator manager to set the operator fee and emits the RoleMemberConfirmed event", async () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; - let voteTimestamp = await getNextBlockTimestamp(); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); - await expect(delegation.connect(curator).setNodeOperatorFeeBP(newOperatorFee)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(curator, await delegation.CURATOR_ROLE(), voteTimestamp, msgData); + await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData); // fee is unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); - // check vote - expect(await delegation.votings(keccak256(msgData), await delegation.CURATOR_ROLE())).to.equal(voteTimestamp); + // check confirm + expect(await delegation.confirmationExpiryTimestamp(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal( + expiryTimestamp, + ); - voteTimestamp = await getNextBlockTimestamp(); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), voteTimestamp, msgData) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData) .and.to.emit(delegation, "NodeOperatorFeeBPSet") .withArgs(nodeOperatorManager, previousOperatorFee, newOperatorFee); expect(await delegation.nodeOperatorFeeBP()).to.equal(newOperatorFee); - // resets the votes - for (const role of await delegation.votingCommittee()) { - expect(await delegation.votings(keccak256(msgData), role)).to.equal(0n); + // resets the confirms + for (const role of await delegation.confirmingRoles()) { + expect(await delegation.confirmationExpiryTimestamp(keccak256(msgData), role)).to.equal(0n); } }); @@ -611,46 +626,48 @@ describe("Delegation.sol", () => { const newOperatorFee = 1000n; await expect(delegation.connect(stranger).setNodeOperatorFeeBP(newOperatorFee)).to.be.revertedWithCustomError( delegation, - "NotACommitteeMember", + "SenderNotMember", ); }); - it("doesn't execute if an earlier vote has expired", async () => { + it("doesn't execute if an earlier confirm has expired", async () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); - const callId = keccak256(msgData); - let voteTimestamp = await getNextBlockTimestamp(); - await expect(delegation.connect(curator).setNodeOperatorFeeBP(newOperatorFee)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(curator, await delegation.CURATOR_ROLE(), voteTimestamp, msgData); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + + await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData); // fee is unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); - // check vote - expect(await delegation.votings(callId, await delegation.CURATOR_ROLE())).to.equal(voteTimestamp); + // check confirm + expect(await delegation.confirmationExpiryTimestamp(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal( + expiryTimestamp, + ); // move time forward await advanceChainTime(days(7n) + 1n); - const expectedVoteTimestamp = await getNextBlockTimestamp(); - expect(expectedVoteTimestamp).to.be.greaterThan(voteTimestamp + days(7n)); + const expectedExpiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + expect(expectedExpiryTimestamp).to.be.greaterThan(expiryTimestamp + days(7n)); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expectedVoteTimestamp, msgData); + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expectedExpiryTimestamp, msgData); // fee is still unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); - // check vote - expect(await delegation.votings(callId, await delegation.NODE_OPERATOR_MANAGER_ROLE())).to.equal( - expectedVoteTimestamp, - ); - - // curator has to vote again - voteTimestamp = await getNextBlockTimestamp(); - await expect(delegation.connect(curator).setNodeOperatorFeeBP(newOperatorFee)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(curator, await delegation.CURATOR_ROLE(), voteTimestamp, msgData) + // check confirm + expect( + await delegation.confirmationExpiryTimestamp(msgData, await delegation.NODE_OPERATOR_MANAGER_ROLE()), + ).to.equal(expectedExpiryTimestamp); + + // curator has to confirm again + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData) .and.to.emit(delegation, "NodeOperatorFeeBPSet") - .withArgs(curator, previousOperatorFee, newOperatorFee); + .withArgs(vaultOwner, previousOperatorFee, newOperatorFee); // fee is now changed expect(await delegation.nodeOperatorFeeBP()).to.equal(newOperatorFee); }); @@ -660,24 +677,24 @@ describe("Delegation.sol", () => { it("reverts if the caller is not a member of the transfer committee", async () => { await expect(delegation.connect(stranger).transferStakingVaultOwnership(recipient)).to.be.revertedWithCustomError( delegation, - "NotACommitteeMember", + "SenderNotMember", ); }); - it("requires both curator and operator to transfer ownership and emits the RoleMemberVoted event", async () => { + it("requires both curator and operator to transfer ownership and emits the RoleMemberConfirmd event", async () => { const newOwner = certainAddress("newOwner"); const msgData = delegation.interface.encodeFunctionData("transferStakingVaultOwnership", [newOwner]); - let voteTimestamp = await getNextBlockTimestamp(); - await expect(delegation.connect(curator).transferStakingVaultOwnership(newOwner)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(curator, await delegation.CURATOR_ROLE(), voteTimestamp, msgData); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + await expect(delegation.connect(vaultOwner).transferStakingVaultOwnership(newOwner)) + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData); // owner is unchanged expect(await vault.owner()).to.equal(delegation); - voteTimestamp = await getNextBlockTimestamp(); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); await expect(delegation.connect(nodeOperatorManager).transferStakingVaultOwnership(newOwner)) - .to.emit(delegation, "RoleMemberVoted") - .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), voteTimestamp, msgData); + .to.emit(delegation, "RoleMemberConfirmed") + .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData); // owner changed expect(await vault.owner()).to.equal(newOwner); }); From a286dc1b8a56627e53eaf6867943af9bf8d682d9 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 13 Feb 2025 13:18:04 +0500 Subject: [PATCH 44/65] fix: prevent 0 lifetime situations --- .../0.8.25/utils/AccessControlConfirmable.sol | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index 36786f143..5e9b3aee6 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -15,25 +15,27 @@ import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/exten abstract contract AccessControlConfirmable is AccessControlEnumerable { /** * @notice Tracks confirmations - * - callId: unique identifier for the call, derived as `keccak256(msg.data)` + * - callData: msg.data of the call (selector + arguments) * - role: role that confirmed the action - * - timestamp: timestamp of the confirmation. + * - expiryTimestamp: timestamp of the confirmation. */ mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; /** * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. + * @dev We cannot set this to 0 because this means that all confirmations have to be in the same block, + * which can never be guaranteed. And, more importantly, if the `_setLifetime` is restricted by + * the `onlyConfirmed` modifier, the confirmation lifetime will be tricky to change. + * This is why this variable is private, set to a default value of 1 day and cannot be set to 0. */ - uint256 public confirmLifetime; + uint256 private confirmLifetime = 1 days; /** - * @notice Returns the expiry timestamp of the confirmation for a given call and role. - * @param _callData The call data of the function. - * @param _role The role that confirmed the call. - * @return The expiry timestamp of the confirmation. + * @notice Returns the confirmation lifetime. + * @return The confirmation lifetime in seconds. */ - function confirmationExpiryTimestamp(bytes calldata _callData, bytes32 _role) public view returns (uint256) { - return confirmations[_callData][_role]; + function getConfirmLifetime() public view returns (uint256) { + return confirmLifetime; } /** @@ -73,7 +75,6 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { */ modifier onlyConfirmed(bytes32[] memory _roles) { if (_roles.length == 0) revert ZeroConfirmingRoles(); - if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); uint256 numberOfRoles = _roles.length; uint256 numberOfConfirms = 0; @@ -114,9 +115,10 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { } /** - * @notice Sets the confirmation lifetime. - * Confirmation lifetime is a period during which the confirmation is counted. Once the period is over, - * the confirmation is considered expired, no longer counts and must be recasted for the confirmation to go through. + * @dev Sets the confirmation lifetime. + * Confirmation lifetime is a period during which the confirmation is counted. Once expired, + * the confirmation no longer counts and must be recasted for the confirmation to go through. + * @dev Does not retroactively apply to existing confirmations. * @param _newConfirmLifetime The new confirmation lifetime in seconds. */ function _setConfirmLifetime(uint256 _newConfirmLifetime) internal { From 0c2f6abee11aced90f73b60ea9a508b3391d3960 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 13 Feb 2025 14:58:28 +0500 Subject: [PATCH 45/65] feat: add lifetime bounds --- .../0.8.25/utils/AccessControlConfirmable.sol | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index 5e9b3aee6..1f80d37da 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -21,6 +21,16 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { */ mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; + /** + * @notice Minimal confirmation lifetime in seconds. + */ + uint256 public constant MIN_CONFIRM_LIFETIME = 1 days; + + /** + * @notice Maximal confirmation lifetime in seconds. + */ + uint256 public constant MAX_CONFIRM_LIFETIME = 30 days; + /** * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. * @dev We cannot set this to 0 because this means that all confirmations have to be in the same block, @@ -28,7 +38,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * the `onlyConfirmed` modifier, the confirmation lifetime will be tricky to change. * This is why this variable is private, set to a default value of 1 day and cannot be set to 0. */ - uint256 private confirmLifetime = 1 days; + uint256 private confirmLifetime = MIN_CONFIRM_LIFETIME; /** * @notice Returns the confirmation lifetime. @@ -122,7 +132,8 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * @param _newConfirmLifetime The new confirmation lifetime in seconds. */ function _setConfirmLifetime(uint256 _newConfirmLifetime) internal { - if (_newConfirmLifetime == 0) revert ConfirmLifetimeCannotBeZero(); + if (_newConfirmLifetime < MIN_CONFIRM_LIFETIME || _newConfirmLifetime > MAX_CONFIRM_LIFETIME) + revert ConfirmLifetimeOutOfBounds(); uint256 oldConfirmLifetime = confirmLifetime; confirmLifetime = _newConfirmLifetime; @@ -147,14 +158,9 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 expiryTimestamp, bytes data); /** - * @dev Thrown when attempting to set confirmation lifetime to zero. - */ - error ConfirmLifetimeCannotBeZero(); - - /** - * @dev Thrown when attempting to confirm when the confirmation lifetime is not set. + * @dev Thrown when attempting to set confirmation lifetime out of bounds. */ - error ConfirmLifetimeNotSet(); + error ConfirmLifetimeOutOfBounds(); /** * @dev Thrown when a caller without a required role attempts to confirm. From a09e964df6308148383d9b2f3d2c03ec5a04b985 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Thu, 13 Feb 2025 15:00:07 +0500 Subject: [PATCH 46/65] test(ACLConfirmable): full coverage --- .../utils/access-control-confirmable.test.ts | 127 ++++++++++++++++++ .../AccessControlConfirmable__Harness.sol | 36 +++++ 2 files changed, 163 insertions(+) create mode 100644 test/0.8.25/utils/access-control-confirmable.test.ts create mode 100644 test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol diff --git a/test/0.8.25/utils/access-control-confirmable.test.ts b/test/0.8.25/utils/access-control-confirmable.test.ts new file mode 100644 index 000000000..7b0e2357d --- /dev/null +++ b/test/0.8.25/utils/access-control-confirmable.test.ts @@ -0,0 +1,127 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { AccessControlConfirmable__Harness } from "typechain-types"; + +import { advanceChainTime, days, getNextBlockTimestamp } from "lib"; + +describe("AccessControlConfirmable.sol", () => { + let harness: AccessControlConfirmable__Harness; + let admin: HardhatEthersSigner; + let role1Member: HardhatEthersSigner; + let role2Member: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + before(async () => { + [admin, stranger, role1Member, role2Member] = await ethers.getSigners(); + + harness = await ethers.deployContract("AccessControlConfirmable__Harness", [admin], admin); + expect(await harness.getConfirmLifetime()).to.equal(await harness.MIN_CONFIRM_LIFETIME()); + expect(await harness.hasRole(await harness.DEFAULT_ADMIN_ROLE(), admin)).to.be.true; + expect(await harness.getRoleMemberCount(await harness.DEFAULT_ADMIN_ROLE())).to.equal(1); + + await harness.grantRole(await harness.ROLE_1(), role1Member); + expect(await harness.hasRole(await harness.ROLE_1(), role1Member)).to.be.true; + expect(await harness.getRoleMemberCount(await harness.ROLE_1())).to.equal(1); + + await harness.grantRole(await harness.ROLE_2(), role2Member); + expect(await harness.hasRole(await harness.ROLE_2(), role2Member)).to.be.true; + expect(await harness.getRoleMemberCount(await harness.ROLE_2())).to.equal(1); + }); + + context("constants", () => { + it("returns the correct constants", async () => { + expect(await harness.MIN_CONFIRM_LIFETIME()).to.equal(days(1n)); + expect(await harness.MAX_CONFIRM_LIFETIME()).to.equal(days(30n)); + }); + }); + + context("getConfirmLifetime()", () => { + it("returns the minimal lifetime initially", async () => { + expect(await harness.getConfirmLifetime()).to.equal(await harness.MIN_CONFIRM_LIFETIME()); + }); + }); + + context("confirmingRoles()", () => { + it("should return the correct roles", async () => { + expect(await harness.confirmingRoles()).to.deep.equal([await harness.ROLE_1(), await harness.ROLE_2()]); + }); + }); + + context("setConfirmLifetime()", () => { + it("sets the confirm lifetime", async () => { + const oldLifetime = await harness.getConfirmLifetime(); + const newLifetime = days(14n); + await expect(harness.setConfirmLifetime(newLifetime)) + .to.emit(harness, "ConfirmLifetimeSet") + .withArgs(admin, oldLifetime, newLifetime); + expect(await harness.getConfirmLifetime()).to.equal(newLifetime); + }); + + it("reverts if the new lifetime is out of bounds", async () => { + await expect( + harness.setConfirmLifetime((await harness.MIN_CONFIRM_LIFETIME()) - 1n), + ).to.be.revertedWithCustomError(harness, "ConfirmLifetimeOutOfBounds"); + + await expect( + harness.setConfirmLifetime((await harness.MAX_CONFIRM_LIFETIME()) + 1n), + ).to.be.revertedWithCustomError(harness, "ConfirmLifetimeOutOfBounds"); + }); + }); + + context("setNumber()", () => { + it("reverts if the sender does not have the role", async () => { + for (const role of await harness.confirmingRoles()) { + expect(await harness.hasRole(role, stranger)).to.be.false; + await expect(harness.connect(stranger).setNumber(1)).to.be.revertedWithCustomError(harness, "SenderNotMember"); + } + }); + + it("sets the number", async () => { + const oldNumber = await harness.number(); + const newNumber = oldNumber + 1n; + // nothing happens + await harness.connect(role1Member).setNumber(newNumber); + expect(await harness.number()).to.equal(oldNumber); + + // confirm + await harness.connect(role2Member).setNumber(newNumber); + expect(await harness.number()).to.equal(newNumber); + }); + + it("doesn't execute if the confirmation has expired", async () => { + const oldNumber = await harness.number(); + const newNumber = 1; + const expiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmLifetime()); + const msgData = harness.interface.encodeFunctionData("setNumber", [newNumber]); + + await expect(harness.connect(role1Member).setNumber(newNumber)) + .to.emit(harness, "RoleMemberConfirmed") + .withArgs(role1Member, await harness.ROLE_1(), expiryTimestamp, msgData); + expect(await harness.confirmations(msgData, await harness.ROLE_1())).to.equal(expiryTimestamp); + // still old number + expect(await harness.number()).to.equal(oldNumber); + + await advanceChainTime(expiryTimestamp + 1n); + + const newExpiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmLifetime()); + await expect(harness.connect(role2Member).setNumber(newNumber)) + .to.emit(harness, "RoleMemberConfirmed") + .withArgs(role2Member, await harness.ROLE_2(), newExpiryTimestamp, msgData); + expect(await harness.confirmations(msgData, await harness.ROLE_2())).to.equal(newExpiryTimestamp); + // still old number + expect(await harness.number()).to.equal(oldNumber); + }); + }); + + context("decrementWithZeroRoles()", () => { + it("reverts if there are no confirming roles", async () => { + await expect(harness.connect(stranger).decrementWithZeroRoles()).to.be.revertedWithCustomError( + harness, + "ZeroConfirmingRoles", + ); + }); + }); +}); diff --git a/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol b/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol new file mode 100644 index 000000000..3a37e5988 --- /dev/null +++ b/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.25; + +import {AccessControlConfirmable} from "contracts/0.8.25/utils/AccessControlConfirmable.sol"; + +contract AccessControlConfirmable__Harness is AccessControlConfirmable { + bytes32 public constant ROLE_1 = keccak256("ROLE_1"); + bytes32 public constant ROLE_2 = keccak256("ROLE_2"); + + uint256 public number; + + constructor(address _admin) { + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + + function confirmingRoles() public pure returns (bytes32[] memory) { + bytes32[] memory roles = new bytes32[](2); + roles[0] = ROLE_1; + roles[1] = ROLE_2; + return roles; + } + + function setConfirmLifetime(uint256 _confirmLifetime) external { + _setConfirmLifetime(_confirmLifetime); + } + + function setNumber(uint256 _number) external onlyConfirmed(confirmingRoles()) { + number = _number; + } + + function decrementWithZeroRoles() external onlyConfirmed(new bytes32[](0)) { + number--; + } +} From fdfe70b58ea3ced7a46f621ee82f7c1a455dd38b Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 15:34:01 +0500 Subject: [PATCH 47/65] test(Permissions): full coverage --- .../contracts/Permissions__Harness.sol | 11 +- .../VaultFactory__MockPermissions.sol | 72 +++ .../contracts/VaultHub__MockPermissions.sol | 21 +- .../vaults/permissions/permissions.test.ts | 481 +++++++++++++++++- 4 files changed, 580 insertions(+), 5 deletions(-) diff --git a/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol index d73cbb826..390097bfb 100644 --- a/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol +++ b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol @@ -10,11 +10,16 @@ contract Permissions__Harness is Permissions { _initialize(_defaultAdmin, _confirmLifetime); } + function revertDoubleInitialize(address _defaultAdmin, uint256 _confirmLifetime) external { + _initialize(_defaultAdmin, _confirmLifetime); + _initialize(_defaultAdmin, _confirmLifetime); + } + function confirmingRoles() external pure returns (bytes32[] memory) { return _confirmingRoles(); } - function fund(uint256 _ether) external { + function fund(uint256 _ether) external payable { _fund(_ether); } @@ -46,6 +51,10 @@ contract Permissions__Harness is Permissions { _requestValidatorExit(_pubkey); } + function voluntaryDisconnect() external { + _voluntaryDisconnect(); + } + function transferStakingVaultOwnership(address _newOwner) external { _transferStakingVaultOwnership(_newOwner); } diff --git a/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol index ba372a73c..61371970d 100644 --- a/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol +++ b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol @@ -76,6 +76,78 @@ contract VaultFactory__MockPermissions { emit PermissionsCreated(_permissionsConfig.defaultAdmin, address(permissions)); } + function revertCreateVaultWithPermissionsWithDoubleInitialize( + PermissionsConfig calldata _permissionsConfig, + bytes calldata _stakingVaultInitializerExtraParams + ) external returns (IStakingVault vault, Permissions__Harness permissions) { + // create StakingVault + vault = IStakingVault(address(new BeaconProxy(BEACON, ""))); + + // create Permissions + bytes memory immutableArgs = abi.encode(vault); + permissions = Permissions__Harness(payable(Clones.cloneWithImmutableArgs(PERMISSIONS_IMPL, immutableArgs))); + + // initialize StakingVault + vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); + + // initialize Permissions + permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + // should revert here + permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + + // setup roles + permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); + permissions.grantRole(permissions.FUND_ROLE(), _permissionsConfig.funder); + permissions.grantRole(permissions.WITHDRAW_ROLE(), _permissionsConfig.withdrawer); + permissions.grantRole(permissions.MINT_ROLE(), _permissionsConfig.minter); + permissions.grantRole(permissions.BURN_ROLE(), _permissionsConfig.burner); + permissions.grantRole(permissions.REBALANCE_ROLE(), _permissionsConfig.rebalancer); + permissions.grantRole(permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositPauser); + permissions.grantRole(permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositResumer); + permissions.grantRole(permissions.REQUEST_VALIDATOR_EXIT_ROLE(), _permissionsConfig.exitRequester); + permissions.grantRole(permissions.VOLUNTARY_DISCONNECT_ROLE(), _permissionsConfig.disconnecter); + + permissions.revokeRole(permissions.DEFAULT_ADMIN_ROLE(), address(this)); + + emit VaultCreated(address(permissions), address(vault)); + emit PermissionsCreated(_permissionsConfig.defaultAdmin, address(permissions)); + } + + function revertCreateVaultWithPermissionsWithZeroDefaultAdmin( + PermissionsConfig calldata _permissionsConfig, + bytes calldata _stakingVaultInitializerExtraParams + ) external returns (IStakingVault vault, Permissions__Harness permissions) { + // create StakingVault + vault = IStakingVault(address(new BeaconProxy(BEACON, ""))); + + // create Permissions + bytes memory immutableArgs = abi.encode(vault); + permissions = Permissions__Harness(payable(Clones.cloneWithImmutableArgs(PERMISSIONS_IMPL, immutableArgs))); + + // initialize StakingVault + vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); + + // should revert here + permissions.initialize(address(0), _permissionsConfig.confirmLifetime); + + // setup roles + permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); + permissions.grantRole(permissions.FUND_ROLE(), _permissionsConfig.funder); + permissions.grantRole(permissions.WITHDRAW_ROLE(), _permissionsConfig.withdrawer); + permissions.grantRole(permissions.MINT_ROLE(), _permissionsConfig.minter); + permissions.grantRole(permissions.BURN_ROLE(), _permissionsConfig.burner); + permissions.grantRole(permissions.REBALANCE_ROLE(), _permissionsConfig.rebalancer); + permissions.grantRole(permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositPauser); + permissions.grantRole(permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _permissionsConfig.depositResumer); + permissions.grantRole(permissions.REQUEST_VALIDATOR_EXIT_ROLE(), _permissionsConfig.exitRequester); + permissions.grantRole(permissions.VOLUNTARY_DISCONNECT_ROLE(), _permissionsConfig.disconnecter); + + permissions.revokeRole(permissions.DEFAULT_ADMIN_ROLE(), address(this)); + + emit VaultCreated(address(permissions), address(vault)); + emit PermissionsCreated(_permissionsConfig.defaultAdmin, address(permissions)); + } + /** * @notice Event emitted on a Vault creation * @param owner The address of the Vault owner diff --git a/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol index f68a3f5a3..0322752b0 100644 --- a/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol +++ b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol @@ -4,7 +4,24 @@ pragma solidity ^0.8.0; contract VaultHub__MockPermissions { - function hello() external pure returns (string memory) { - return "hello"; + event Mock__SharesMinted(address indexed _stakingVault, address indexed _recipient, uint256 _shares); + event Mock__SharesBurned(address indexed _stakingVault, uint256 _shares); + event Mock__Rebalanced(uint256 _ether); + event Mock__VoluntaryDisconnect(address indexed _stakingVault); + + function mintSharesBackedByVault(address _stakingVault, address _recipient, uint256 _shares) external { + emit Mock__SharesMinted(_stakingVault, _recipient, _shares); + } + + function burnSharesBackedByVault(address _stakingVault, uint256 _shares) external { + emit Mock__SharesBurned(_stakingVault, _shares); + } + + function rebalance() external payable { + emit Mock__Rebalanced(msg.value); + } + + function voluntaryDisconnect(address _stakingVault) external { + emit Mock__VoluntaryDisconnect(_stakingVault); } } diff --git a/test/0.8.25/vaults/permissions/permissions.test.ts b/test/0.8.25/vaults/permissions/permissions.test.ts index 868ff179e..84cd5909e 100644 --- a/test/0.8.25/vaults/permissions/permissions.test.ts +++ b/test/0.8.25/vaults/permissions/permissions.test.ts @@ -15,7 +15,9 @@ import { } from "typechain-types"; import { PermissionsConfigStruct } from "typechain-types/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions"; -import { days, findEvents } from "lib"; +import { certainAddress, days, ether, findEvents } from "lib"; + +import { Snapshot } from "test/suite"; describe("Permissions", () => { let deployer: HardhatEthersSigner; @@ -30,6 +32,7 @@ describe("Permissions", () => { let depositResumer: HardhatEthersSigner; let exitRequester: HardhatEthersSigner; let disconnecter: HardhatEthersSigner; + let stranger: HardhatEthersSigner; let depositContract: DepositContract__MockForStakingVault; let permissionsImpl: Permissions__Harness; @@ -40,6 +43,8 @@ describe("Permissions", () => { let stakingVault: StakingVault; let permissions: Permissions__Harness; + let originalState: string; + before(async () => { [ deployer, @@ -54,6 +59,7 @@ describe("Permissions", () => { depositResumer, exitRequester, disconnecter, + stranger, ] = await ethers.getSigners(); // 1. Deploy DepositContract @@ -120,7 +126,15 @@ describe("Permissions", () => { expect(permissionsCreatedEvent.args.admin).to.equal(defaultAdmin); }); - context("initial permissions", () => { + beforeEach(async () => { + originalState = await Snapshot.take(); + }); + + afterEach(async () => { + await Snapshot.restore(originalState); + }); + + context("initial state", () => { it("should have the correct roles", async () => { await checkSoleMember(defaultAdmin, await permissions.DEFAULT_ADMIN_ROLE()); await checkSoleMember(funder, await permissions.FUND_ROLE()); @@ -128,6 +142,469 @@ describe("Permissions", () => { await checkSoleMember(minter, await permissions.MINT_ROLE()); await checkSoleMember(burner, await permissions.BURN_ROLE()); await checkSoleMember(rebalancer, await permissions.REBALANCE_ROLE()); + await checkSoleMember(depositPauser, await permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE()); + await checkSoleMember(depositResumer, await permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE()); + await checkSoleMember(exitRequester, await permissions.REQUEST_VALIDATOR_EXIT_ROLE()); + await checkSoleMember(disconnecter, await permissions.VOLUNTARY_DISCONNECT_ROLE()); + }); + }); + + context("initialize()", () => { + it("reverts if called twice", async () => { + await expect( + vaultFactory.connect(deployer).revertCreateVaultWithPermissionsWithDoubleInitialize( + { + defaultAdmin, + nodeOperator, + confirmLifetime: days(7n), + funder, + withdrawer, + minter, + burner, + rebalancer, + depositPauser, + depositResumer, + exitRequester, + disconnecter, + } as PermissionsConfigStruct, + "0x", + ), + ).to.be.revertedWithCustomError(permissions, "AlreadyInitialized"); + }); + + it("reverts if called on the implementation", async () => { + const newImplementation = await ethers.deployContract("Permissions__Harness"); + await expect(newImplementation.initialize(defaultAdmin, days(7n))).to.be.revertedWithCustomError( + permissions, + "NonProxyCallsForbidden", + ); + }); + + it("reverts if zero address is passed as default admin", async () => { + await expect( + vaultFactory.connect(deployer).revertCreateVaultWithPermissionsWithZeroDefaultAdmin( + { + defaultAdmin, + nodeOperator, + confirmLifetime: days(7n), + funder, + withdrawer, + minter, + burner, + rebalancer, + depositPauser, + depositResumer, + exitRequester, + disconnecter, + } as PermissionsConfigStruct, + "0x", + ), + ) + .to.be.revertedWithCustomError(permissions, "ZeroArgument") + .withArgs("_defaultAdmin"); + }); + }); + + context("stakingVault()", () => { + it("returns the correct staking vault", async () => { + expect(await permissions.stakingVault()).to.equal(stakingVault); + }); + }); + + context("grantRoles()", () => { + it("mass-grants roles", async () => { + const [ + fundRole, + withdrawRole, + mintRole, + burnRole, + rebalanceRole, + pauseDepositRole, + resumeDepositRole, + exitRequesterRole, + disconnectRole, + ] = await Promise.all([ + permissions.FUND_ROLE(), + permissions.WITHDRAW_ROLE(), + permissions.MINT_ROLE(), + permissions.BURN_ROLE(), + permissions.REBALANCE_ROLE(), + permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), + permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), + permissions.REQUEST_VALIDATOR_EXIT_ROLE(), + permissions.VOLUNTARY_DISCONNECT_ROLE(), + ]); + + const [ + anotherMinter, + anotherFunder, + anotherWithdrawer, + anotherBurner, + anotherRebalancer, + anotherDepositPauser, + anotherDepositResumer, + anotherExitRequester, + anotherDisconnecter, + ] = [ + certainAddress("another-minter"), + certainAddress("another-funder"), + certainAddress("another-withdrawer"), + certainAddress("another-burner"), + certainAddress("another-rebalancer"), + certainAddress("another-deposit-pauser"), + certainAddress("another-deposit-resumer"), + certainAddress("another-exit-requester"), + certainAddress("another-disconnecter"), + ]; + + const assignments = [ + { role: fundRole, account: anotherFunder }, + { role: withdrawRole, account: anotherWithdrawer }, + { role: mintRole, account: anotherMinter }, + { role: burnRole, account: anotherBurner }, + { role: rebalanceRole, account: anotherRebalancer }, + { role: pauseDepositRole, account: anotherDepositPauser }, + { role: resumeDepositRole, account: anotherDepositResumer }, + { role: exitRequesterRole, account: anotherExitRequester }, + { role: disconnectRole, account: anotherDisconnecter }, + ]; + + await expect(permissions.connect(defaultAdmin).grantRoles(assignments)) + .to.emit(permissions, "RoleGranted") + .withArgs(fundRole, anotherFunder, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(withdrawRole, anotherWithdrawer, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(mintRole, anotherMinter, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(burnRole, anotherBurner, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(rebalanceRole, anotherRebalancer, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(pauseDepositRole, anotherDepositPauser, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(resumeDepositRole, anotherDepositResumer, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(exitRequesterRole, anotherExitRequester, defaultAdmin) + .and.to.emit(permissions, "RoleGranted") + .withArgs(disconnectRole, anotherDisconnecter, defaultAdmin); + + for (const assignment of assignments) { + expect(await permissions.hasRole(assignment.role, assignment.account)).to.be.true; + expect(await permissions.getRoleMemberCount(assignment.role)).to.equal(2); + } + }); + + it("emits only one RoleGranted event per unique role-account pair", async () => { + const anotherMinter = certainAddress("another-minter"); + + const tx = await permissions.connect(defaultAdmin).grantRoles([ + { role: await permissions.MINT_ROLE(), account: anotherMinter }, + { role: await permissions.MINT_ROLE(), account: anotherMinter }, + ]); + + const receipt = await tx.wait(); + if (!receipt) throw new Error("Transaction failed"); + + const events = findEvents(receipt, "RoleGranted"); + expect(events.length).to.equal(1); + expect(events[0].args.role).to.equal(await permissions.MINT_ROLE()); + expect(events[0].args.account).to.equal(anotherMinter); + + expect(await permissions.hasRole(await permissions.MINT_ROLE(), anotherMinter)).to.be.true; + }); + + it("reverts if there are no assignments", async () => { + await expect(permissions.connect(defaultAdmin).grantRoles([])) + .to.be.revertedWithCustomError(permissions, "ZeroArgument") + .withArgs("_assignments"); + }); + }); + + context("revokeRoles()", () => { + it("mass-revokes roles", async () => { + const [ + fundRole, + withdrawRole, + mintRole, + burnRole, + rebalanceRole, + pauseDepositRole, + resumeDepositRole, + exitRequesterRole, + disconnectRole, + ] = await Promise.all([ + permissions.FUND_ROLE(), + permissions.WITHDRAW_ROLE(), + permissions.MINT_ROLE(), + permissions.BURN_ROLE(), + permissions.REBALANCE_ROLE(), + permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), + permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), + permissions.REQUEST_VALIDATOR_EXIT_ROLE(), + permissions.VOLUNTARY_DISCONNECT_ROLE(), + ]); + + const assignments = [ + { role: fundRole, account: funder }, + { role: withdrawRole, account: withdrawer }, + { role: mintRole, account: minter }, + { role: burnRole, account: burner }, + { role: rebalanceRole, account: rebalancer }, + { role: pauseDepositRole, account: depositPauser }, + { role: resumeDepositRole, account: depositResumer }, + { role: exitRequesterRole, account: exitRequester }, + { role: disconnectRole, account: disconnecter }, + ]; + + await expect(permissions.connect(defaultAdmin).revokeRoles(assignments)) + .to.emit(permissions, "RoleRevoked") + .withArgs(fundRole, funder, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(withdrawRole, withdrawer, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(mintRole, minter, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(burnRole, burner, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(rebalanceRole, rebalancer, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(pauseDepositRole, depositPauser, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(resumeDepositRole, depositResumer, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(exitRequesterRole, exitRequester, defaultAdmin) + .and.to.emit(permissions, "RoleRevoked") + .withArgs(disconnectRole, disconnecter, defaultAdmin); + + for (const assignment of assignments) { + expect(await permissions.hasRole(assignment.role, assignment.account)).to.be.false; + expect(await permissions.getRoleMemberCount(assignment.role)).to.equal(0); + } + }); + + it("emits only one RoleRevoked event per unique role-account pair", async () => { + const tx = await permissions.connect(defaultAdmin).revokeRoles([ + { role: await permissions.MINT_ROLE(), account: minter }, + { role: await permissions.MINT_ROLE(), account: minter }, + ]); + + const receipt = await tx.wait(); + if (!receipt) throw new Error("Transaction failed"); + + const events = findEvents(receipt, "RoleRevoked"); + expect(events.length).to.equal(1); + expect(events[0].args.role).to.equal(await permissions.MINT_ROLE()); + expect(events[0].args.account).to.equal(minter); + + expect(await permissions.hasRole(await permissions.MINT_ROLE(), minter)).to.be.false; + }); + + it("reverts if there are no assignments", async () => { + await expect(permissions.connect(defaultAdmin).revokeRoles([])) + .to.be.revertedWithCustomError(permissions, "ZeroArgument") + .withArgs("_assignments"); + }); + }); + + context("confirmingRoles()", () => { + it("returns the correct roles", async () => { + expect(await permissions.confirmingRoles()).to.deep.equal([await permissions.DEFAULT_ADMIN_ROLE()]); + }); + }); + + context("fund()", () => { + it("funds the StakingVault", async () => { + const prevBalance = await ethers.provider.getBalance(stakingVault); + const fundAmount = ether("1"); + await expect(permissions.connect(funder).fund(fundAmount, { value: fundAmount })) + .to.emit(stakingVault, "Funded") + .withArgs(permissions, fundAmount); + + expect(await ethers.provider.getBalance(stakingVault)).to.equal(prevBalance + fundAmount); + }); + + it("reverts if the caller is not a member of the fund role", async () => { + expect(await permissions.hasRole(await permissions.FUND_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).fund(ether("1"), { value: ether("1") })) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.FUND_ROLE()); + }); + }); + + context("withdraw()", () => { + it("withdraws the StakingVault", async () => { + const fundAmount = ether("1"); + await permissions.connect(funder).fund(fundAmount, { value: fundAmount }); + + const withdrawAmount = fundAmount; + const prevBalance = await ethers.provider.getBalance(stakingVault); + await expect(permissions.connect(withdrawer).withdraw(withdrawer, withdrawAmount)) + .to.emit(stakingVault, "Withdrawn") + .withArgs(permissions, withdrawer, withdrawAmount); + + expect(await ethers.provider.getBalance(stakingVault)).to.equal(prevBalance - withdrawAmount); + }); + + it("reverts if the caller is not a member of the withdraw role", async () => { + expect(await permissions.hasRole(await permissions.WITHDRAW_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).withdraw(stranger, ether("1"))) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.WITHDRAW_ROLE()); + }); + }); + + context("mintShares()", () => { + it("emits mock event on the mock vault hub", async () => { + const mintAmount = ether("1"); + await expect(permissions.connect(minter).mintShares(minter, mintAmount)) + .to.emit(vaultHub, "Mock__SharesMinted") + .withArgs(stakingVault, minter, mintAmount); + }); + + it("reverts if the caller is not a member of the mint role", async () => { + expect(await permissions.hasRole(await permissions.MINT_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).mintShares(stranger, ether("1"))) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.MINT_ROLE()); + }); + }); + + context("burnShares()", () => { + it("emits mock event on the mock vault hub", async () => { + const burnAmount = ether("1"); + await expect(permissions.connect(burner).burnShares(burnAmount)) + .to.emit(vaultHub, "Mock__SharesBurned") + .withArgs(stakingVault, burnAmount); + }); + + it("reverts if the caller is not a member of the burn role", async () => { + expect(await permissions.hasRole(await permissions.BURN_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).burnShares(ether("1"))) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.BURN_ROLE()); + }); + }); + + context("rebalanceVault()", () => { + it("rebalances the StakingVault", async () => { + expect(await stakingVault.vaultHub()).to.equal(vaultHub); + const fundAmount = ether("1"); + await permissions.connect(funder).fund(fundAmount, { value: fundAmount }); + + const rebalanceAmount = fundAmount; + const prevBalance = await ethers.provider.getBalance(stakingVault); + await expect(permissions.connect(rebalancer).rebalanceVault(rebalanceAmount)) + .to.emit(vaultHub, "Mock__Rebalanced") + .withArgs(rebalanceAmount); + + expect(await ethers.provider.getBalance(stakingVault)).to.equal(prevBalance - rebalanceAmount); + }); + + it("reverts if the caller is not a member of the rebalance role", async () => { + expect(await permissions.hasRole(await permissions.REBALANCE_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).rebalanceVault(ether("1"))) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.REBALANCE_ROLE()); + }); + }); + + context("pauseBeaconChainDeposits()", () => { + it("pauses the BeaconChainDeposits", async () => { + await expect(permissions.connect(depositPauser).pauseBeaconChainDeposits()).to.emit( + stakingVault, + "BeaconChainDepositsPaused", + ); + + expect(await stakingVault.beaconChainDepositsPaused()).to.be.true; + }); + + it("reverts if the caller is not a member of the pause deposit role", async () => { + expect(await permissions.hasRole(await permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).pauseBeaconChainDeposits()) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE()); + + expect(await stakingVault.beaconChainDepositsPaused()).to.be.false; + }); + }); + + context("resumeBeaconChainDeposits()", () => { + it("resumes the BeaconChainDeposits", async () => { + await permissions.connect(depositPauser).pauseBeaconChainDeposits(); + expect(await stakingVault.beaconChainDepositsPaused()).to.be.true; + + await expect(permissions.connect(depositResumer).resumeBeaconChainDeposits()).to.emit( + stakingVault, + "BeaconChainDepositsResumed", + ); + + expect(await stakingVault.beaconChainDepositsPaused()).to.be.false; + }); + + it("reverts if the caller is not a member of the resume deposit role", async () => { + expect(await permissions.hasRole(await permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).resumeBeaconChainDeposits()) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.RESUME_BEACON_CHAIN_DEPOSITS_ROLE()); + }); + }); + + context("requestValidatorExit()", () => { + it("requests a validator exit", async () => { + await expect(permissions.connect(exitRequester).requestValidatorExit("0xabcdef")) + .to.emit(stakingVault, "ValidatorsExitRequest") + .withArgs(permissions, "0xabcdef"); + }); + + it("reverts if the caller is not a member of the request exit role", async () => { + expect(await permissions.hasRole(await permissions.REQUEST_VALIDATOR_EXIT_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).requestValidatorExit("0xabcdef")) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.REQUEST_VALIDATOR_EXIT_ROLE()); + }); + }); + + context("voluntaryDisconnect()", () => { + it("voluntarily disconnects the StakingVault", async () => { + await expect(permissions.connect(disconnecter).voluntaryDisconnect()) + .to.emit(vaultHub, "Mock__VoluntaryDisconnect") + .withArgs(stakingVault); + }); + + it("reverts if the caller is not a member of the disconnect role", async () => { + expect(await permissions.hasRole(await permissions.VOLUNTARY_DISCONNECT_ROLE(), stranger)).to.be.false; + + await expect(permissions.connect(stranger).voluntaryDisconnect()) + .to.be.revertedWithCustomError(permissions, "AccessControlUnauthorizedAccount") + .withArgs(stranger, await permissions.VOLUNTARY_DISCONNECT_ROLE()); + }); + }); + + context("transferStakingVaultOwnership()", () => { + it("transfers the StakingVault ownership", async () => { + const newOwner = certainAddress("new-owner"); + await expect(permissions.connect(defaultAdmin).transferStakingVaultOwnership(newOwner)) + .to.emit(stakingVault, "OwnershipTransferred") + .withArgs(permissions, newOwner); + + expect(await stakingVault.owner()).to.equal(newOwner); + }); + + it("reverts if the caller is not a member of the default admin role", async () => { + expect(await permissions.hasRole(await permissions.DEFAULT_ADMIN_ROLE(), stranger)).to.be.false; + + await expect( + permissions.connect(stranger).transferStakingVaultOwnership(certainAddress("new-owner")), + ).to.be.revertedWithCustomError(permissions, "SenderNotMember"); }); }); From de8c97959d4fce1c2e21700236f5910a9ebb1820 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 17:33:22 +0500 Subject: [PATCH 48/65] fix(tests): update dashboard tests --- test/0.8.25/vaults/dashboard/dashboard.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/0.8.25/vaults/dashboard/dashboard.test.ts b/test/0.8.25/vaults/dashboard/dashboard.test.ts index ed0f85440..3b7eaad4e 100644 --- a/test/0.8.25/vaults/dashboard/dashboard.test.ts +++ b/test/0.8.25/vaults/dashboard/dashboard.test.ts @@ -45,6 +45,8 @@ describe("Dashboard.sol", () => { let dashboard: Dashboard; let dashboardAddress: string; + const confirmLifetime = days(7n); + let originalState: string; const BP_BASE = 10_000n; @@ -125,13 +127,16 @@ describe("Dashboard.sol", () => { context("initialize", () => { it("reverts if already initialized", async () => { - await expect(dashboard.initialize(vaultOwner)).to.be.revertedWithCustomError(dashboard, "AlreadyInitialized"); + await expect(dashboard.initialize(vaultOwner, confirmLifetime)).to.be.revertedWithCustomError( + dashboard, + "AlreadyInitialized", + ); }); it("reverts if called on the implementation", async () => { const dashboard_ = await ethers.deployContract("Dashboard", [weth, lidoLocator]); - await expect(dashboard_.initialize(vaultOwner)).to.be.revertedWithCustomError( + await expect(dashboard_.initialize(vaultOwner, confirmLifetime)).to.be.revertedWithCustomError( dashboard_, "NonProxyCallsForbidden", ); @@ -177,7 +182,7 @@ describe("Dashboard.sol", () => { expect(await dashboard.sharesMinted()).to.equal(sockets.sharesMinted); expect(await dashboard.reserveRatioBP()).to.equal(sockets.reserveRatioBP); expect(await dashboard.thresholdReserveRatioBP()).to.equal(sockets.reserveRatioThresholdBP); - expect(await dashboard.treasuryFee()).to.equal(sockets.treasuryFeeBP); + expect(await dashboard.treasuryFeeBP()).to.equal(sockets.treasuryFeeBP); }); }); @@ -460,7 +465,7 @@ describe("Dashboard.sol", () => { it("reverts if called by a non-admin", async () => { await expect(dashboard.connect(stranger).transferStakingVaultOwnership(vaultOwner)).to.be.revertedWithCustomError( dashboard, - "NotACommitteeMember", + "SenderNotMember", ); }); From 6963f651df180c7f7f1b89d230e805d7c93f810f Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 17:33:37 +0500 Subject: [PATCH 49/65] fix(Dashboard): update comments --- contracts/0.8.25/vaults/Dashboard.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 1fdfd9d97..ad057142f 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -87,7 +87,9 @@ contract Dashboard is Permissions { } /** - * @notice Initializes the contract with the default admin role + * @notice Initializes the contract + * @param _defaultAdmin Address of the default admin + * @param _confirmLifetime Confirm lifetime in seconds */ function initialize(address _defaultAdmin, uint256 _confirmLifetime) external virtual { // reduces gas cost for `mintWsteth` @@ -327,8 +329,6 @@ contract Dashboard is Permissions { _burnWstETH(_amountOfWstETH); } - // TODO: move down - /** * @notice Burns stETH tokens (in shares) backed by the vault from the sender using permit (with value in stETH). * @param _amountOfShares Amount of stETH shares to burn From e2b9b3b5f068d380c6b3c4279e8b33d3726c1093 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 17:37:22 +0500 Subject: [PATCH 50/65] fix(Delegation): update tests --- .../vaults/delegation/delegation.test.ts | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/test/0.8.25/vaults/delegation/delegation.test.ts b/test/0.8.25/vaults/delegation/delegation.test.ts index c808279cd..93ea9bb4e 100644 --- a/test/0.8.25/vaults/delegation/delegation.test.ts +++ b/test/0.8.25/vaults/delegation/delegation.test.ts @@ -248,23 +248,23 @@ describe("Delegation.sol", () => { }); it("sets the new confirm lifetime", async () => { - const oldConfirmLifetime = await delegation.confirmLifetime(); + const oldConfirmLifetime = await delegation.getConfirmLifetime(); const newConfirmLifetime = days(10n); const msgData = delegation.interface.encodeFunctionData("setConfirmLifetime", [newConfirmLifetime]); - let confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + let confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(vaultOwner).setConfirmLifetime(newConfirmLifetime)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), confirmTimestamp, msgData); - confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(nodeOperatorManager).setConfirmLifetime(newConfirmLifetime)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), confirmTimestamp, msgData) .and.to.emit(delegation, "ConfirmLifetimeSet") .withArgs(nodeOperatorManager, oldConfirmLifetime, newConfirmLifetime); - expect(await delegation.confirmLifetime()).to.equal(newConfirmLifetime); + expect(await delegation.getConfirmLifetime()).to.equal(newConfirmLifetime); }); }); @@ -402,7 +402,7 @@ describe("Delegation.sol", () => { const curatorFeeBP = 1000; // 10% const operatorFeeBP = 1000; // 10% - await delegation.connect(vaultOwner).setCuratorFeeBP(curatorFeeBP); + await delegation.connect(curatorFeeSetter).setCuratorFeeBP(curatorFeeBP); await delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(operatorFeeBP); await delegation.connect(funder).fund({ value: amount }); @@ -633,7 +633,7 @@ describe("Delegation.sol", () => { it("requires both default admin and operator manager to set the operator fee and emits the RoleMemberConfirmed event", async () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) @@ -642,11 +642,9 @@ describe("Delegation.sol", () => { // fee is unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); // check confirm - expect(await delegation.confirmationExpiryTimestamp(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal( - expiryTimestamp, - ); + expect(await delegation.confirmations(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal(expiryTimestamp); - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData) @@ -657,7 +655,7 @@ describe("Delegation.sol", () => { // resets the confirms for (const role of await delegation.confirmingRoles()) { - expect(await delegation.confirmationExpiryTimestamp(keccak256(msgData), role)).to.equal(0n); + expect(await delegation.confirmations(keccak256(msgData), role)).to.equal(0n); } }); @@ -673,7 +671,7 @@ describe("Delegation.sol", () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") @@ -681,13 +679,11 @@ describe("Delegation.sol", () => { // fee is unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); // check confirm - expect(await delegation.confirmationExpiryTimestamp(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal( - expiryTimestamp, - ); + expect(await delegation.confirmations(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal(expiryTimestamp); // move time forward await advanceChainTime(days(7n) + 1n); - const expectedExpiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + const expectedExpiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); expect(expectedExpiryTimestamp).to.be.greaterThan(expiryTimestamp + days(7n)); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") @@ -696,12 +692,12 @@ describe("Delegation.sol", () => { // fee is still unchanged expect(await delegation.nodeOperatorFeeBP()).to.equal(previousOperatorFee); // check confirm - expect( - await delegation.confirmationExpiryTimestamp(msgData, await delegation.NODE_OPERATOR_MANAGER_ROLE()), - ).to.equal(expectedExpiryTimestamp); + expect(await delegation.confirmations(msgData, await delegation.NODE_OPERATOR_MANAGER_ROLE())).to.equal( + expectedExpiryTimestamp, + ); // curator has to confirm again - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData) @@ -723,14 +719,14 @@ describe("Delegation.sol", () => { it("requires both curator and operator to transfer ownership and emits the RoleMemberConfirmd event", async () => { const newOwner = certainAddress("newOwner"); const msgData = delegation.interface.encodeFunctionData("transferStakingVaultOwnership", [newOwner]); - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(vaultOwner).transferStakingVaultOwnership(newOwner)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData); // owner is unchanged expect(await vault.owner()).to.equal(delegation); - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.confirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); await expect(delegation.connect(nodeOperatorManager).transferStakingVaultOwnership(newOwner)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData); From 986cfdef3cd286a7e02f5895ab502648578338d1 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 14 Feb 2025 13:09:07 +0000 Subject: [PATCH 51/65] chore: migrate foundry to stable --- foundry.toml | 2 +- foundry/lib/forge-std | 2 +- test/common/memUtils.t.sol | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 3ddeddae8..3798d585b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,7 +15,7 @@ test = 'test' cache = true # The cache directory if enabled -cache_path = 'foundry/cache' +cache_path = 'foundry/cache' # Only run tests in contracts matching the specified glob pattern match_path = '**/test/**/*.t.sol' diff --git a/foundry/lib/forge-std b/foundry/lib/forge-std index ffa2ee0d9..bf909b22f 160000 --- a/foundry/lib/forge-std +++ b/foundry/lib/forge-std @@ -1 +1 @@ -Subproject commit ffa2ee0d921b4163b7abd0f1122df93ead205805 +Subproject commit bf909b22fa55e244796dfa920c9639fdffa1c545 diff --git a/test/common/memUtils.t.sol b/test/common/memUtils.t.sol index 1e10db057..7fd2c916e 100644 --- a/test/common/memUtils.t.sol +++ b/test/common/memUtils.t.sol @@ -483,6 +483,7 @@ contract MemUtilsTest is Test, MemUtilsTestHelper { assertEq(dst, abi.encodePacked(bytes32(0x2211111111111111111111111111111111111111111111111111111111111111))); } + /// forge-config: default.allow_internal_expect_revert = true function test_copyBytes_RevertsWhenSrcArrayIsOutOfBounds() external { bytes memory src = abi.encodePacked( bytes32(0x1111111111111111111111111111111111111111111111111111111111111111) From 4c4002a9c7d5213cc139044a1a6c4e94a217d4ea Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 18:12:08 +0500 Subject: [PATCH 52/65] test(Factory): remove curator check --- test/0.8.25/vaults/vaultFactory.test.ts | 18 +++++++----------- .../vaults-happy-path.integration.ts | 15 +++++++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index 0b5a55ccd..a595103c4 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -21,7 +21,7 @@ import { } from "typechain-types"; import { DelegationConfigStruct } from "typechain-types/contracts/0.8.25/vaults/VaultFactory"; -import { createVaultProxy, ether } from "lib"; +import { createVaultProxy, days, ether } from "lib"; import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; @@ -113,14 +113,17 @@ describe("VaultFactory.sol", () => { withdrawer: await vaultOwner1.getAddress(), minter: await vaultOwner1.getAddress(), burner: await vaultOwner1.getAddress(), - curator: await vaultOwner1.getAddress(), + curatorFeeSetter: await vaultOwner1.getAddress(), + curatorFeeClaimer: await vaultOwner1.getAddress(), + nodeOperatorManager: await operator.getAddress(), + nodeOperatorFeeConfirmer: await operator.getAddress(), + nodeOperatorFeeClaimer: await operator.getAddress(), rebalancer: await vaultOwner1.getAddress(), depositPauser: await vaultOwner1.getAddress(), depositResumer: await vaultOwner1.getAddress(), exitRequester: await vaultOwner1.getAddress(), disconnecter: await vaultOwner1.getAddress(), - nodeOperatorManager: await operator.getAddress(), - nodeOperatorFeeClaimer: await operator.getAddress(), + confirmLifetime: days(7n), curatorFeeBP: 100n, nodeOperatorFeeBP: 200n, }; @@ -167,13 +170,6 @@ describe("VaultFactory.sol", () => { }); context("createVaultWithDelegation", () => { - it("reverts if `curator` is zero address", async () => { - const params = { ...delegationParams, curator: ZeroAddress }; - await expect(createVaultProxy(vaultOwner1, vaultFactory, params)) - .to.revertedWithCustomError(vaultFactory, "ZeroArgument") - .withArgs("curator"); - }); - it("works with empty `params`", async () => { console.log({ delegationParams, diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 59df58cb4..5287ac3f6 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { Delegation, StakingVault } from "typechain-types"; -import { computeDepositDataRoot, impersonate, log, trace, updateBalance } from "lib"; +import { computeDepositDataRoot, days, impersonate, log, trace, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { getReportTimeElapsed, @@ -163,16 +163,19 @@ describe("Scenario: Staking Vaults Happy Path", () => { withdrawer: curator, minter: curator, burner: curator, - curator, rebalancer: curator, depositPauser: curator, depositResumer: curator, exitRequester: curator, disconnecter: curator, + curatorFeeSetter: curator, + curatorFeeClaimer: curator, nodeOperatorManager: nodeOperator, + nodeOperatorFeeConfirmer: nodeOperator, nodeOperatorFeeClaimer: nodeOperator, curatorFeeBP: VAULT_OWNER_FEE, nodeOperatorFeeBP: VAULT_NODE_OPERATOR_FEE, + confirmLifetime: days(7n), }, "0x", ); @@ -187,13 +190,13 @@ describe("Scenario: Staking Vaults Happy Path", () => { expect(await isSoleRoleMember(owner, await delegation.DEFAULT_ADMIN_ROLE())).to.be.true; - expect(await isSoleRoleMember(curator, await delegation.CURATOR_ROLE())).to.be.true; + expect(await isSoleRoleMember(curator, await delegation.CURATOR_FEE_SET_ROLE())).to.be.true; + expect(await isSoleRoleMember(curator, await delegation.CURATOR_FEE_CLAIM_ROLE())).to.be.true; expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_MANAGER_ROLE())).to.be.true; + expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_FEE_CLAIM_ROLE())).to.be.true; + expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE())).to.be.true; - expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_FEE_CLAIMER_ROLE())).to.be.true; - - expect(await isSoleRoleMember(curator, await delegation.CURATOR_ROLE())).to.be.true; expect(await isSoleRoleMember(curator, await delegation.FUND_ROLE())).to.be.true; expect(await isSoleRoleMember(curator, await delegation.WITHDRAW_ROLE())).to.be.true; expect(await isSoleRoleMember(curator, await delegation.MINT_ROLE())).to.be.true; From 48d80c038b82b3bb68ab4b82b7fe87ae361d5123 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Fri, 14 Feb 2025 18:19:25 +0500 Subject: [PATCH 53/65] feat: revert if node operator is zero address on init --- contracts/0.8.25/vaults/StakingVault.sol | 2 ++ .../vaults/staking-vault/staking-vault.test.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 2e1b911c7..f2fdf5f04 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -120,6 +120,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { * @param - Additional initialization parameters */ function initialize(address _owner, address _nodeOperator, bytes calldata /* _params */) external initializer { + if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); + __Ownable_init(_owner); _getStorage().nodeOperator = _nodeOperator; } diff --git a/test/0.8.25/vaults/staking-vault/staking-vault.test.ts b/test/0.8.25/vaults/staking-vault/staking-vault.test.ts index 075fd82a3..33a465bfe 100644 --- a/test/0.8.25/vaults/staking-vault/staking-vault.test.ts +++ b/test/0.8.25/vaults/staking-vault/staking-vault.test.ts @@ -14,7 +14,7 @@ import { VaultHub__MockForStakingVault, } from "typechain-types"; -import { computeDepositDataRoot, de0x, ether, findEvents, impersonate, streccak } from "lib"; +import { computeDepositDataRoot, de0x, ether, findEvents, impersonate, proxify, streccak } from "lib"; import { Snapshot } from "test/suite"; @@ -85,7 +85,9 @@ describe("StakingVault.sol", () => { .to.be.revertedWithCustomError(stakingVaultImplementation, "ZeroArgument") .withArgs("_beaconChainDepositContract"); }); + }); + context("initialize", () => { it("petrifies the implementation by setting the initialized version to 2^64 - 1", async () => { expect(await stakingVaultImplementation.getInitializedVersion()).to.equal(2n ** 64n - 1n); expect(await stakingVaultImplementation.version()).to.equal(1n); @@ -96,6 +98,14 @@ describe("StakingVault.sol", () => { stakingVaultImplementation.connect(stranger).initialize(vaultOwner, operator, "0x"), ).to.be.revertedWithCustomError(stakingVaultImplementation, "InvalidInitialization"); }); + + it("reverts if the node operator is zero address", async () => { + const [vault_] = await proxify({ impl: stakingVaultImplementation, admin: vaultOwner }); + await expect(vault_.initialize(vaultOwner, ZeroAddress, "0x")).to.be.revertedWithCustomError( + stakingVaultImplementation, + "ZeroArgument", + ); + }); }); context("initial state", () => { From 087b03e714a4dc26972c3dd21f0a3a05c288785a Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 14 Feb 2025 15:36:32 +0000 Subject: [PATCH 54/65] fix(test): vebo gas test --- test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index 44a7080ba..b0c79a11a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -6,7 +6,6 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; -import { trace } from "lib"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; import { @@ -203,7 +202,7 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { it(`a committee member submits the report data, exit requests are emitted`, async () => { const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); - const receipt = await trace("oracle.submit", tx); + const receipt = (await tx.wait()) as ContractTransactionReceipt; await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); expect((await oracle.getConsensusReport()).processingStarted).to.equal(true); From 014df012c53f14d71ccd6c27f2f8efa38fa8b04e Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 14 Feb 2025 16:11:51 +0000 Subject: [PATCH 55/65] test: speed up gas validation test --- .../validator-exit-bus-oracle.gas.test.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index b0c79a11a..c92fc799c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -145,9 +145,7 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { let reportHash: string; let originalState: string; - before(async () => { - originalState = await Snapshot.take(); - }); + before(async () => (originalState = await Snapshot.take())); after(async () => await Snapshot.restore(originalState)); @@ -208,12 +206,19 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { const timestamp = await oracle.getTime(); - for (const request of exitRequests.requests) { - await expect(tx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); - } + const evFirst = exitRequests.requests[0]; + const evLast = exitRequests.requests[exitRequests.requests.length - 1]; + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(evFirst.moduleId, evFirst.nodeOpId, evFirst.valIndex, evFirst.valPubkey, timestamp); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(evLast.moduleId, evLast.nodeOpId, evLast.valIndex, evLast.valPubkey, timestamp); + const { gasUsed } = receipt; + gasUsages.push({ totalRequests, requestsPerModule: exitRequests.requestsPerModule, From 9bc4f779c06421f84cf3a7af91f390bf0bc35db5 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 17 Feb 2025 12:35:23 +0500 Subject: [PATCH 56/65] fix: minor fixes --- contracts/0.8.25/vaults/Dashboard.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 52adf0ec8..5d83f3e11 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -77,7 +77,7 @@ contract Dashboard is Permissions { * @param _wETH Address of the weth token contract. * @param _lidoLocator Address of the Lido locator contract. */ - constructor(address _wETH, address _lidoLocator) Permissions() { + constructor(address _wETH, address _lidoLocator) { if (_wETH == address(0)) revert ZeroArgument("_wETH"); if (_lidoLocator == address(0)) revert ZeroArgument("_lidoLocator"); @@ -250,7 +250,7 @@ contract Dashboard is Permissions { } /** - * @notice Withdraws stETH tokens from the staking vault to wrapped ether. + * @notice Withdraws wETH tokens from the staking vault to wrapped ether. * @param _recipient Address of the recipient * @param _amountOfWETH Amount of WETH to withdraw */ From a494579e5548acfd4de0010b2caf5fca86ffa77a Mon Sep 17 00:00:00 2001 From: failingtwice Date: Mon, 17 Feb 2025 12:40:55 +0500 Subject: [PATCH 57/65] fix: update comments --- contracts/0.8.25/vaults/Dashboard.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 5d83f3e11..b14e05cff 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -269,7 +269,7 @@ contract Dashboard is Permissions { } /** - * @notice Mints stETH tokens backed by the vault to the recipient. + * @notice Mints stETH shares backed by the vault to the recipient. * @param _recipient Address of the recipient * @param _amountOfShares Amount of stETH shares to mint */ @@ -311,9 +311,9 @@ contract Dashboard is Permissions { } /** - * @notice Burns stETH shares from the sender backed by the vault. Expects stETH amount approved to this contract. + * @notice Burns stETH tokens from the sender backed by the vault. Expects stETH amount approved to this contract. * !NB: this will revert with `VaultHub.ZeroArgument("_amountOfShares")` if the amount of stETH is less than 1 share - * @param _amountOfStETH Amount of stETH shares to burn + * @param _amountOfStETH Amount of stETH tokens to burn */ function burnStETH(uint256 _amountOfStETH) external { _burnStETH(_amountOfStETH); @@ -330,7 +330,7 @@ contract Dashboard is Permissions { } /** - * @notice Burns stETH tokens (in shares) backed by the vault from the sender using permit (with value in stETH). + * @notice Burns stETH shares backed by the vault from the sender using permit (with value in stETH). * @param _amountOfShares Amount of stETH shares to burn * @param _permit data required for the stETH.permit() with amount in stETH */ @@ -377,7 +377,7 @@ contract Dashboard is Permissions { } /** - * @notice recovers ERC20 tokens or ether from the dashboard contract to sender + * @notice Recovers ERC20 tokens or ether from the dashboard contract to sender * @param _token Address of the token to recover or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether * @param _recipient Address of the recovery recipient */ @@ -498,7 +498,7 @@ contract Dashboard is Permissions { } /** - * @dev calculates total shares vault can mint + * @dev Calculates total shares vault can mint * @param _valuation custom vault valuation */ function _totalMintableShares(uint256 _valuation) internal view returns (uint256) { From 5d853cd35661024c48d4303c7338b46315e15aac Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 18 Feb 2025 10:25:03 +0000 Subject: [PATCH 58/65] fix(test): vaults happy path --- .../vaults-happy-path.integration.ts | 48 ++++++------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 59df58cb4..8e19bae85 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { Delegation, StakingVault } from "typechain-types"; -import { computeDepositDataRoot, impersonate, log, trace, updateBalance } from "lib"; +import { computeDepositDataRoot, impersonate, log, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { getReportTimeElapsed, @@ -122,11 +122,8 @@ describe("Scenario: Staking Vaults Happy Path", () => { await lido.connect(ethHolder).submit(ZeroAddress, { value: LIDO_DEPOSIT }); const dsmSigner = await impersonate(depositSecurityModule.address, LIDO_DEPOSIT); - const depositNorTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); - await trace("lido.deposit", depositNorTx); - - const depositSdvtTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); - await trace("lido.deposit", depositSdvtTx); + await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); + await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); const reportData: Partial = { clDiff: LIDO_DEPOSIT, @@ -177,7 +174,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { "0x", ); - const createVaultTxReceipt = await trace("vaultsFactory.createVault", deployTx); + const createVaultTxReceipt = (await deployTx.wait()) as ContractTransactionReceipt; const createVaultEvents = ctx.getEvents(createVaultTxReceipt, "VaultCreated"); expect(createVaultEvents.length).to.equal(1n); @@ -227,8 +224,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { }); it("Should allow Staker to fund vault via delegation contract", async () => { - const depositTx = await delegation.connect(curator).fund({ value: VAULT_DEPOSIT }); - await trace("delegation.fund", depositTx); + await delegation.connect(curator).fund({ value: VAULT_DEPOSIT }); const vaultBalance = await ethers.provider.getBalance(stakingVault); @@ -258,9 +254,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { }); } - const topUpTx = await stakingVault.connect(nodeOperator).depositToBeaconChain(deposits); - - await trace("stakingVault.depositToBeaconChain", topUpTx); + await stakingVault.connect(nodeOperator).depositToBeaconChain(deposits); stakingVaultBeaconBalance += VAULT_DEPOSIT; stakingVaultAddress = await stakingVault.getAddress(); @@ -291,7 +285,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { .withArgs(stakingVault, stakingVault.valuation()); const mintTx = await delegation.connect(curator).mintShares(curator, stakingVaultMaxMintingShares); - const mintTxReceipt = await trace("delegation.mint", mintTx); + const mintTxReceipt = (await mintTx.wait()) as ContractTransactionReceipt; const mintEvents = ctx.getEvents(mintTxReceipt, "MintedSharesOnVault"); expect(mintEvents.length).to.equal(1n); @@ -350,10 +344,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { const operatorBalanceBefore = await ethers.provider.getBalance(nodeOperator); const claimPerformanceFeesTx = await delegation.connect(nodeOperator).claimNodeOperatorFee(nodeOperator); - const claimPerformanceFeesTxReceipt = await trace( - "delegation.claimNodeOperatorFee", - claimPerformanceFeesTx, - ); + const claimPerformanceFeesTxReceipt = (await claimPerformanceFeesTx.wait()) as ContractTransactionReceipt; const operatorBalanceAfter = await ethers.provider.getBalance(nodeOperator); const gasFee = claimPerformanceFeesTxReceipt.gasPrice * claimPerformanceFeesTxReceipt.cumulativeGasUsed; @@ -398,7 +389,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { const managerBalanceBefore = await ethers.provider.getBalance(curator); const claimEthTx = await delegation.connect(curator).claimCuratorFee(curator); - const { gasUsed, gasPrice } = await trace("delegation.claimCuratorFee", claimEthTx); + const { gasUsed, gasPrice } = (await claimEthTx.wait()) as ContractTransactionReceipt; const managerBalanceAfter = await ethers.provider.getBalance(curator); const vaultBalance = await ethers.provider.getBalance(stakingVaultAddress); @@ -418,13 +409,8 @@ describe("Scenario: Staking Vaults Happy Path", () => { const { lido } = ctx.contracts; // Token master can approve the vault to burn the shares - const approveVaultTx = await lido - .connect(curator) - .approve(delegation, await lido.getPooledEthByShares(stakingVaultMaxMintingShares)); - await trace("lido.approve", approveVaultTx); - - const burnTx = await delegation.connect(curator).burnShares(stakingVaultMaxMintingShares); - await trace("delegation.burn", burnTx); + await lido.connect(curator).approve(delegation, await lido.getPooledEthByShares(stakingVaultMaxMintingShares)); + await delegation.connect(curator).burnShares(stakingVaultMaxMintingShares); const { elapsedProtocolReward, elapsedVaultReward } = await calculateReportParams(); const vaultValue = await addRewards(elapsedVaultReward / 2n); // Half the vault rewards value after validator exit @@ -436,12 +422,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { inOutDeltas: [VAULT_DEPOSIT], } as OracleReportParams; - const { reportTx } = (await report(ctx, params)) as { - reportTx: TransactionResponse; - extraDataTx: TransactionResponse; - }; - - await trace("report", reportTx); + await report(ctx, params); const lockedOnVault = await stakingVault.locked(); expect(lockedOnVault).to.be.gt(0n); // lockedOnVault should be greater than 0, because of the debt @@ -455,15 +436,14 @@ describe("Scenario: Staking Vaults Happy Path", () => { const socket = await accounting["vaultSocket(address)"](stakingVaultAddress); const sharesMinted = await lido.getPooledEthByShares(socket.sharesMinted); - const rebalanceTx = await delegation.connect(curator).rebalanceVault(sharesMinted, { value: sharesMinted }); - await trace("delegation.rebalanceVault", rebalanceTx); + await delegation.connect(curator).rebalanceVault(sharesMinted, { value: sharesMinted }); expect(await stakingVault.locked()).to.equal(VAULT_CONNECTION_DEPOSIT); // 1 ETH locked as a connection fee }); it("Should allow Manager to disconnect vaults from the hub", async () => { const disconnectTx = await delegation.connect(curator).voluntaryDisconnect(); - const disconnectTxReceipt = await trace("delegation.voluntaryDisconnect", disconnectTx); + const disconnectTxReceipt = (await disconnectTx.wait()) as ContractTransactionReceipt; const disconnectEvents = ctx.getEvents(disconnectTxReceipt, "VaultDisconnected"); expect(disconnectEvents.length).to.equal(1n); From fd9b0b70a0fb79148a821fc1f9d4cc9400f6c50d Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 18 Feb 2025 10:45:51 +0000 Subject: [PATCH 59/65] fix(test): vebo tests --- ...ator-exit-bus-oracle.accessControl.test.ts | 10 ++--- .../validator-exit-bus-oracle.gas.test.ts | 37 +++++++++---------- ...alidator-exit-bus-oracle.happyPath.test.ts | 10 ++--- ...r-exit-bus-oracle.submitReportData.test.ts | 10 ++--- test/deploy/validatorExitBusOracle.ts | 21 ++++++++--- 5 files changed, 42 insertions(+), 46 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 53c0e1e29..93d8ae4a2 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -70,7 +70,9 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { return "0x" + requests.map(encodeExitRequestHex).join(""); }; - const deploy = async () => { + before(async () => { + [admin, member1, member2, member3, stranger, account1] = await ethers.getSigners(); + const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; @@ -103,12 +105,6 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { await consensus.connect(member1).submitReport(refSlot, reportHash, CONSENSUS_VERSION); await consensus.connect(member3).submitReport(refSlot, reportHash, CONSENSUS_VERSION); - }; - - before(async () => { - [admin, member1, member2, member3, stranger, account1] = await ethers.getSigners(); - - await deploy(); }); beforeEach(async () => (originalState = await Snapshot.take())); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index c92fc799c..6cd0985c7 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -76,25 +76,6 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { return "0x" + requests.map(encodeExitRequestHex).join(""); }; - const deploy = async () => { - const deployed = await deployVEBO(admin.address); - oracle = deployed.oracle; - consensus = deployed.consensus; - - await initVEBO({ - admin: admin.address, - oracle, - consensus, - resumeAfterDeploy: true, - }); - - oracleVersion = await oracle.getContractVersion(); - - await consensus.addMember(member1, 1); - await consensus.addMember(member2, 2); - await consensus.addMember(member3, 2); - }; - const triggerConsensusOnHash = async (hash: string) => { const { refSlot } = await consensus.getCurrentFrame(); await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); @@ -124,7 +105,23 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { before(async () => { [admin, member1, member2, member3] = await ethers.getSigners(); - await deploy(); + + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); }); after(async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 615050cf4..74f411f6c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -77,7 +77,9 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { return "0x" + requests.map(encodeExitRequestHex).join(""); }; - const deploy = async () => { + before(async () => { + [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; @@ -95,12 +97,6 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { await consensus.addMember(member1, 1); await consensus.addMember(member2, 2); await consensus.addMember(member3, 2); - }; - - before(async () => { - [admin, member1, member2, member3, stranger] = await ethers.getSigners(); - - await deploy(); }); const triggerConsensusOnHash = async (hash: string) => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index a5f7fd628..da220cfc9 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -103,7 +103,9 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { return (await oracle.getLastRequestedValidatorIndices(moduleId, [nodeOpId]))[0]; } - const deploy = async () => { + before(async () => { + [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; @@ -122,12 +124,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await consensus.addMember(member1, 1); await consensus.addMember(member2, 2); await consensus.addMember(member3, 2); - }; - - before(async () => { - [admin, member1, member2, member3, stranger] = await ethers.getSigners(); - - await deploy(); }); context("discarded report prevents data submit", () => { diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 1b5e0e280..9ca2c44de 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -18,7 +18,7 @@ import { deployLidoLocator, updateLidoLocatorImplementation } from "./locator"; export const DATA_FORMAT_LIST = 1; async function deployMockAccountingOracle(secondsPerSlot = SECONDS_PER_SLOT, genesisTime = GENESIS_TIME) { - const lido = await ethers.deployContract("Lido__MockForAccountingOracle"); + const lido = await ethers.deployContract("Accounting__MockForAccountingOracle"); const ao = await ethers.deployContract("AccountingOracle__MockForSanityChecker", [ await lido.getAddress(), secondsPerSlot, @@ -28,10 +28,21 @@ async function deployMockAccountingOracle(secondsPerSlot = SECONDS_PER_SLOT, gen } async function deployOracleReportSanityCheckerForExitBus(lidoLocator: string, admin: string) { - const maxValidatorExitRequestsPerReport = 2000; - const limitsList = [0, 0, 0, 0, maxValidatorExitRequestsPerReport, 0, 0, 0, 0, 0, 0, 0]; - - return await ethers.deployContract("OracleReportSanityChecker", [lidoLocator, admin, limitsList]); + return await ethers.getContractFactory("OracleReportSanityChecker").then((f) => + f.deploy(lidoLocator, admin, { + exitedValidatorsPerDayLimit: 0n, + appearedValidatorsPerDayLimit: 0n, + annualBalanceIncreaseBPLimit: 0n, + maxValidatorExitRequestsPerReport: 2000, + maxItemsPerExtraDataTransaction: 0n, + maxNodeOperatorsPerExtraDataItem: 0n, + requestTimestampMargin: 0n, + maxPositiveTokenRebase: 0n, + initialSlashingAmountPWei: 0n, + inactivityPenaltiesAmountPWei: 0n, + clBalanceOraclesErrorUpperBPLimit: 0n, + }), + ); } export async function deployVEBO( From 74e8478374a0edded7b4f37900a4e1d7f92143ab Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 13:55:07 +0500 Subject: [PATCH 60/65] fix: comment more details --- contracts/0.8.25/vaults/Dashboard.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index b14e05cff..6a1f50365 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -30,7 +30,7 @@ interface IWstETH is IERC20, IERC20Permit { /** * @title Dashboard - * @notice This contract is a UX-layer for `StakingVault`. + * @notice This contract is a UX-layer for StakingVault and meant to be used as its owner. * This contract improves the vault UX by bundling all functions from the StakingVault and VaultHub * in this single contract. It provides administrative functions for managing the StakingVault, * including funding, withdrawing, minting, burning, and rebalancing operations. From 515504f9f82f878062e46b3c8b5bdd815009e4e6 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 13:58:21 +0500 Subject: [PATCH 61/65] fix: remove unused role --- contracts/0.8.25/vaults/Delegation.sol | 8 +------- contracts/0.8.25/vaults/VaultFactory.sol | 4 ---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index a417b702c..877c574b2 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -38,11 +38,6 @@ contract Delegation is Dashboard { */ bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.Delegation.NodeOperatorManagerRole"); - /** - * @notice Confirms node operator fee. - */ - bytes32 public constant NODE_OPERATOR_FEE_CONFIRM_ROLE = keccak256("vaults.Delegation.NodeOperatorFeeConfirmRole"); - /** * @notice Claims node operator fee. */ @@ -81,7 +76,7 @@ contract Delegation is Dashboard { /** * @notice Initializes the contract: * - sets up the roles; - * - sets the confirm lifetime to 7 days (can be changed later by CURATOR_ROLE and NODE_OPERATOR_MANAGER_ROLE). + * - sets the confirm lifetime to 7 days (can be changed later by DEFAULT_ADMIN_ROLE and NODE_OPERATOR_MANAGER_ROLE). * @dev The msg.sender here is VaultFactory. The VaultFactory is temporarily granted * DEFAULT_ADMIN_ROLE AND NODE_OPERATOR_MANAGER_ROLE to be able to set initial fees and roles in VaultFactory. * All the roles are revoked from VaultFactory by the end of the initialization. @@ -94,7 +89,6 @@ contract Delegation is Dashboard { // at the end of the initialization _grantRole(NODE_OPERATOR_MANAGER_ROLE, _defaultAdmin); _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE); - _setRoleAdmin(NODE_OPERATOR_FEE_CONFIRM_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_FEE_CLAIM_ROLE, NODE_OPERATOR_MANAGER_ROLE); } diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index bb85c2d51..e075c7787 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -24,7 +24,6 @@ struct DelegationConfig { address curatorFeeSetter; address curatorFeeClaimer; address nodeOperatorManager; - address nodeOperatorFeeConfirmer; address nodeOperatorFeeClaimer; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; @@ -86,11 +85,9 @@ contract VaultFactory { delegation.grantRole(delegation.CURATOR_FEE_CLAIM_ROLE(), _delegationConfig.curatorFeeClaimer); delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), _delegationConfig.nodeOperatorManager); delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), _delegationConfig.nodeOperatorFeeClaimer); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), _delegationConfig.nodeOperatorFeeConfirmer); // grant temporary roles to factory for setting fees delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), address(this)); // set fees delegation.setCuratorFeeBP(_delegationConfig.curatorFeeBP); @@ -98,7 +95,6 @@ contract VaultFactory { // revoke temporary roles from factory delegation.revokeRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); - delegation.revokeRole(delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE(), address(this)); delegation.revokeRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), address(this)); delegation.revokeRole(delegation.DEFAULT_ADMIN_ROLE(), address(this)); From 0751a1f3ad0c685f763965f4a42ccc059ab45fba Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 14:19:54 +0500 Subject: [PATCH 62/65] feat(VaultFactory): pass role members array --- contracts/0.8.25/vaults/VaultFactory.sol | 79 +++++++++++++------ .../vaults/delegation/delegation.test.ts | 30 +++---- test/0.8.25/vaults/vaultFactory.test.ts | 25 +++--- .../vaults-happy-path.integration.ts | 26 +++--- 4 files changed, 90 insertions(+), 70 deletions(-) diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index e075c7787..7198b2d82 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -12,22 +12,22 @@ import {Delegation} from "./Delegation.sol"; struct DelegationConfig { address defaultAdmin; - address funder; - address withdrawer; - address minter; - address burner; - address rebalancer; - address depositPauser; - address depositResumer; - address exitRequester; - address disconnecter; - address curatorFeeSetter; - address curatorFeeClaimer; address nodeOperatorManager; - address nodeOperatorFeeClaimer; + uint256 confirmLifetime; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; - uint256 confirmLifetime; + address[] funders; + address[] withdrawers; + address[] minters; + address[] burners; + address[] rebalancers; + address[] depositPausers; + address[] depositResumers; + address[] exitRequesters; + address[] disconnecters; + address[] curatorFeeSetters; + address[] curatorFeeClaimers; + address[] nodeOperatorFeeClaimers; } contract VaultFactory { @@ -71,20 +71,47 @@ contract VaultFactory { // setup roles from config // basic permissions to the staking vault delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), _delegationConfig.defaultAdmin); - delegation.grantRole(delegation.FUND_ROLE(), _delegationConfig.funder); - delegation.grantRole(delegation.WITHDRAW_ROLE(), _delegationConfig.withdrawer); - delegation.grantRole(delegation.MINT_ROLE(), _delegationConfig.minter); - delegation.grantRole(delegation.BURN_ROLE(), _delegationConfig.burner); - delegation.grantRole(delegation.REBALANCE_ROLE(), _delegationConfig.rebalancer); - delegation.grantRole(delegation.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), _delegationConfig.depositPauser); - delegation.grantRole(delegation.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _delegationConfig.depositResumer); - delegation.grantRole(delegation.REQUEST_VALIDATOR_EXIT_ROLE(), _delegationConfig.exitRequester); - delegation.grantRole(delegation.VOLUNTARY_DISCONNECT_ROLE(), _delegationConfig.disconnecter); - // delegation roles - delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), _delegationConfig.curatorFeeSetter); - delegation.grantRole(delegation.CURATOR_FEE_CLAIM_ROLE(), _delegationConfig.curatorFeeClaimer); delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), _delegationConfig.nodeOperatorManager); - delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), _delegationConfig.nodeOperatorFeeClaimer); + + for (uint256 i = 0; i < _delegationConfig.funders.length; i++) { + delegation.grantRole(delegation.FUND_ROLE(), _delegationConfig.funders[i]); + } + for (uint256 i = 0; i < _delegationConfig.withdrawers.length; i++) { + delegation.grantRole(delegation.WITHDRAW_ROLE(), _delegationConfig.withdrawers[i]); + } + for (uint256 i = 0; i < _delegationConfig.minters.length; i++) { + delegation.grantRole(delegation.MINT_ROLE(), _delegationConfig.minters[i]); + } + for (uint256 i = 0; i < _delegationConfig.burners.length; i++) { + delegation.grantRole(delegation.BURN_ROLE(), _delegationConfig.burners[i]); + } + for (uint256 i = 0; i < _delegationConfig.rebalancers.length; i++) { + delegation.grantRole(delegation.REBALANCE_ROLE(), _delegationConfig.rebalancers[i]); + } + for (uint256 i = 0; i < _delegationConfig.depositPausers.length; i++) { + delegation.grantRole(delegation.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), _delegationConfig.depositPausers[i]); + } + for (uint256 i = 0; i < _delegationConfig.depositResumers.length; i++) { + delegation.grantRole(delegation.RESUME_BEACON_CHAIN_DEPOSITS_ROLE(), _delegationConfig.depositResumers[i]); + } + for (uint256 i = 0; i < _delegationConfig.exitRequesters.length; i++) { + delegation.grantRole(delegation.REQUEST_VALIDATOR_EXIT_ROLE(), _delegationConfig.exitRequesters[i]); + } + for (uint256 i = 0; i < _delegationConfig.disconnecters.length; i++) { + delegation.grantRole(delegation.VOLUNTARY_DISCONNECT_ROLE(), _delegationConfig.disconnecters[i]); + } + for (uint256 i = 0; i < _delegationConfig.curatorFeeSetters.length; i++) { + delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), _delegationConfig.curatorFeeSetters[i]); + } + for (uint256 i = 0; i < _delegationConfig.curatorFeeClaimers.length; i++) { + delegation.grantRole(delegation.CURATOR_FEE_CLAIM_ROLE(), _delegationConfig.curatorFeeClaimers[i]); + } + for (uint256 i = 0; i < _delegationConfig.nodeOperatorFeeClaimers.length; i++) { + delegation.grantRole( + delegation.NODE_OPERATOR_FEE_CLAIM_ROLE(), + _delegationConfig.nodeOperatorFeeClaimers[i] + ); + } // grant temporary roles to factory for setting fees delegation.grantRole(delegation.CURATOR_FEE_SET_ROLE(), address(this)); diff --git a/test/0.8.25/vaults/delegation/delegation.test.ts b/test/0.8.25/vaults/delegation/delegation.test.ts index 93ea9bb4e..262ac660e 100644 --- a/test/0.8.25/vaults/delegation/delegation.test.ts +++ b/test/0.8.25/vaults/delegation/delegation.test.ts @@ -39,7 +39,6 @@ describe("Delegation.sol", () => { let curatorFeeSetter: HardhatEthersSigner; let curatorFeeClaimer: HardhatEthersSigner; let nodeOperatorManager: HardhatEthersSigner; - let nodeOperatorFeeConfirmer: HardhatEthersSigner; let nodeOperatorFeeClaimer: HardhatEthersSigner; let stranger: HardhatEthersSigner; @@ -78,7 +77,6 @@ describe("Delegation.sol", () => { curatorFeeSetter, curatorFeeClaimer, nodeOperatorManager, - nodeOperatorFeeConfirmer, nodeOperatorFeeClaimer, stranger, beaconOwner, @@ -110,23 +108,22 @@ describe("Delegation.sol", () => { const vaultCreationTx = await factory.connect(vaultOwner).createVaultWithDelegation( { defaultAdmin: vaultOwner, - funder, - withdrawer, - minter, - burner, - rebalancer, - depositPauser, - depositResumer, - exitRequester, - disconnecter, - curatorFeeSetter, - curatorFeeClaimer, nodeOperatorManager, - nodeOperatorFeeConfirmer, - nodeOperatorFeeClaimer, + confirmLifetime: days(7n), curatorFeeBP: 0n, nodeOperatorFeeBP: 0n, - confirmLifetime: days(7n), + funders: [funder], + withdrawers: [withdrawer], + minters: [minter], + burners: [burner], + rebalancers: [rebalancer], + depositPausers: [depositPauser], + depositResumers: [depositResumer], + exitRequesters: [exitRequester], + disconnecters: [disconnecter], + curatorFeeSetters: [curatorFeeSetter], + curatorFeeClaimers: [curatorFeeClaimer], + nodeOperatorFeeClaimers: [nodeOperatorFeeClaimer], }, "0x", ); @@ -218,7 +215,6 @@ describe("Delegation.sol", () => { await assertSoleMember(curatorFeeSetter, await delegation.CURATOR_FEE_SET_ROLE()); await assertSoleMember(curatorFeeClaimer, await delegation.CURATOR_FEE_CLAIM_ROLE()); await assertSoleMember(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE()); - await assertSoleMember(nodeOperatorFeeConfirmer, await delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE()); await assertSoleMember(nodeOperatorFeeClaimer, await delegation.NODE_OPERATOR_FEE_CLAIM_ROLE()); expect(await delegation.curatorFeeBP()).to.equal(0n); diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index a595103c4..ea356854a 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -109,23 +109,22 @@ describe("VaultFactory.sol", () => { delegationParams = { defaultAdmin: await admin.getAddress(), - funder: await vaultOwner1.getAddress(), - withdrawer: await vaultOwner1.getAddress(), - minter: await vaultOwner1.getAddress(), - burner: await vaultOwner1.getAddress(), - curatorFeeSetter: await vaultOwner1.getAddress(), - curatorFeeClaimer: await vaultOwner1.getAddress(), nodeOperatorManager: await operator.getAddress(), - nodeOperatorFeeConfirmer: await operator.getAddress(), - nodeOperatorFeeClaimer: await operator.getAddress(), - rebalancer: await vaultOwner1.getAddress(), - depositPauser: await vaultOwner1.getAddress(), - depositResumer: await vaultOwner1.getAddress(), - exitRequester: await vaultOwner1.getAddress(), - disconnecter: await vaultOwner1.getAddress(), confirmLifetime: days(7n), curatorFeeBP: 100n, nodeOperatorFeeBP: 200n, + funders: [await vaultOwner1.getAddress()], + withdrawers: [await vaultOwner1.getAddress()], + minters: [await vaultOwner1.getAddress()], + burners: [await vaultOwner1.getAddress()], + curatorFeeSetters: [await vaultOwner1.getAddress()], + curatorFeeClaimers: [await vaultOwner1.getAddress()], + nodeOperatorFeeClaimers: [await operator.getAddress()], + rebalancers: [await vaultOwner1.getAddress()], + depositPausers: [await vaultOwner1.getAddress()], + depositResumers: [await vaultOwner1.getAddress()], + exitRequesters: [await vaultOwner1.getAddress()], + disconnecters: [await vaultOwner1.getAddress()], }; }); diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 5287ac3f6..8df4ea3f2 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -159,23 +159,22 @@ describe("Scenario: Staking Vaults Happy Path", () => { const deployTx = await stakingVaultFactory.connect(owner).createVaultWithDelegation( { defaultAdmin: owner, - funder: curator, - withdrawer: curator, - minter: curator, - burner: curator, - rebalancer: curator, - depositPauser: curator, - depositResumer: curator, - exitRequester: curator, - disconnecter: curator, - curatorFeeSetter: curator, - curatorFeeClaimer: curator, nodeOperatorManager: nodeOperator, - nodeOperatorFeeConfirmer: nodeOperator, - nodeOperatorFeeClaimer: nodeOperator, curatorFeeBP: VAULT_OWNER_FEE, nodeOperatorFeeBP: VAULT_NODE_OPERATOR_FEE, confirmLifetime: days(7n), + funders: [curator], + withdrawers: [curator], + minters: [curator], + burners: [curator], + rebalancers: [curator], + depositPausers: [curator], + depositResumers: [curator], + exitRequesters: [curator], + disconnecters: [curator], + curatorFeeSetters: [curator], + curatorFeeClaimers: [curator], + nodeOperatorFeeClaimers: [nodeOperator], }, "0x", ); @@ -195,7 +194,6 @@ describe("Scenario: Staking Vaults Happy Path", () => { expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_MANAGER_ROLE())).to.be.true; expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_FEE_CLAIM_ROLE())).to.be.true; - expect(await isSoleRoleMember(nodeOperator, await delegation.NODE_OPERATOR_FEE_CONFIRM_ROLE())).to.be.true; expect(await isSoleRoleMember(curator, await delegation.FUND_ROLE())).to.be.true; expect(await isSoleRoleMember(curator, await delegation.WITHDRAW_ROLE())).to.be.true; From 4c7ff7a71f257cd0238e34a09a797de2e819ed5d Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 14:29:14 +0500 Subject: [PATCH 63/65] fix: update comment --- contracts/0.8.25/vaults/Permissions.sol | 2 +- foundry/lib/forge-std | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index c94357c63..51b9f767b 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -232,7 +232,7 @@ abstract contract Permissions is AccessControlConfirmable { } /** - * @dev Checks the DEFAULT_ADMIN_ROLE and transfers the StakingVault ownership. + * @dev Checks the confirming roles and transfers the StakingVault ownership. * @param _newOwner The address to transfer the StakingVault ownership to. */ function _transferStakingVaultOwnership(address _newOwner) internal onlyConfirmed(_confirmingRoles()) { diff --git a/foundry/lib/forge-std b/foundry/lib/forge-std index bf909b22f..8f24d6b04 160000 --- a/foundry/lib/forge-std +++ b/foundry/lib/forge-std @@ -1 +1 @@ -Subproject commit bf909b22fa55e244796dfa920c9639fdffa1c545 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa From a09aa975349496b28f08829e2280128404ad0375 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 14:53:47 +0500 Subject: [PATCH 64/65] =?UTF-8?q?feat:=20rename=20=F0=9F=A7=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0.8.25/utils/AccessControlConfirmable.sol | 60 +++++++++---------- contracts/0.8.25/vaults/Dashboard.sol | 6 +- contracts/0.8.25/vaults/Delegation.sol | 20 +++---- contracts/0.8.25/vaults/Permissions.sol | 4 +- contracts/0.8.25/vaults/VaultFactory.sol | 4 +- .../utils/access-control-confirmable.test.ts | 48 ++++++++------- .../AccessControlConfirmable__Harness.sol | 4 +- .../0.8.25/vaults/dashboard/dashboard.test.ts | 6 +- .../vaults/delegation/delegation.test.ts | 44 +++++++------- .../contracts/Permissions__Harness.sol | 14 ++--- .../VaultFactory__MockPermissions.sol | 10 ++-- .../vaults/permissions/permissions.test.ts | 6 +- test/0.8.25/vaults/vaultFactory.test.ts | 2 +- .../vaults-happy-path.integration.ts | 2 +- 14 files changed, 116 insertions(+), 114 deletions(-) diff --git a/contracts/0.8.25/utils/AccessControlConfirmable.sol b/contracts/0.8.25/utils/AccessControlConfirmable.sol index 1f80d37da..a8ea6b43e 100644 --- a/contracts/0.8.25/utils/AccessControlConfirmable.sol +++ b/contracts/0.8.25/utils/AccessControlConfirmable.sol @@ -22,30 +22,30 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; /** - * @notice Minimal confirmation lifetime in seconds. + * @notice Minimal confirmation expiry in seconds. */ - uint256 public constant MIN_CONFIRM_LIFETIME = 1 days; + uint256 public constant MIN_CONFIRM_EXPIRY = 1 days; /** - * @notice Maximal confirmation lifetime in seconds. + * @notice Maximal confirmation expiry in seconds. */ - uint256 public constant MAX_CONFIRM_LIFETIME = 30 days; + uint256 public constant MAX_CONFIRM_EXPIRY = 30 days; /** - * @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. + * @notice Confirmation expiry in seconds; after this period, the confirmation expires and no longer counts. * @dev We cannot set this to 0 because this means that all confirmations have to be in the same block, - * which can never be guaranteed. And, more importantly, if the `_setLifetime` is restricted by - * the `onlyConfirmed` modifier, the confirmation lifetime will be tricky to change. + * which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` is restricted by + * the `onlyConfirmed` modifier, the confirmation expiry will be tricky to change. * This is why this variable is private, set to a default value of 1 day and cannot be set to 0. */ - uint256 private confirmLifetime = MIN_CONFIRM_LIFETIME; + uint256 private confirmExpiry = MIN_CONFIRM_EXPIRY; /** - * @notice Returns the confirmation lifetime. - * @return The confirmation lifetime in seconds. + * @notice Returns the confirmation expiry. + * @return The confirmation expiry in seconds. */ - function getConfirmLifetime() public view returns (uint256) { - return confirmLifetime; + function getConfirmExpiry() public view returns (uint256) { + return confirmExpiry; } /** @@ -60,7 +60,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * * 2. Confirmation counting: * - Counts the current caller's confirmations if they're a member of any of the specified roles - * - Counts existing confirmations that are not expired, i.e. lifetime is not exceeded + * - Counts existing confirmations that are not expired, i.e. expiry is not exceeded * * 3. Execution: * - If all members of the specified roles have confirmed, executes the function @@ -78,7 +78,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { * * @param _roles Array of role identifiers that must confirm the call in order to execute it * - * @notice Confirmations past their lifetime are not counted and must be recast + * @notice Confirmations past their expiry are not counted and must be recast * @notice Only members of the specified roles can submit confirmations * @notice The order of confirmations does not matter * @@ -90,7 +90,7 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { uint256 numberOfConfirms = 0; bool[] memory deferredConfirms = new bool[](numberOfRoles); bool isRoleMember = false; - uint256 expiryTimestamp = block.timestamp + confirmLifetime; + uint256 expiryTimestamp = block.timestamp + confirmExpiry; for (uint256 i = 0; i < numberOfRoles; ++i) { bytes32 role = _roles[i]; @@ -125,28 +125,28 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { } /** - * @dev Sets the confirmation lifetime. - * Confirmation lifetime is a period during which the confirmation is counted. Once expired, + * @dev Sets the confirmation expiry. + * Confirmation expiry is a period during which the confirmation is counted. Once expired, * the confirmation no longer counts and must be recasted for the confirmation to go through. * @dev Does not retroactively apply to existing confirmations. - * @param _newConfirmLifetime The new confirmation lifetime in seconds. + * @param _newConfirmExpiry The new confirmation expiry in seconds. */ - function _setConfirmLifetime(uint256 _newConfirmLifetime) internal { - if (_newConfirmLifetime < MIN_CONFIRM_LIFETIME || _newConfirmLifetime > MAX_CONFIRM_LIFETIME) - revert ConfirmLifetimeOutOfBounds(); + function _setConfirmExpiry(uint256 _newConfirmExpiry) internal { + if (_newConfirmExpiry < MIN_CONFIRM_EXPIRY || _newConfirmExpiry > MAX_CONFIRM_EXPIRY) + revert ConfirmExpiryOutOfBounds(); - uint256 oldConfirmLifetime = confirmLifetime; - confirmLifetime = _newConfirmLifetime; + uint256 oldConfirmExpiry = confirmExpiry; + confirmExpiry = _newConfirmExpiry; - emit ConfirmLifetimeSet(msg.sender, oldConfirmLifetime, _newConfirmLifetime); + emit ConfirmExpirySet(msg.sender, oldConfirmExpiry, _newConfirmExpiry); } /** - * @dev Emitted when the confirmation lifetime is set. - * @param oldConfirmLifetime The old confirmation lifetime. - * @param newConfirmLifetime The new confirmation lifetime. + * @dev Emitted when the confirmation expiry is set. + * @param oldConfirmExpiry The old confirmation expiry. + * @param newConfirmExpiry The new confirmation expiry. */ - event ConfirmLifetimeSet(address indexed sender, uint256 oldConfirmLifetime, uint256 newConfirmLifetime); + event ConfirmExpirySet(address indexed sender, uint256 oldConfirmExpiry, uint256 newConfirmExpiry); /** * @dev Emitted when a role member confirms. @@ -158,9 +158,9 @@ abstract contract AccessControlConfirmable is AccessControlEnumerable { event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 expiryTimestamp, bytes data); /** - * @dev Thrown when attempting to set confirmation lifetime out of bounds. + * @dev Thrown when attempting to set confirmation expiry out of bounds. */ - error ConfirmLifetimeOutOfBounds(); + error ConfirmExpiryOutOfBounds(); /** * @dev Thrown when a caller without a required role attempts to confirm. diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index 6a1f50365..2ee30f7d1 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -89,14 +89,14 @@ contract Dashboard is Permissions { /** * @notice Initializes the contract * @param _defaultAdmin Address of the default admin - * @param _confirmLifetime Confirm lifetime in seconds + * @param _confirmExpiry Confirm expiry in seconds */ - function initialize(address _defaultAdmin, uint256 _confirmLifetime) external virtual { + function initialize(address _defaultAdmin, uint256 _confirmExpiry) external virtual { // reduces gas cost for `mintWsteth` // invariant: dashboard does not hold stETH on its balance STETH.approve(address(WSTETH), type(uint256).max); - _initialize(_defaultAdmin, _confirmLifetime); + _initialize(_defaultAdmin, _confirmExpiry); } // ==================== View Functions ==================== diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index 877c574b2..18884561d 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -31,7 +31,7 @@ contract Delegation is Dashboard { /** * @notice Node operator manager role: - * - confirms confirm lifetime; + * - confirms confirm expiry; * - confirms ownership transfer; * - assigns NODE_OPERATOR_FEE_CONFIRM_ROLE; * - assigns NODE_OPERATOR_FEE_CLAIM_ROLE. @@ -76,13 +76,13 @@ contract Delegation is Dashboard { /** * @notice Initializes the contract: * - sets up the roles; - * - sets the confirm lifetime to 7 days (can be changed later by DEFAULT_ADMIN_ROLE and NODE_OPERATOR_MANAGER_ROLE). + * - sets the confirm expiry to 7 days (can be changed later by DEFAULT_ADMIN_ROLE and NODE_OPERATOR_MANAGER_ROLE). * @dev The msg.sender here is VaultFactory. The VaultFactory is temporarily granted * DEFAULT_ADMIN_ROLE AND NODE_OPERATOR_MANAGER_ROLE to be able to set initial fees and roles in VaultFactory. * All the roles are revoked from VaultFactory by the end of the initialization. */ - function initialize(address _defaultAdmin, uint256 _confirmLifetime) external override { - _initialize(_defaultAdmin, _confirmLifetime); + function initialize(address _defaultAdmin, uint256 _confirmExpiry) external override { + _initialize(_defaultAdmin, _confirmExpiry); // the next line implies that the msg.sender is an operator // however, the msg.sender is the VaultFactory, and the role will be revoked @@ -146,13 +146,13 @@ contract Delegation is Dashboard { } /** - * @notice Sets the confirm lifetime. - * Confirm lifetime is a period during which the confirm is counted. Once the period is over, + * @notice Sets the confirm expiry. + * Confirm expiry is a period during which the confirm is counted. Once the period is over, * the confirm is considered expired, no longer counts and must be recasted. - * @param _newConfirmLifetime The new confirm lifetime in seconds. + * @param _newConfirmExpiry The new confirm expiry in seconds. */ - function setConfirmLifetime(uint256 _newConfirmLifetime) external onlyConfirmed(_confirmingRoles()) { - _setConfirmLifetime(_newConfirmLifetime); + function setConfirmExpiry(uint256 _newConfirmExpiry) external onlyConfirmed(_confirmingRoles()) { + _setConfirmExpiry(_newConfirmExpiry); } /** @@ -253,7 +253,7 @@ contract Delegation is Dashboard { /** * @notice Returns the roles that can: - * - change the confirm lifetime; + * - change the confirm expiry; * - set the curator fee; * - set the node operator fee; * - transfer the ownership of the StakingVault. diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 51b9f767b..e55604359 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -90,7 +90,7 @@ abstract contract Permissions is AccessControlConfirmable { _SELF = address(this); } - function _initialize(address _defaultAdmin, uint256 _confirmLifetime) internal { + function _initialize(address _defaultAdmin, uint256 _confirmExpiry) internal { if (initialized) revert AlreadyInitialized(); if (address(this) == _SELF) revert NonProxyCallsForbidden(); if (_defaultAdmin == address(0)) revert ZeroArgument("_defaultAdmin"); @@ -99,7 +99,7 @@ abstract contract Permissions is AccessControlConfirmable { vaultHub = VaultHub(stakingVault().vaultHub()); _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setConfirmLifetime(_confirmLifetime); + _setConfirmExpiry(_confirmExpiry); emit Initialized(_defaultAdmin); } diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 7198b2d82..3a6509931 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -13,7 +13,7 @@ import {Delegation} from "./Delegation.sol"; struct DelegationConfig { address defaultAdmin; address nodeOperatorManager; - uint256 confirmLifetime; + uint256 confirmExpiry; uint16 curatorFeeBP; uint16 nodeOperatorFeeBP; address[] funders; @@ -66,7 +66,7 @@ contract VaultFactory { ); // initialize Delegation - delegation.initialize(address(this), _delegationConfig.confirmLifetime); + delegation.initialize(address(this), _delegationConfig.confirmExpiry); // setup roles from config // basic permissions to the staking vault diff --git a/test/0.8.25/utils/access-control-confirmable.test.ts b/test/0.8.25/utils/access-control-confirmable.test.ts index 7b0e2357d..3a97c8ccd 100644 --- a/test/0.8.25/utils/access-control-confirmable.test.ts +++ b/test/0.8.25/utils/access-control-confirmable.test.ts @@ -18,7 +18,7 @@ describe("AccessControlConfirmable.sol", () => { [admin, stranger, role1Member, role2Member] = await ethers.getSigners(); harness = await ethers.deployContract("AccessControlConfirmable__Harness", [admin], admin); - expect(await harness.getConfirmLifetime()).to.equal(await harness.MIN_CONFIRM_LIFETIME()); + expect(await harness.getConfirmExpiry()).to.equal(await harness.MIN_CONFIRM_EXPIRY()); expect(await harness.hasRole(await harness.DEFAULT_ADMIN_ROLE(), admin)).to.be.true; expect(await harness.getRoleMemberCount(await harness.DEFAULT_ADMIN_ROLE())).to.equal(1); @@ -33,14 +33,14 @@ describe("AccessControlConfirmable.sol", () => { context("constants", () => { it("returns the correct constants", async () => { - expect(await harness.MIN_CONFIRM_LIFETIME()).to.equal(days(1n)); - expect(await harness.MAX_CONFIRM_LIFETIME()).to.equal(days(30n)); + expect(await harness.MIN_CONFIRM_EXPIRY()).to.equal(days(1n)); + expect(await harness.MAX_CONFIRM_EXPIRY()).to.equal(days(30n)); }); }); - context("getConfirmLifetime()", () => { - it("returns the minimal lifetime initially", async () => { - expect(await harness.getConfirmLifetime()).to.equal(await harness.MIN_CONFIRM_LIFETIME()); + context("getConfirmExpiry()", () => { + it("returns the minimal expiry initially", async () => { + expect(await harness.getConfirmExpiry()).to.equal(await harness.MIN_CONFIRM_EXPIRY()); }); }); @@ -50,24 +50,26 @@ describe("AccessControlConfirmable.sol", () => { }); }); - context("setConfirmLifetime()", () => { - it("sets the confirm lifetime", async () => { - const oldLifetime = await harness.getConfirmLifetime(); - const newLifetime = days(14n); - await expect(harness.setConfirmLifetime(newLifetime)) - .to.emit(harness, "ConfirmLifetimeSet") - .withArgs(admin, oldLifetime, newLifetime); - expect(await harness.getConfirmLifetime()).to.equal(newLifetime); + context("setConfirmExpiry()", () => { + it("sets the confirm expiry", async () => { + const oldExpiry = await harness.getConfirmExpiry(); + const newExpiry = days(14n); + await expect(harness.setConfirmExpiry(newExpiry)) + .to.emit(harness, "ConfirmExpirySet") + .withArgs(admin, oldExpiry, newExpiry); + expect(await harness.getConfirmExpiry()).to.equal(newExpiry); }); - it("reverts if the new lifetime is out of bounds", async () => { - await expect( - harness.setConfirmLifetime((await harness.MIN_CONFIRM_LIFETIME()) - 1n), - ).to.be.revertedWithCustomError(harness, "ConfirmLifetimeOutOfBounds"); + it("reverts if the new expiry is out of bounds", async () => { + await expect(harness.setConfirmExpiry((await harness.MIN_CONFIRM_EXPIRY()) - 1n)).to.be.revertedWithCustomError( + harness, + "ConfirmExpiryOutOfBounds", + ); - await expect( - harness.setConfirmLifetime((await harness.MAX_CONFIRM_LIFETIME()) + 1n), - ).to.be.revertedWithCustomError(harness, "ConfirmLifetimeOutOfBounds"); + await expect(harness.setConfirmExpiry((await harness.MAX_CONFIRM_EXPIRY()) + 1n)).to.be.revertedWithCustomError( + harness, + "ConfirmExpiryOutOfBounds", + ); }); }); @@ -94,7 +96,7 @@ describe("AccessControlConfirmable.sol", () => { it("doesn't execute if the confirmation has expired", async () => { const oldNumber = await harness.number(); const newNumber = 1; - const expiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmLifetime()); + const expiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmExpiry()); const msgData = harness.interface.encodeFunctionData("setNumber", [newNumber]); await expect(harness.connect(role1Member).setNumber(newNumber)) @@ -106,7 +108,7 @@ describe("AccessControlConfirmable.sol", () => { await advanceChainTime(expiryTimestamp + 1n); - const newExpiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmLifetime()); + const newExpiryTimestamp = (await getNextBlockTimestamp()) + (await harness.getConfirmExpiry()); await expect(harness.connect(role2Member).setNumber(newNumber)) .to.emit(harness, "RoleMemberConfirmed") .withArgs(role2Member, await harness.ROLE_2(), newExpiryTimestamp, msgData); diff --git a/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol b/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol index 3a37e5988..459ab5d44 100644 --- a/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol +++ b/test/0.8.25/utils/contracts/AccessControlConfirmable__Harness.sol @@ -22,8 +22,8 @@ contract AccessControlConfirmable__Harness is AccessControlConfirmable { return roles; } - function setConfirmLifetime(uint256 _confirmLifetime) external { - _setConfirmLifetime(_confirmLifetime); + function setConfirmExpiry(uint256 _confirmExpiry) external { + _setConfirmExpiry(_confirmExpiry); } function setNumber(uint256 _number) external onlyConfirmed(confirmingRoles()) { diff --git a/test/0.8.25/vaults/dashboard/dashboard.test.ts b/test/0.8.25/vaults/dashboard/dashboard.test.ts index 3b7eaad4e..25cf13c3d 100644 --- a/test/0.8.25/vaults/dashboard/dashboard.test.ts +++ b/test/0.8.25/vaults/dashboard/dashboard.test.ts @@ -45,7 +45,7 @@ describe("Dashboard.sol", () => { let dashboard: Dashboard; let dashboardAddress: string; - const confirmLifetime = days(7n); + const confirmExpiry = days(7n); let originalState: string; @@ -127,7 +127,7 @@ describe("Dashboard.sol", () => { context("initialize", () => { it("reverts if already initialized", async () => { - await expect(dashboard.initialize(vaultOwner, confirmLifetime)).to.be.revertedWithCustomError( + await expect(dashboard.initialize(vaultOwner, confirmExpiry)).to.be.revertedWithCustomError( dashboard, "AlreadyInitialized", ); @@ -136,7 +136,7 @@ describe("Dashboard.sol", () => { it("reverts if called on the implementation", async () => { const dashboard_ = await ethers.deployContract("Dashboard", [weth, lidoLocator]); - await expect(dashboard_.initialize(vaultOwner, confirmLifetime)).to.be.revertedWithCustomError( + await expect(dashboard_.initialize(vaultOwner, confirmExpiry)).to.be.revertedWithCustomError( dashboard_, "NonProxyCallsForbidden", ); diff --git a/test/0.8.25/vaults/delegation/delegation.test.ts b/test/0.8.25/vaults/delegation/delegation.test.ts index 262ac660e..158453916 100644 --- a/test/0.8.25/vaults/delegation/delegation.test.ts +++ b/test/0.8.25/vaults/delegation/delegation.test.ts @@ -109,7 +109,7 @@ describe("Delegation.sol", () => { { defaultAdmin: vaultOwner, nodeOperatorManager, - confirmLifetime: days(7n), + confirmExpiry: days(7n), curatorFeeBP: 0n, nodeOperatorFeeBP: 0n, funders: [funder], @@ -235,32 +235,32 @@ describe("Delegation.sol", () => { }); }); - context("setConfirmLifetime", () => { - it("reverts if the caller is not a member of the confirm lifetime committee", async () => { - await expect(delegation.connect(stranger).setConfirmLifetime(days(10n))).to.be.revertedWithCustomError( + context("setConfirmExpiry", () => { + it("reverts if the caller is not a member of the confirm expiry committee", async () => { + await expect(delegation.connect(stranger).setConfirmExpiry(days(10n))).to.be.revertedWithCustomError( delegation, "SenderNotMember", ); }); - it("sets the new confirm lifetime", async () => { - const oldConfirmLifetime = await delegation.getConfirmLifetime(); - const newConfirmLifetime = days(10n); - const msgData = delegation.interface.encodeFunctionData("setConfirmLifetime", [newConfirmLifetime]); - let confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + it("sets the new confirm expiry", async () => { + const oldConfirmExpiry = await delegation.getConfirmExpiry(); + const newConfirmExpiry = days(10n); + const msgData = delegation.interface.encodeFunctionData("setConfirmExpiry", [newConfirmExpiry]); + let confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); - await expect(delegation.connect(vaultOwner).setConfirmLifetime(newConfirmLifetime)) + await expect(delegation.connect(vaultOwner).setConfirmExpiry(newConfirmExpiry)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), confirmTimestamp, msgData); - confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); - await expect(delegation.connect(nodeOperatorManager).setConfirmLifetime(newConfirmLifetime)) + confirmTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); + await expect(delegation.connect(nodeOperatorManager).setConfirmExpiry(newConfirmExpiry)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), confirmTimestamp, msgData) - .and.to.emit(delegation, "ConfirmLifetimeSet") - .withArgs(nodeOperatorManager, oldConfirmLifetime, newConfirmLifetime); + .and.to.emit(delegation, "ConfirmExpirySet") + .withArgs(nodeOperatorManager, oldConfirmExpiry, newConfirmExpiry); - expect(await delegation.getConfirmLifetime()).to.equal(newConfirmLifetime); + expect(await delegation.getConfirmExpiry()).to.equal(newConfirmExpiry); }); }); @@ -629,7 +629,7 @@ describe("Delegation.sol", () => { it("requires both default admin and operator manager to set the operator fee and emits the RoleMemberConfirmed event", async () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) @@ -640,7 +640,7 @@ describe("Delegation.sol", () => { // check confirm expect(await delegation.confirmations(msgData, await delegation.DEFAULT_ADMIN_ROLE())).to.equal(expiryTimestamp); - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData) @@ -667,7 +667,7 @@ describe("Delegation.sol", () => { const previousOperatorFee = await delegation.nodeOperatorFeeBP(); const newOperatorFee = 1000n; const msgData = delegation.interface.encodeFunctionData("setNodeOperatorFeeBP", [newOperatorFee]); - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") @@ -679,7 +679,7 @@ describe("Delegation.sol", () => { // move time forward await advanceChainTime(days(7n) + 1n); - const expectedExpiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + const expectedExpiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); expect(expectedExpiryTimestamp).to.be.greaterThan(expiryTimestamp + days(7n)); await expect(delegation.connect(nodeOperatorManager).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") @@ -693,7 +693,7 @@ describe("Delegation.sol", () => { ); // curator has to confirm again - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); await expect(delegation.connect(vaultOwner).setNodeOperatorFeeBP(newOperatorFee)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData) @@ -715,14 +715,14 @@ describe("Delegation.sol", () => { it("requires both curator and operator to transfer ownership and emits the RoleMemberConfirmd event", async () => { const newOwner = certainAddress("newOwner"); const msgData = delegation.interface.encodeFunctionData("transferStakingVaultOwnership", [newOwner]); - let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + let expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); await expect(delegation.connect(vaultOwner).transferStakingVaultOwnership(newOwner)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(vaultOwner, await delegation.DEFAULT_ADMIN_ROLE(), expiryTimestamp, msgData); // owner is unchanged expect(await vault.owner()).to.equal(delegation); - expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmLifetime()); + expiryTimestamp = (await getNextBlockTimestamp()) + (await delegation.getConfirmExpiry()); await expect(delegation.connect(nodeOperatorManager).transferStakingVaultOwnership(newOwner)) .to.emit(delegation, "RoleMemberConfirmed") .withArgs(nodeOperatorManager, await delegation.NODE_OPERATOR_MANAGER_ROLE(), expiryTimestamp, msgData); diff --git a/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol index 390097bfb..a2cad94e1 100644 --- a/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol +++ b/test/0.8.25/vaults/permissions/contracts/Permissions__Harness.sol @@ -6,13 +6,13 @@ pragma solidity ^0.8.0; import {Permissions} from "contracts/0.8.25/vaults/Permissions.sol"; contract Permissions__Harness is Permissions { - function initialize(address _defaultAdmin, uint256 _confirmLifetime) external { - _initialize(_defaultAdmin, _confirmLifetime); + function initialize(address _defaultAdmin, uint256 _confirmExpiry) external { + _initialize(_defaultAdmin, _confirmExpiry); } - function revertDoubleInitialize(address _defaultAdmin, uint256 _confirmLifetime) external { - _initialize(_defaultAdmin, _confirmLifetime); - _initialize(_defaultAdmin, _confirmLifetime); + function revertDoubleInitialize(address _defaultAdmin, uint256 _confirmExpiry) external { + _initialize(_defaultAdmin, _confirmExpiry); + _initialize(_defaultAdmin, _confirmExpiry); } function confirmingRoles() external pure returns (bytes32[] memory) { @@ -59,7 +59,7 @@ contract Permissions__Harness is Permissions { _transferStakingVaultOwnership(_newOwner); } - function setConfirmLifetime(uint256 _newConfirmLifetime) external { - _setConfirmLifetime(_newConfirmLifetime); + function setConfirmExpiry(uint256 _newConfirmExpiry) external { + _setConfirmExpiry(_newConfirmExpiry); } } diff --git a/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol index 61371970d..fe0994484 100644 --- a/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol +++ b/test/0.8.25/vaults/permissions/contracts/VaultFactory__MockPermissions.sol @@ -12,7 +12,7 @@ import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.so struct PermissionsConfig { address defaultAdmin; address nodeOperator; - uint256 confirmLifetime; + uint256 confirmExpiry; address funder; address withdrawer; address minter; @@ -56,7 +56,7 @@ contract VaultFactory__MockPermissions { vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); // initialize Permissions - permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + permissions.initialize(address(this), _permissionsConfig.confirmExpiry); // setup roles permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); @@ -91,9 +91,9 @@ contract VaultFactory__MockPermissions { vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); // initialize Permissions - permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + permissions.initialize(address(this), _permissionsConfig.confirmExpiry); // should revert here - permissions.initialize(address(this), _permissionsConfig.confirmLifetime); + permissions.initialize(address(this), _permissionsConfig.confirmExpiry); // setup roles permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); @@ -128,7 +128,7 @@ contract VaultFactory__MockPermissions { vault.initialize(address(permissions), _permissionsConfig.nodeOperator, _stakingVaultInitializerExtraParams); // should revert here - permissions.initialize(address(0), _permissionsConfig.confirmLifetime); + permissions.initialize(address(0), _permissionsConfig.confirmExpiry); // setup roles permissions.grantRole(permissions.DEFAULT_ADMIN_ROLE(), _permissionsConfig.defaultAdmin); diff --git a/test/0.8.25/vaults/permissions/permissions.test.ts b/test/0.8.25/vaults/permissions/permissions.test.ts index 84cd5909e..2aa692985 100644 --- a/test/0.8.25/vaults/permissions/permissions.test.ts +++ b/test/0.8.25/vaults/permissions/permissions.test.ts @@ -87,7 +87,7 @@ describe("Permissions", () => { { defaultAdmin, nodeOperator, - confirmLifetime: days(7n), + confirmExpiry: days(7n), funder, withdrawer, minter, @@ -156,7 +156,7 @@ describe("Permissions", () => { { defaultAdmin, nodeOperator, - confirmLifetime: days(7n), + confirmExpiry: days(7n), funder, withdrawer, minter, @@ -186,7 +186,7 @@ describe("Permissions", () => { { defaultAdmin, nodeOperator, - confirmLifetime: days(7n), + confirmExpiry: days(7n), funder, withdrawer, minter, diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index ea356854a..626f0a5bd 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -110,7 +110,7 @@ describe("VaultFactory.sol", () => { delegationParams = { defaultAdmin: await admin.getAddress(), nodeOperatorManager: await operator.getAddress(), - confirmLifetime: days(7n), + confirmExpiry: days(7n), curatorFeeBP: 100n, nodeOperatorFeeBP: 200n, funders: [await vaultOwner1.getAddress()], diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 7e7bf69b7..6f1c82bc9 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -159,7 +159,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { nodeOperatorManager: nodeOperator, curatorFeeBP: VAULT_OWNER_FEE, nodeOperatorFeeBP: VAULT_NODE_OPERATOR_FEE, - confirmLifetime: days(7n), + confirmExpiry: days(7n), funders: [curator], withdrawers: [curator], minters: [curator], From 2a004a17c0eee8703e4e78499ba39e73e76ba328 Mon Sep 17 00:00:00 2001 From: failingtwice Date: Wed, 19 Feb 2025 19:35:49 +0500 Subject: [PATCH 65/65] fix(VaultHub): rename mint/burn --- contracts/0.8.25/vaults/Permissions.sol | 4 ++-- contracts/0.8.25/vaults/VaultHub.sol | 8 ++++---- .../vaults/contracts/VaultHub__MockForVault.sol | 4 ++-- .../contracts/VaultHub__MockForDashboard.sol | 4 ++-- .../contracts/VaultHub__MockForDelegation.sol | 4 ++-- .../contracts/VaultHub__MockPermissions.sol | 4 ++-- .../vaults/vaulthub/vaulthub.pausable.test.ts | 15 ++++++--------- 7 files changed, 20 insertions(+), 23 deletions(-) diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index e55604359..aceaec766 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -181,7 +181,7 @@ abstract contract Permissions is AccessControlConfirmable { * @dev The zero checks for parameters are performed in the VaultHub contract. */ function _mintShares(address _recipient, uint256 _shares) internal onlyRole(MINT_ROLE) { - vaultHub.mintSharesBackedByVault(address(stakingVault()), _recipient, _shares); + vaultHub.mintShares(address(stakingVault()), _recipient, _shares); } /** @@ -190,7 +190,7 @@ abstract contract Permissions is AccessControlConfirmable { * @dev The zero check for parameters is performed in the VaultHub contract. */ function _burnShares(uint256 _shares) internal onlyRole(BURN_ROLE) { - vaultHub.burnSharesBackedByVault(address(stakingVault()), _shares); + vaultHub.burnShares(address(stakingVault()), _shares); } /** diff --git a/contracts/0.8.25/vaults/VaultHub.sol b/contracts/0.8.25/vaults/VaultHub.sol index 3c8d10b47..d09516adb 100644 --- a/contracts/0.8.25/vaults/VaultHub.sol +++ b/contracts/0.8.25/vaults/VaultHub.sol @@ -213,7 +213,7 @@ abstract contract VaultHub is PausableUntilWithRoles { /// @param _recipient address of the receiver /// @param _amountOfShares amount of stETH shares to mint /// @dev msg.sender should be vault's owner - function mintSharesBackedByVault(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed { + function mintShares(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed { if (_vault == address(0)) revert ZeroArgument("_vault"); if (_recipient == address(0)) revert ZeroArgument("_recipient"); if (_amountOfShares == 0) revert ZeroArgument("_amountOfShares"); @@ -252,7 +252,7 @@ abstract contract VaultHub is PausableUntilWithRoles { /// @param _amountOfShares amount of shares to burn /// @dev msg.sender should be vault's owner /// @dev VaultHub must have all the stETH on its balance - function burnSharesBackedByVault(address _vault, uint256 _amountOfShares) public whenResumed { + function burnShares(address _vault, uint256 _amountOfShares) public whenResumed { if (_vault == address(0)) revert ZeroArgument("_vault"); if (_amountOfShares == 0) revert ZeroArgument("_amountOfShares"); _vaultAuth(_vault, "burn"); @@ -271,10 +271,10 @@ abstract contract VaultHub is PausableUntilWithRoles { /// @notice separate burn function for EOA vault owners; requires vaultHub to be approved to transfer stETH /// @dev msg.sender should be vault's owner - function transferAndBurnSharesBackedByVault(address _vault, uint256 _amountOfShares) external { + function transferAndBurnShares(address _vault, uint256 _amountOfShares) external { STETH.transferSharesFrom(msg.sender, address(this), _amountOfShares); - burnSharesBackedByVault(_vault, _amountOfShares); + burnShares(_vault, _amountOfShares); } /// @notice force rebalance of the vault to have sufficient reserve ratio diff --git a/test/0.8.25/vaults/contracts/VaultHub__MockForVault.sol b/test/0.8.25/vaults/contracts/VaultHub__MockForVault.sol index 430e52de7..4daf8c990 100644 --- a/test/0.8.25/vaults/contracts/VaultHub__MockForVault.sol +++ b/test/0.8.25/vaults/contracts/VaultHub__MockForVault.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.25; contract VaultHub__MockForVault { - function mintSharesBackedByVault(address _recipient, uint256 _amountOfShares) external returns (uint256 locked) {} + function mintShares(address _recipient, uint256 _amountOfShares) external returns (uint256 locked) {} - function burnSharesBackedByVault(uint256 _amountOfShares) external {} + function burnShares(uint256 _amountOfShares) external {} function rebalance() external payable {} } diff --git a/test/0.8.25/vaults/dashboard/contracts/VaultHub__MockForDashboard.sol b/test/0.8.25/vaults/dashboard/contracts/VaultHub__MockForDashboard.sol index 95781fb4a..7e7d02ed8 100644 --- a/test/0.8.25/vaults/dashboard/contracts/VaultHub__MockForDashboard.sol +++ b/test/0.8.25/vaults/dashboard/contracts/VaultHub__MockForDashboard.sol @@ -41,7 +41,7 @@ contract VaultHub__MockForDashboard { emit Mock__VaultDisconnected(vault); } - function mintSharesBackedByVault(address vault, address recipient, uint256 amount) external { + function mintShares(address vault, address recipient, uint256 amount) external { if (vault == address(0)) revert ZeroArgument("_vault"); if (recipient == address(0)) revert ZeroArgument("recipient"); if (amount == 0) revert ZeroArgument("amount"); @@ -50,7 +50,7 @@ contract VaultHub__MockForDashboard { vaultSockets[vault].sharesMinted = uint96(vaultSockets[vault].sharesMinted + amount); } - function burnSharesBackedByVault(address _vault, uint256 _amountOfShares) external { + function burnShares(address _vault, uint256 _amountOfShares) external { if (_vault == address(0)) revert ZeroArgument("_vault"); if (_amountOfShares == 0) revert ZeroArgument("_amountOfShares"); steth.burnExternalShares(_amountOfShares); diff --git a/test/0.8.25/vaults/delegation/contracts/VaultHub__MockForDelegation.sol b/test/0.8.25/vaults/delegation/contracts/VaultHub__MockForDelegation.sol index 3a49e852b..5108f8b8e 100644 --- a/test/0.8.25/vaults/delegation/contracts/VaultHub__MockForDelegation.sol +++ b/test/0.8.25/vaults/delegation/contracts/VaultHub__MockForDelegation.sol @@ -20,11 +20,11 @@ contract VaultHub__MockForDelegation { emit Mock__VaultDisconnected(vault); } - function mintSharesBackedByVault(address /* vault */, address recipient, uint256 amount) external { + function mintShares(address /* vault */, address recipient, uint256 amount) external { steth.mint(recipient, amount); } - function burnSharesBackedByVault(address /* vault */, uint256 amount) external { + function burnShares(address /* vault */, uint256 amount) external { steth.burn(amount); } diff --git a/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol index 0322752b0..6ee7437ef 100644 --- a/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol +++ b/test/0.8.25/vaults/permissions/contracts/VaultHub__MockPermissions.sol @@ -9,11 +9,11 @@ contract VaultHub__MockPermissions { event Mock__Rebalanced(uint256 _ether); event Mock__VoluntaryDisconnect(address indexed _stakingVault); - function mintSharesBackedByVault(address _stakingVault, address _recipient, uint256 _shares) external { + function mintShares(address _stakingVault, address _recipient, uint256 _shares) external { emit Mock__SharesMinted(_stakingVault, _recipient, _shares); } - function burnSharesBackedByVault(address _stakingVault, uint256 _shares) external { + function burnShares(address _stakingVault, uint256 _shares) external { emit Mock__SharesBurned(_stakingVault, _shares); } diff --git a/test/0.8.25/vaults/vaulthub/vaulthub.pausable.test.ts b/test/0.8.25/vaults/vaulthub/vaulthub.pausable.test.ts index feb145fa0..d8f3ba6f4 100644 --- a/test/0.8.25/vaults/vaulthub/vaulthub.pausable.test.ts +++ b/test/0.8.25/vaults/vaulthub/vaulthub.pausable.test.ts @@ -157,28 +157,25 @@ describe("VaultHub.sol:pausableUntil", () => { await expect(vaultHub.voluntaryDisconnect(user)).to.be.revertedWithCustomError(vaultHub, "ResumedExpected"); }); - it("reverts mintSharesBackedByVault() if paused", async () => { - await expect(vaultHub.mintSharesBackedByVault(stranger, user, 1000n)).to.be.revertedWithCustomError( + it("reverts mintShares() if paused", async () => { + await expect(vaultHub.mintShares(stranger, user, 1000n)).to.be.revertedWithCustomError( vaultHub, "ResumedExpected", ); }); - it("reverts burnSharesBackedByVault() if paused", async () => { - await expect(vaultHub.burnSharesBackedByVault(stranger, 1000n)).to.be.revertedWithCustomError( - vaultHub, - "ResumedExpected", - ); + it("reverts burnShares() if paused", async () => { + await expect(vaultHub.burnShares(stranger, 1000n)).to.be.revertedWithCustomError(vaultHub, "ResumedExpected"); }); it("reverts rebalance() if paused", async () => { await expect(vaultHub.rebalance()).to.be.revertedWithCustomError(vaultHub, "ResumedExpected"); }); - it("reverts transferAndBurnSharesBackedByVault() if paused", async () => { + it("reverts transferAndBurnShares() if paused", async () => { await steth.connect(user).approve(vaultHub, 1000n); - await expect(vaultHub.transferAndBurnSharesBackedByVault(stranger, 1000n)).to.be.revertedWithCustomError( + await expect(vaultHub.transferAndBurnShares(stranger, 1000n)).to.be.revertedWithCustomError( vaultHub, "ResumedExpected", );