diff --git a/contracts/common/interfaces/ICSModule.sol b/contracts/common/interfaces/ICSModule.sol new file mode 100644 index 000000000..a918a965b --- /dev/null +++ b/contracts/common/interfaces/ICSModule.sol @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +// For full version see: https://github.com/lidofinance/community-staking-module/blob/develop/src/interfaces/ICSModule.sol +import { IStakingModule } from "../../0.8.9/interfaces/IStakingModule.sol"; + +pragma solidity 0.8.9; + + +struct NodeOperator { + // All the counters below are used together e.g. in the _updateDepositableValidatorsCount + /* 1 */ uint32 totalAddedKeys; // @dev increased and decreased when removed + /* 1 */ uint32 totalWithdrawnKeys; // @dev only increased + /* 1 */ uint32 totalDepositedKeys; // @dev only increased + /* 1 */ uint32 totalVettedKeys; // @dev both increased and decreased + /* 1 */ uint32 stuckValidatorsCount; // @dev both increased and decreased + /* 1 */ uint32 depositableValidatorsCount; // @dev any value + /* 1 */ uint32 targetLimit; + /* 1 */ uint8 targetLimitMode; + /* 2 */ uint32 totalExitedKeys; // @dev only increased except for the unsafe updates + /* 2 */ uint32 enqueuedCount; // Tracks how many places are occupied by the node operator's keys in the queue. + /* 2 */ address managerAddress; + /* 3 */ address proposedManagerAddress; + /* 4 */ address rewardAddress; + /* 5 */ address proposedRewardAddress; + /* 5 */ bool extendedManagerPermissions; +} + +struct NodeOperatorManagementProperties { + address managerAddress; + address rewardAddress; + bool extendedManagerPermissions; +} + +/// @title Lido's Community Staking Module interface +interface ICSModule is IStakingModule +{ + error NodeOperatorDoesNotExist(); + error ZeroRewardAddress(); + + /// @notice Gets node operator non-withdrawn keys + /// @param nodeOperatorId ID of the node operator + /// @return Non-withdrawn keys count + function getNodeOperatorNonWithdrawnKeys( + uint256 nodeOperatorId + ) external view returns (uint256); + + /// @notice Returns the node operator by id + /// @param nodeOperatorId Node Operator id + function getNodeOperator( + uint256 nodeOperatorId + ) external view returns (NodeOperator memory); + + /// @notice Gets node operator signing keys + /// @param nodeOperatorId ID of the node operator + /// @param startIndex Index of the first key + /// @param keysCount Count of keys to get + /// @return Signing keys + function getSigningKeys( + uint256 nodeOperatorId, + uint256 startIndex, + uint256 keysCount + ) external view returns (bytes memory); + + /// @notice Gets node operator signing keys with signatures + /// @param nodeOperatorId ID of the node operator + /// @param startIndex Index of the first key + /// @param keysCount Count of keys to get + /// @return keys Signing keys + /// @return signatures Signatures of (deposit_message, domain) tuples + function getSigningKeysWithSignatures( + uint256 nodeOperatorId, + uint256 startIndex, + uint256 keysCount + ) external view returns (bytes memory keys, bytes memory signatures); + + /// @notice Report node operator's key as slashed and apply initial slashing penalty. + /// @param nodeOperatorId Operator ID in the module. + /// @param keyIndex Index of the slashed key in the node operator's keys. + function submitInitialSlashing( + uint256 nodeOperatorId, + uint256 keyIndex + ) external; + + /// @notice Report node operator's key as withdrawn and settle withdrawn amount. + /// @param nodeOperatorId Operator ID in the module. + /// @param keyIndex Index of the withdrawn key in the node operator's keys. + /// @param amount Amount of withdrawn ETH in wei. + /// @param isSlashed Validator is slashed or not + function submitWithdrawal( + uint256 nodeOperatorId, + uint256 keyIndex, + uint256 amount, + bool isSlashed + ) external; + + function depositETH(uint256 nodeOperatorId) external payable; +} \ No newline at end of file diff --git a/lib/oracle.ts b/lib/oracle.ts index 9bfc0aa19..df3c18391 100644 --- a/lib/oracle.ts +++ b/lib/oracle.ts @@ -2,10 +2,12 @@ import { bigintToHex } from "bigint-conversion"; import { keccak256, ZeroHash } from "ethers"; import { ethers } from "hardhat"; -import { AccountingOracle, HashConsensus } from "typechain-types"; +import { AccountingOracle, HashConsensus, OracleReportSanityChecker } from "typechain-types"; import { numberToHex } from "./string"; +import { ether, impersonate } from "."; + function splitArrayIntoChunks(inputArray: T[], maxItemsPerChunk: number): T[][] { const result: T[][] = []; for (let i = 0; i < inputArray.length; i += maxItemsPerChunk) { @@ -194,7 +196,7 @@ export type OracleReportProps = { config?: ExtraDataConfig; }; -export function constructOracleReport({ reportFieldsWithoutExtraData, extraData, config }: OracleReportProps) { +export function prepareExtraData(extraData: ExtraData, config?: ExtraDataConfig) { const extraDataItems: string[] = []; if (Array.isArray(extraData)) { @@ -212,11 +214,17 @@ export function constructOracleReport({ reportFieldsWithoutExtraData, extraData, const extraDataChunks = packExtraDataItemsToChunksLinkedByHash(extraDataItems, maxItemsPerChunk); const extraDataChunkHashes = extraDataChunks.map((chunk) => calcExtraDataListHash(chunk)); + return { extraDataItemsCount, extraDataChunks, extraDataChunkHashes }; +} + +export function constructOracleReport({ reportFieldsWithoutExtraData, extraData, config }: OracleReportProps) { + const { extraDataItemsCount, extraDataChunks, extraDataChunkHashes } = prepareExtraData(extraData, config); + const report: OracleReport = { ...reportFieldsWithoutExtraData, - extraDataHash: extraDataItems.length ? extraDataChunkHashes[0] : ZeroHash, - extraDataItemsCount: extraDataItems.length, - extraDataFormat: extraDataItems.length ? EXTRA_DATA_FORMAT_LIST : EXTRA_DATA_FORMAT_EMPTY, + extraDataHash: extraDataItemsCount ? extraDataChunkHashes[0] : ZeroHash, + extraDataItemsCount, + extraDataFormat: extraDataItemsCount ? EXTRA_DATA_FORMAT_LIST : EXTRA_DATA_FORMAT_EMPTY, }; const reportHash = calcReportDataHash(getReportDataItems(report)); @@ -239,3 +247,22 @@ export async function getSlotTimestamp(slot: bigint, consensus: HashConsensus) { const chainConfig = await consensus.getChainConfig(); return chainConfig.genesisTime + chainConfig.secondsPerSlot * slot; } + +// Might be useful for tests on scratch where even reporting a single exited validator +// is too much for the default limit +export async function setAnnualBalanceIncreaseLimit(sanityChecker: OracleReportSanityChecker, limitBP: bigint) { + const adminRole = await sanityChecker.DEFAULT_ADMIN_ROLE(); + + const admin = await sanityChecker.getRoleMember(adminRole, 0); + const adminSigner = await impersonate(admin, ether("1")); + + const setLimitRole = await sanityChecker.ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE(); + + // Grant the role to the current signer + await sanityChecker.connect(adminSigner).grantRole(setLimitRole, adminSigner.address); + + await sanityChecker.connect(adminSigner).setAnnualBalanceIncreaseBPLimit(limitBP); + + // Revoke the role after setting the limit + await sanityChecker.connect(adminSigner).revokeRole(setLimitRole, adminSigner.address); +} diff --git a/lib/protocol/context.ts b/lib/protocol/context.ts index 068637461..4ed60bb5b 100644 --- a/lib/protocol/context.ts +++ b/lib/protocol/context.ts @@ -12,6 +12,10 @@ const getSigner = async (signer: Signer, balance = ether("100"), signers: Protoc return impersonate(signerAddress, balance); }; +export const withCSM = () => { + return process.env.INTEGRATION_WITH_CSM !== "off"; +}; + export const getProtocolContext = async (): Promise => { if (hre.network.name === "hardhat") { const networkConfig = hre.config.networks[hre.network.name]; @@ -27,7 +31,7 @@ export const getProtocolContext = async (): Promise => { // By default, all flags are "on" const flags = { - withCSM: process.env.INTEGRATION_WITH_CSM !== "off", + withCSM: withCSM(), } as ProtocolContextFlags; log.debug("Protocol context flags", { diff --git a/lib/protocol/discover.ts b/lib/protocol/discover.ts index b2e156d0d..49dbf3b82 100644 --- a/lib/protocol/discover.ts +++ b/lib/protocol/discover.ts @@ -1,6 +1,14 @@ import hre from "hardhat"; -import { AccountingOracle, Lido, LidoLocator, StakingRouter, WithdrawalQueueERC721 } from "typechain-types"; +import { + AccountingOracle, + ICSModule, + Lido, + LidoLocator, + NodeOperatorsRegistry, + StakingRouter, + WithdrawalQueueERC721, +} from "typechain-types"; import { batch, log } from "lib"; @@ -120,11 +128,18 @@ const getAragonContracts = async (lido: LoadedContract, config: ProtocolNe * Load staking modules contracts registered in the staking router. */ const getStakingModules = async (stakingRouter: LoadedContract, config: ProtocolNetworkConfig) => { - const [nor, sdvt] = await stakingRouter.getStakingModules(); - return (await batch({ + const [nor, sdvt, csm] = await stakingRouter.getStakingModules(); + + const promises: { [key: string]: Promise> } = { nor: loadContract("NodeOperatorsRegistry", config.get("nor") || nor.stakingModuleAddress), sdvt: loadContract("NodeOperatorsRegistry", config.get("sdvt") || sdvt.stakingModuleAddress), - })) as StakingModuleContracts; + }; + + if (csm) { + promises.csm = loadContract("ICSModule", config.get("csm") || csm.stakingModuleAddress); + } + + return (await batch(promises)) as StakingModuleContracts; }; /** diff --git a/lib/protocol/helpers/accounting.ts b/lib/protocol/helpers/accounting.ts index bdb04d4fb..37a086223 100644 --- a/lib/protocol/helpers/accounting.ts +++ b/lib/protocol/helpers/accounting.ts @@ -12,11 +12,13 @@ import { certainAddress, ether, EXTRA_DATA_FORMAT_EMPTY, + EXTRA_DATA_FORMAT_LIST, getCurrentBlockTimestamp, HASH_CONSENSUS_FAR_FUTURE_EPOCH, impersonate, log, ONE_GWEI, + prepareExtraData, } from "lib"; import { ProtocolContext } from "../types"; @@ -213,6 +215,56 @@ export const report = async ( }); }; +export async function reportWithoutExtraData( + ctx: ProtocolContext, + numExitedValidatorsByStakingModule: bigint[], + stakingModuleIdsWithNewlyExitedValidators: bigint[], + extraData: ReturnType, +) { + const { accountingOracle } = ctx.contracts; + + const { extraDataItemsCount, extraDataChunks, extraDataChunkHashes } = extraData; + + const reportData: OracleReportParams = { + excludeVaultsBalances: true, + extraDataFormat: EXTRA_DATA_FORMAT_LIST, + extraDataHash: extraDataChunkHashes[0], + extraDataItemsCount: BigInt(extraDataItemsCount), + numExitedValidatorsByStakingModule, + stakingModuleIdsWithNewlyExitedValidators, + skipWithdrawals: true, + }; + + const { data } = await report(ctx, { ...reportData, dryRun: true }); + + const items = getReportDataItems(data); + const hash = calcReportDataHash(items); + const oracleVersion = await accountingOracle.getContractVersion(); + + const submitter = await reachConsensus(ctx, { + refSlot: BigInt(data.refSlot), + reportHash: hash, + consensusVersion: BigInt(data.consensusVersion), + }); + + const reportTx = await accountingOracle.connect(submitter).submitReportData(data, oracleVersion); + log.debug("Pushed oracle report main data", { + "Ref slot": data.refSlot, + "Consensus version": data.consensusVersion, + "Report hash": hash, + }); + + // Get processing state after main report is submitted + const processingStateAfterMainReport = await accountingOracle.getProcessingState(); + + // Verify that extra data is not yet submitted + expect(processingStateAfterMainReport.extraDataSubmitted).to.be.false; + expect(processingStateAfterMainReport.extraDataItemsCount).to.equal(extraDataItemsCount); + expect(processingStateAfterMainReport.extraDataItemsSubmitted).to.equal(0n); + + return { reportTx, data, submitter, extraDataChunks, extraDataChunkHashes }; +} + export const getReportTimeElapsed = async (ctx: ProtocolContext) => { const { hashConsensus } = ctx.contracts; const { slotsPerEpoch, secondsPerSlot, genesisTime } = await hashConsensus.getChainConfig(); diff --git a/lib/protocol/helpers/index.ts b/lib/protocol/helpers/index.ts index 5eb37f6c5..eb05b1543 100644 --- a/lib/protocol/helpers/index.ts +++ b/lib/protocol/helpers/index.ts @@ -16,8 +16,8 @@ export { } from "./accounting"; export { ensureDsmGuardians } from "./dsm"; -export { norEnsureOperators } from "./nor"; -export { sdvtEnsureOperators } from "./sdvt"; +export { norSdvtEnsureOperators } from "./nor-sdvt"; +export { calcNodeOperatorRewards } from "./staking-module"; export { createVaultProxy, createVaultsReportTree, diff --git a/lib/protocol/helpers/nor.ts b/lib/protocol/helpers/nor-sdvt.ts similarity index 54% rename from lib/protocol/helpers/nor.ts rename to lib/protocol/helpers/nor-sdvt.ts index 82f36e022..ed4771ef4 100644 --- a/lib/protocol/helpers/nor.ts +++ b/lib/protocol/helpers/nor-sdvt.ts @@ -1,38 +1,46 @@ import { expect } from "chai"; -import { ethers, randomBytes } from "ethers"; +import { ethers } from "ethers"; + +import { NodeOperatorsRegistry } from "typechain-types"; import { certainAddress, log } from "lib"; +import { LoadedContract } from "lib/protocol/types"; import { ProtocolContext, StakingModuleName } from "../types"; import { depositAndReportValidators } from "./staking"; +import { NOR_MODULE_ID, randomPubkeys, randomSignatures,SDVT_MODULE_ID } from "./staking-module"; -export const NOR_MODULE_ID = 1n; const MIN_OPS_COUNT = 3n; const MIN_OP_KEYS_COUNT = 10n; -const PUBKEY_LENGTH = 48n; -const SIGNATURE_LENGTH = 96n; +async function isNor(module: LoadedContract, ctx: ProtocolContext) { + return (await module.getAddress()) === ctx.contracts.nor.target; +} -export const norEnsureOperators = async ( +export const norSdvtEnsureOperators = async ( ctx: ProtocolContext, + module: LoadedContract, minOperatorsCount = MIN_OPS_COUNT, minOperatorKeysCount = MIN_OP_KEYS_COUNT, ) => { - const { nor } = ctx.contracts; - - const newOperatorsCount = await norEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); + const { numBefore, numAdded } = await norSdvtEnsureOperatorsHaveMinKeys( + ctx, + module, + minOperatorsCount, + minOperatorKeysCount, + ); for (let operatorId = 0n; operatorId < minOperatorsCount; operatorId++) { - const nodeOperatorBefore = await nor.getNodeOperator(operatorId, false); + const nodeOperatorBefore = await module.getNodeOperator(operatorId, false); if (nodeOperatorBefore.totalVettedValidators < nodeOperatorBefore.totalAddedValidators) { - await norSetOperatorStakingLimit(ctx, { + await norSdvtSetOperatorStakingLimit(ctx, module, { operatorId, limit: nodeOperatorBefore.totalAddedValidators, }); } - const nodeOperatorAfter = await nor.getNodeOperator(operatorId, false); + const nodeOperatorAfter = await module.getNodeOperator(operatorId, false); expect(nodeOperatorAfter.totalVettedValidators).to.equal(nodeOperatorBefore.totalAddedValidators); } @@ -42,81 +50,84 @@ export const norEnsureOperators = async ( "Min keys count": minOperatorKeysCount, }); - if (newOperatorsCount > 0) { - await depositAndReportValidators(ctx, NOR_MODULE_ID, newOperatorsCount); + if (numAdded > 0) { + const moduleId = (await isNor(module, ctx)) ? NOR_MODULE_ID : SDVT_MODULE_ID; + await depositAndReportValidators(ctx, moduleId, numAdded * (minOperatorKeysCount / 2n)); } + return { numBefore, numAdded }; }; /** * Fills the Nor operators with some keys to deposit in case there are not enough of them. */ -const norEnsureOperatorsHaveMinKeys = async ( +const norSdvtEnsureOperatorsHaveMinKeys = async ( ctx: ProtocolContext, + module: LoadedContract, minOperatorsCount = MIN_OPS_COUNT, minKeysCount = MIN_OP_KEYS_COUNT, -): Promise => { - const newOperatorsCount = await norEnsureMinOperators(ctx, minOperatorsCount); - - const { nor } = ctx.contracts; +): Promise<{ numBefore: bigint; numAdded: bigint }> => { + const { numBefore, numAdded } = await norSdvtEnsureMinOperators(ctx, module, minOperatorsCount); for (let operatorId = 0n; operatorId < minOperatorsCount; operatorId++) { - const keysCount = await nor.getTotalSigningKeyCount(operatorId); + const keysCount = await module.getTotalSigningKeyCount(operatorId); if (keysCount < minKeysCount) { - await norAddOperatorKeys(ctx, { + await norSdvtAddOperatorKeys(ctx, module, { operatorId, keysToAdd: minKeysCount - keysCount, }); } - const keysCountAfter = await nor.getTotalSigningKeyCount(operatorId); + const keysCountAfter = await module.getTotalSigningKeyCount(operatorId); expect(keysCountAfter).to.be.gte(minKeysCount); } - return newOperatorsCount; + return { numBefore, numAdded }; }; /** * Fills the NOR with some operators in case there are not enough of them. */ -const norEnsureMinOperators = async (ctx: ProtocolContext, minOperatorsCount = MIN_OPS_COUNT): Promise => { - const { nor } = ctx.contracts; - - const before = await nor.getNodeOperatorsCount(); - let count = 0n; +const norSdvtEnsureMinOperators = async ( + ctx: ProtocolContext, + module: LoadedContract, + minOperatorsCount = MIN_OPS_COUNT, +): Promise<{ numBefore: bigint; numAdded: bigint }> => { + const numBefore = await module.getNodeOperatorsCount(); + let numAdded = 0n; - while (before + count < minOperatorsCount) { - const operatorId = before + count; + while (numBefore + numAdded < minOperatorsCount) { + const operatorId = numBefore + numAdded; const operator = { - name: getOperatorName("nor", operatorId), - rewardAddress: getOperatorRewardAddress("nor", operatorId), + name: getOperatorName((await isNor(module, ctx)) ? "nor" : "sdvt", operatorId), + rewardAddress: getOperatorRewardAddress((await isNor(module, ctx)) ? "nor" : "sdvt", operatorId), }; - await norAddNodeOperator(ctx, operator); - count++; + await norSdvtAddNodeOperator(ctx, module, operator); + numAdded++; } - const after = await nor.getNodeOperatorsCount(); + const after = await module.getNodeOperatorsCount(); - expect(after).to.equal(before + count); + expect(after).to.equal(numBefore + numAdded); expect(after).to.be.gte(minOperatorsCount); - return count; + return { numBefore, numAdded }; }; /** * Adds a new node operator to the NOR. */ -export const norAddNodeOperator = async ( +export const norSdvtAddNodeOperator = async ( ctx: ProtocolContext, + module: LoadedContract, params: { name: string; rewardAddress: string; }, ) => { - const { nor } = ctx.contracts; const { name, rewardAddress } = params; log.debug(`Adding fake NOR operator`, { @@ -124,9 +135,10 @@ export const norAddNodeOperator = async ( "Reward address": rewardAddress, }); - const agentSigner = await ctx.getSigner("agent"); - const operatorId = await nor.getNodeOperatorsCount(); - await nor.connect(agentSigner).addNodeOperator(name, rewardAddress); + const operatorId = await module.getNodeOperatorsCount(); + + const managerSigner = (await isNor(module, ctx)) ? await ctx.getSigner("agent") : await ctx.getSigner("voting"); + await module.connect(managerSigner).addNodeOperator(name, rewardAddress); log.debug("Added NOR fake operator", { "Operator ID": operatorId, @@ -142,14 +154,14 @@ export const norAddNodeOperator = async ( /** * Adds some signing keys to the operator in the NOR. */ -export const norAddOperatorKeys = async ( +export const norSdvtAddOperatorKeys = async ( ctx: ProtocolContext, + module: LoadedContract, params: { operatorId: bigint; keysToAdd: bigint; }, ) => { - const { nor } = ctx.contracts; const { operatorId, keysToAdd } = params; log.debug(`Adding fake keys to NOR operator ${operatorId}`, { @@ -157,17 +169,17 @@ export const norAddOperatorKeys = async ( "Keys to add": keysToAdd, }); - const totalKeysBefore = await nor.getTotalSigningKeyCount(operatorId); - const unusedKeysBefore = await nor.getUnusedSigningKeyCount(operatorId); + const totalKeysBefore = await module.getTotalSigningKeyCount(operatorId); + const unusedKeysBefore = await module.getUnusedSigningKeyCount(operatorId); const votingSigner = await ctx.getSigner("voting"); - await nor + await module .connect(votingSigner) .addSigningKeys(operatorId, keysToAdd, randomPubkeys(Number(keysToAdd)), randomSignatures(Number(keysToAdd))); - const totalKeysAfter = await nor.getTotalSigningKeyCount(operatorId); - const unusedKeysAfter = await nor.getUnusedSigningKeyCount(operatorId); + const totalKeysAfter = await module.getTotalSigningKeyCount(operatorId); + const unusedKeysAfter = await module.getUnusedSigningKeyCount(operatorId); expect(totalKeysAfter).to.equal(totalKeysBefore + keysToAdd); expect(unusedKeysAfter).to.equal(unusedKeysBefore + keysToAdd); @@ -184,31 +196,17 @@ export const norAddOperatorKeys = async ( log.success(`Added fake keys to NOR operator ${operatorId}`); }; -/** - * Generates an array of random pubkeys in the correct format for NOR - */ -export const randomPubkeys = (count: number) => { - return randomBytes(count * Number(PUBKEY_LENGTH)); -}; - -/** - * Generates an array of random signatures in the correct format for NOR - */ -export const randomSignatures = (count: number) => { - return randomBytes(count * Number(SIGNATURE_LENGTH)); -}; - /** * Sets the staking limit for the operator. */ -export const norSetOperatorStakingLimit = async ( +export const norSdvtSetOperatorStakingLimit = async ( ctx: ProtocolContext, + module: LoadedContract, params: { operatorId: bigint; limit: bigint; }, ) => { - const { nor } = ctx.contracts; const { operatorId, limit } = params; log.debug(`Setting NOR operator ${operatorId} staking limit`, { @@ -217,7 +215,7 @@ export const norSetOperatorStakingLimit = async ( }); const votingSigner = await ctx.getSigner("voting"); - await nor.connect(votingSigner).setNodeOperatorStakingLimit(operatorId, limit); + await module.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 deleted file mode 100644 index d8f38eff1..000000000 --- a/lib/protocol/helpers/sdvt.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { expect } from "chai"; -import { randomBytes } from "ethers"; - -import { ether, impersonate, log, streccak } from "lib"; - -import { ProtocolContext } from "../types"; - -import { getOperatorManagerAddress, getOperatorName, getOperatorRewardAddress } from "./nor"; -import { depositAndReportValidators } from "./staking"; - -export const SDVT_MODULE_ID = 2n; -const MIN_OPS_COUNT = 3n; -const MIN_OP_KEYS_COUNT = 10n; - -const PUBKEY_LENGTH = 48n; -const SIGNATURE_LENGTH = 96n; - -const MANAGE_SIGNING_KEYS_ROLE = streccak("MANAGE_SIGNING_KEYS"); - -export const sdvtEnsureOperators = async ( - ctx: ProtocolContext, - minOperatorsCount = MIN_OPS_COUNT, - minOperatorKeysCount = MIN_OP_KEYS_COUNT, -) => { - const newOperatorsCount = await sdvtEnsureOperatorsHaveMinKeys(ctx, minOperatorsCount, minOperatorKeysCount); - - const { sdvt } = ctx.contracts; - - for (let operatorId = 0n; operatorId < minOperatorsCount; operatorId++) { - const nodeOperatorBefore = await sdvt.getNodeOperator(operatorId, false); - - if (nodeOperatorBefore.totalVettedValidators < nodeOperatorBefore.totalAddedValidators) { - await sdvtSetOperatorStakingLimit(ctx, { - operatorId, - limit: nodeOperatorBefore.totalAddedValidators, - }); - } - - const nodeOperatorAfter = await sdvt.getNodeOperator(operatorId, false); - - expect(nodeOperatorAfter.totalVettedValidators).to.equal(nodeOperatorBefore.totalAddedValidators); - } - - if (newOperatorsCount > 0) { - await depositAndReportValidators(ctx, SDVT_MODULE_ID, newOperatorsCount); - } -}; - -/** - * Fills the Simple DVT operators with some keys to deposit in case there are not enough of them. - */ -const sdvtEnsureOperatorsHaveMinKeys = async ( - ctx: ProtocolContext, - minOperatorsCount = MIN_OPS_COUNT, - minKeysCount = MIN_OP_KEYS_COUNT, -): Promise => { - const newOperatorsCount = await sdvtEnsureMinOperators(ctx, minOperatorsCount); - - const { sdvt } = ctx.contracts; - - for (let operatorId = 0n; operatorId < minOperatorsCount; operatorId++) { - const unusedKeysCount = await sdvt.getUnusedSigningKeyCount(operatorId); - - if (unusedKeysCount < minKeysCount) { - log.debug(`Adding SDVT fake keys to operator ${operatorId}`, { - "Unused keys count": unusedKeysCount, - "Min keys count": minKeysCount, - }); - - await sdvtAddNodeOperatorKeys(ctx, { - operatorId, - keysToAdd: minKeysCount - unusedKeysCount, - }); - } - - const unusedKeysCountAfter = await sdvt.getUnusedSigningKeyCount(operatorId); - - expect(unusedKeysCountAfter).to.be.gte(minKeysCount); - } - - log.debug("Checked SDVT operators keys count", { - "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): Promise => { - const { sdvt } = ctx.contracts; - - const before = await sdvt.getNodeOperatorsCount(); - let count = 0n; - - while (before + count < minOperatorsCount) { - const operatorId = before + count; - - const operator = { - operatorId, - name: getOperatorName("sdvt", operatorId), - rewardAddress: getOperatorRewardAddress("sdvt", operatorId), - managerAddress: getOperatorManagerAddress("sdvt", 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++; - } - - const after = await sdvt.getNodeOperatorsCount(); - - expect(after).to.equal(before + count); - expect(after).to.be.gte(minOperatorsCount); - - log.debug("Checked SDVT operators count", { - "Min operators count": minOperatorsCount, - "Operators count": after, - }); - - return count; -}; - -/** - * Adds a new node operator to the Simple DVT. - */ -const sdvtAddNodeOperator = async ( - ctx: ProtocolContext, - params: { - operatorId: bigint; - name: string; - rewardAddress: string; - managerAddress: string; - }, -) => { - const { sdvt, acl } = ctx.contracts; - const { operatorId, name, rewardAddress, managerAddress } = params; - - const easyTrackExecutor = await ctx.getSigner("easyTrack"); - - 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))], - ); - - log.success(`Added fake SDVT operator ${operatorId}`); -}; - -/** - * Adds some signing keys to the operator in the Simple DVT. - */ -const sdvtAddNodeOperatorKeys = async ( - ctx: ProtocolContext, - params: { - operatorId: bigint; - keysToAdd: bigint; - }, -) => { - const { sdvt } = ctx.contracts; - const { operatorId, keysToAdd } = params; - - const totalKeysBefore = await sdvt.getTotalSigningKeyCount(operatorId); - const unusedKeysBefore = await sdvt.getUnusedSigningKeyCount(operatorId); - const { rewardAddress } = await sdvt.getNodeOperator(operatorId, false); - - const actor = await impersonate(rewardAddress, ether("100")); - await sdvt - .connect(actor) - .addSigningKeys( - operatorId, - keysToAdd, - randomBytes(Number(keysToAdd * PUBKEY_LENGTH)), - randomBytes(Number(keysToAdd * SIGNATURE_LENGTH)), - ); - - const totalKeysAfter = await sdvt.getTotalSigningKeyCount(operatorId); - const unusedKeysAfter = await sdvt.getUnusedSigningKeyCount(operatorId); - - expect(totalKeysAfter).to.equal(totalKeysBefore + keysToAdd); - expect(unusedKeysAfter).to.equal(unusedKeysBefore + keysToAdd); - - log.success(`Added fake keys to SDVT operator ${operatorId}`); -}; - -/** - * Sets the staking limit for the operator. - */ -const sdvtSetOperatorStakingLimit = async ( - ctx: ProtocolContext, - params: { - operatorId: bigint; - limit: bigint; - }, -) => { - const { sdvt } = ctx.contracts; - 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-module.ts b/lib/protocol/helpers/staking-module.ts new file mode 100644 index 000000000..88c3afd16 --- /dev/null +++ b/lib/protocol/helpers/staking-module.ts @@ -0,0 +1,40 @@ +import { randomBytes } from "ethers"; + +import { IStakingModule } from "typechain-types"; + +import { LoadedContract } from "lib"; + +export const NOR_MODULE_ID = 1n; +export const SDVT_MODULE_ID = 2n; +export const CSM_MODULE_ID = 3n; + +const PUBKEY_LENGTH = 48n; +const SIGNATURE_LENGTH = 96n; + +export async function calcNodeOperatorRewards( + module: LoadedContract, + nodeOperatorId: bigint, + mintedShares: bigint, +): Promise { + const operatorSummary = await module.getNodeOperatorSummary(nodeOperatorId); + const moduleSummary = await module.getStakingModuleSummary(); + + const operatorTotalActiveKeys = operatorSummary.totalDepositedValidators - operatorSummary.totalExitedValidators; + const moduleTotalActiveKeys = moduleSummary.totalDepositedValidators - moduleSummary.totalExitedValidators; + + return (mintedShares * BigInt(operatorTotalActiveKeys)) / BigInt(moduleTotalActiveKeys); +} + +/** + * Generates an array of random pubkeys in the correct format for NOR + */ +export const randomPubkeys = (count: number) => { + return randomBytes(count * Number(PUBKEY_LENGTH)); +}; + +/** + * Generates an array of random signatures in the correct format for NOR + */ +export const randomSignatures = (count: number) => { + return randomBytes(count * Number(SIGNATURE_LENGTH)); +}; diff --git a/lib/protocol/helpers/staking.ts b/lib/protocol/helpers/staking.ts index 03422bef4..3649aede7 100644 --- a/lib/protocol/helpers/staking.ts +++ b/lib/protocol/helpers/staking.ts @@ -43,9 +43,9 @@ export const ensureStakeLimit = async (ctx: ProtocolContext) => { export const depositAndReportValidators = async (ctx: ProtocolContext, moduleId: bigint, depositsCount: bigint) => { const { lido, depositSecurityModule } = ctx.contracts; - const ethHolder = await impersonate(certainAddress("provision:eth:whale"), ether("100000")); + const ethHolder = await impersonate(certainAddress("provision:eth:whale"), ether("1000000")); - await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); + await lido.connect(ethHolder).submit(ZeroAddress, { value: ether("20000") }); // Deposit validators const dsmSigner = await impersonate(depositSecurityModule.address, ether("100000")); diff --git a/lib/protocol/index.ts b/lib/protocol/index.ts index 062c1a0b1..4d9465b40 100644 --- a/lib/protocol/index.ts +++ b/lib/protocol/index.ts @@ -1,4 +1,4 @@ -export { getProtocolContext } from "./context"; +export { getProtocolContext, withCSM } from "./context"; export type { ProtocolContext, ProtocolSigners, ProtocolContracts } from "./types"; export * from "./helpers"; diff --git a/lib/protocol/networks.ts b/lib/protocol/networks.ts index ea9c7a539..73e904215 100644 --- a/lib/protocol/networks.ts +++ b/lib/protocol/networks.ts @@ -64,6 +64,7 @@ const defaultEnv = { // stacking modules nor: "NODE_OPERATORS_REGISTRY_ADDRESS", sdvt: "SIMPLE_DVT_REGISTRY_ADDRESS", + csm: "CSM_REGISTRY_ADDRESS", // hash consensus hashConsensus: "HASH_CONSENSUS_ADDRESS", // vaults diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 7b2ef99f3..9b5405140 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -7,8 +7,7 @@ import { ensureOracleCommitteeMembers, ensureStakeLimit, finalizeWithdrawalQueue, - norEnsureOperators, - sdvtEnsureOperators, + norSdvtEnsureOperators, unpauseStaking, unpauseWithdrawalQueue, } from "./helpers"; @@ -36,8 +35,8 @@ export const provision = async (ctx: ProtocolContext) => { await unpauseStaking(ctx); await unpauseWithdrawalQueue(ctx); - await norEnsureOperators(ctx, 3n, 5n); - await sdvtEnsureOperators(ctx, 3n, 5n); + await norSdvtEnsureOperators(ctx, ctx.contracts.nor, 5n, 9n); + await norSdvtEnsureOperators(ctx, ctx.contracts.sdvt, 5n, 9n); await finalizeWithdrawalQueue(ctx); diff --git a/lib/protocol/types.ts b/lib/protocol/types.ts index 818a6305b..e639a2102 100644 --- a/lib/protocol/types.ts +++ b/lib/protocol/types.ts @@ -9,6 +9,7 @@ import { Burner, DepositSecurityModule, HashConsensus, + ICSModule, Kernel, Lido, LidoExecutionLayerRewardsVault, @@ -54,6 +55,7 @@ export type ProtocolNetworkItems = { // stacking modules nor: string; sdvt: string; + csm: string; // hash consensus hashConsensus: string; // vaults @@ -88,6 +90,7 @@ export interface ContractTypes { UpgradeableBeacon: UpgradeableBeacon; VaultHub: VaultHub; OperatorGrid: OperatorGrid; + ICSModule: ICSModule; } export type ContractName = keyof ContractTypes; @@ -123,6 +126,7 @@ export type AragonContracts = { export type StakingModuleContracts = { nor: LoadedContract; sdvt: LoadedContract; + csm?: LoadedContract; }; export type StakingModuleName = "nor" | "sdvt"; diff --git a/lib/string.ts b/lib/string.ts index b43080903..efccb74e8 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -24,3 +24,8 @@ export function numberToHex(n: BigNumberish, byteLen: number | undefined = undef const s = n.toString(16); return byteLen === undefined ? s : s.padStart(byteLen * 2, "0"); } + +export function hexToBytes(hex: string): Uint8Array { + const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex; + return new Uint8Array(Buffer.from(cleanHex, "hex")); +} diff --git a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts new file mode 100644 index 000000000..18da66b2a --- /dev/null +++ b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts @@ -0,0 +1,426 @@ +import { expect } from "chai"; +import { ContractTransactionReceipt } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; + +import { Burner__factory, IStakingModule,NodeOperatorsRegistry } from "typechain-types"; + +import { + advanceChainTime, + ether, + EXTRA_DATA_TYPE_EXITED_VALIDATORS, + EXTRA_DATA_TYPE_STUCK_VALIDATORS, + findEventsWithInterfaces, + ItemType, + LoadedContract, + log, + prepareExtraData, + RewardDistributionState, + setAnnualBalanceIncreaseLimit, +} from "lib"; +import { getProtocolContext, ProtocolContext, withCSM } from "lib/protocol"; +import { reportWithoutExtraData } from "lib/protocol/helpers/accounting"; +import { norSdvtEnsureOperators } from "lib/protocol/helpers/nor-sdvt"; +import { + calcNodeOperatorRewards, + CSM_MODULE_ID, + NOR_MODULE_ID, + SDVT_MODULE_ID, +} from "lib/protocol/helpers/staking-module"; + +import { MAX_BASIS_POINTS, Snapshot } from "test/suite"; + +const MIN_KEYS_PER_OPERATOR = 5n; +const MIN_OPERATORS_COUNT = 50n; + +class ListKeyMapHelper { + private map: Map = new Map(); + + constructor() { + this.map = new Map(); + } + + set(keys: unknown[], value: ValueType): void { + const compositeKey = this.createKey(keys); + this.map.set(compositeKey, value); + } + + get(keys: unknown[]): ValueType | undefined { + const compositeKey = this.createKey(keys); + const result = this.map.get(compositeKey); + if (result === undefined) { + log.error("HelperMap: get: result is undefined for key " + compositeKey); + } + return result; + } + + private createKey(keys: unknown[]): string { + return keys.map((k) => String(k)).join("-"); + } +} + +describe("Integration: AccountingOracle extra data full items", () => { + let ctx: ProtocolContext; + let stranger: HardhatEthersSigner; + + let snapshot: string; + let originalState: string; + let maxNodeOperatorsPerExtraDataItem: number; + let maxItemsPerExtraDataTransaction: number; + + before(async () => { + ctx = await getProtocolContext(); + snapshot = await Snapshot.take(); + + [stranger] = await ethers.getSigners(); + await setBalance(stranger.address, ether("1000000")); + + if (ctx.isScratch) { + const { oracleReportSanityChecker } = ctx.contracts; + // Need this to pass the annual balance increase limit check in sanity checker for scratch deploy + // with not that much TVL + await setAnnualBalanceIncreaseLimit(oracleReportSanityChecker, MAX_BASIS_POINTS); + + // Need this to pass the annual balance / appeared validators per day + // increase limit check in sanity checker for scratch deploy with not that much TVL + await advanceChainTime(1n * 24n * 60n * 60n); + } + + await prepareModules(); + + const limits = await ctx.contracts.oracleReportSanityChecker.getOracleReportLimits(); + maxNodeOperatorsPerExtraDataItem = Number(limits.maxNodeOperatorsPerExtraDataItem); + maxItemsPerExtraDataTransaction = Number(limits.maxItemsPerExtraDataTransaction); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + after(async () => await Snapshot.restore(snapshot)); + + async function prepareModules() { + const { nor, sdvt } = ctx.contracts; + + await ctx.contracts.lido.connect(await ctx.getSigner("voting")).removeStakingLimit(); + + await norSdvtEnsureOperators(ctx, nor, MIN_OPERATORS_COUNT, MIN_KEYS_PER_OPERATOR); + await advanceChainTime(1n * 24n * 60n * 60n); + await norSdvtEnsureOperators(ctx, sdvt, MIN_OPERATORS_COUNT, MIN_KEYS_PER_OPERATOR); + await advanceChainTime(1n * 24n * 60n * 60n); + } + + async function distributeReward(module: LoadedContract, fromSigner: HardhatEthersSigner) { + // Get initial reward distribution state + const rewardDistributionState = await module.getRewardDistributionState(); + expect(rewardDistributionState).to.equal(RewardDistributionState.ReadyForDistribution); + + // Distribute rewards + const tx = await module.connect(fromSigner).distributeReward(); + + // Verify reward distribution state after + const finalState = await module.getRewardDistributionState(); + expect(finalState).to.equal(RewardDistributionState.Distributed); + + return (await tx.wait()) as ContractTransactionReceipt; + } + + async function assertModulesRewardDistributionState(expectedState: RewardDistributionState) { + const { nor, sdvt } = ctx.contracts; + + const norState = await nor.getRewardDistributionState(); + const sdvtState = await sdvt.getRewardDistributionState(); + + expect(norState).to.equal(expectedState, "NOR reward distribution state is incorrect"); + expect(sdvtState).to.equal(expectedState, "SDVT reward distribution state is incorrect"); + } + + function testReportingModuleWithMaxExtraDataItems({ + norStuckItems, + norExitedItems, + sdvtStuckItems, + sdvtExitedItems, + csmStuckItems, + csmExitedItems, + }: { + norStuckItems: number; + norExitedItems: number; + sdvtStuckItems: number; + sdvtExitedItems: number; + csmStuckItems: number; + csmExitedItems: number; + }) { + return async () => { + const { accountingOracle, nor, sdvt, csm } = ctx.contracts; + + const modules = [ + { moduleId: NOR_MODULE_ID, module: nor }, + { moduleId: SDVT_MODULE_ID, module: sdvt }, + ...(ctx.flags.withCSM ? [{ moduleId: CSM_MODULE_ID, module: csm! }] : []), + ]; + + // Get active node operator IDs for NOR + const norIds: bigint[] = []; + for (let i = 0; i < Number(await nor.getNodeOperatorsCount()); i++) { + const nodeOperator = await nor.getNodeOperator(BigInt(i), false); + if (nodeOperator.active) { + norIds.push(BigInt(i)); + } + } + + // Get active node operator IDs for SDVT + const sdvtIds: bigint[] = []; + for (let i = 0; i < Number(await sdvt.getNodeOperatorsCount()); i++) { + const nodeOperator = await sdvt.getNodeOperator(BigInt(i), false); + if (nodeOperator.active) { + sdvtIds.push(BigInt(i)); + } + } + + expect(norIds.length).to.gte(2 * maxNodeOperatorsPerExtraDataItem); + expect(sdvtIds.length).to.gte(2 * maxNodeOperatorsPerExtraDataItem); + + // Prepare arrays for stuck and exited keys + const csmIds: bigint[] = []; + for (let i = 0; i < maxNodeOperatorsPerExtraDataItem; i++) { + csmIds.push(BigInt(i)); + } + + // Slice arrays based on item counts + const idsExited = new Map(); + const idsStuck = new Map(); + + idsExited.set(NOR_MODULE_ID, norIds.slice(0, norExitedItems * maxNodeOperatorsPerExtraDataItem)); + idsStuck.set( + NOR_MODULE_ID, + norIds.slice( + norStuckItems * maxNodeOperatorsPerExtraDataItem, + 2 * norStuckItems * maxNodeOperatorsPerExtraDataItem, + ), + ); + + idsExited.set(SDVT_MODULE_ID, sdvtIds.slice(0, sdvtExitedItems * maxNodeOperatorsPerExtraDataItem)); + idsStuck.set( + SDVT_MODULE_ID, + sdvtIds.slice( + sdvtStuckItems * maxNodeOperatorsPerExtraDataItem, + 2 * sdvtStuckItems * maxNodeOperatorsPerExtraDataItem, + ), + ); + + if (ctx.flags.withCSM) { + idsExited.set(CSM_MODULE_ID, csmIds.slice(0, csmExitedItems * maxNodeOperatorsPerExtraDataItem)); + idsStuck.set(CSM_MODULE_ID, csmIds.slice(0, csmStuckItems * maxNodeOperatorsPerExtraDataItem)); + } + + const numKeysReportedByNo = new ListKeyMapHelper(); // [moduleId, nodeOpId, type] -> numKeys + + const reportExtraItems: ItemType[] = []; + + for (const { moduleId, module } of modules) { + const ids = idsStuck.get(moduleId)!; + for (const id of ids) { + const summary = await module.getNodeOperatorSummary(id); + const numKeys = summary.stuckValidatorsCount + 1n; + numKeysReportedByNo.set([moduleId, id, EXTRA_DATA_TYPE_STUCK_VALIDATORS], numKeys); + reportExtraItems.push({ + moduleId: Number(moduleId), + nodeOpIds: [Number(id)], + keysCounts: [Number(numKeys)], + type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, + }); + } + } + + for (const { moduleId, module } of modules) { + const ids = idsExited.get(moduleId)!; + for (const id of ids) { + const summary = await module.getNodeOperatorSummary(id); + const numKeys = summary.totalExitedValidators + 1n; + numKeysReportedByNo.set([moduleId, id, EXTRA_DATA_TYPE_EXITED_VALIDATORS], numKeys); + reportExtraItems.push({ + moduleId: Number(moduleId), + nodeOpIds: [Number(id)], + keysCounts: [Number(numKeys)], + type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, + }); + } + } + + const extraData = prepareExtraData(reportExtraItems, { maxItemsPerChunk: maxItemsPerExtraDataTransaction }); + + // Prepare modules with exited validators and their counts + const modulesWithExited = []; + const numExitedValidatorsByStakingModule = []; + + if (norExitedItems > 0) { + modulesWithExited.push(NOR_MODULE_ID); + const norExitedBefore = (await nor.getStakingModuleSummary()).totalExitedValidators; + numExitedValidatorsByStakingModule.push( + norExitedBefore + BigInt(norExitedItems) * BigInt(maxNodeOperatorsPerExtraDataItem), + ); + } + + if (sdvtExitedItems > 0) { + modulesWithExited.push(SDVT_MODULE_ID); + const sdvtExitedBefore = (await sdvt.getStakingModuleSummary()).totalExitedValidators; + numExitedValidatorsByStakingModule.push( + sdvtExitedBefore + BigInt(sdvtExitedItems) * BigInt(maxNodeOperatorsPerExtraDataItem), + ); + } + + if (csmExitedItems > 0 && ctx.flags.withCSM) { + modulesWithExited.push(CSM_MODULE_ID); + const csmExitedBefore = (await csm!.getStakingModuleSummary()).totalExitedValidators; + numExitedValidatorsByStakingModule.push( + csmExitedBefore + BigInt(csmExitedItems) * BigInt(maxNodeOperatorsPerExtraDataItem), + ); + } + + // Store initial share balances for node operators with stuck validators + const sharesBefore = new ListKeyMapHelper(); + for (const { moduleId, module } of modules) { + if (moduleId === CSM_MODULE_ID) continue; + const ids = idsStuck.get(moduleId)!; + for (const id of ids) { + const nodeOperator = await module.getNodeOperator(id, false); + sharesBefore.set([moduleId, id], await ctx.contracts.lido.sharesOf(nodeOperator.rewardAddress)); + } + } + + const { reportTx, submitter, extraDataChunks } = await reportWithoutExtraData( + ctx, + numExitedValidatorsByStakingModule, + modulesWithExited, + extraData, + ); + + await assertModulesRewardDistributionState(RewardDistributionState.TransferredToModule); + + for (let i = 0; i < extraDataChunks.length; i++) { + await accountingOracle.connect(submitter).submitReportExtraDataList(extraDataChunks[i]); + } + + const processingState = await accountingOracle.getProcessingState(); + expect(processingState.extraDataItemsCount).to.equal(extraData.extraDataItemsCount); + expect(processingState.extraDataItemsSubmitted).to.equal(extraData.extraDataItemsCount); + expect(processingState.extraDataSubmitted).to.be.true; + + // Distribute rewards + const distributeTxReceipts: Record = {}; + for (const { moduleId, module } of modules) { + if (moduleId === CSM_MODULE_ID) continue; + distributeTxReceipts[String(moduleId)] = await distributeReward( + module as unknown as LoadedContract, + stranger, + ); + } + + for (const { moduleId, module } of modules) { + const moduleIdsExited = idsExited.get(moduleId)!; + for (const id of moduleIdsExited) { + const summary = await module.getNodeOperatorSummary(id); + const numExpectedExited = numKeysReportedByNo.get([moduleId, id, EXTRA_DATA_TYPE_EXITED_VALIDATORS]); + expect(summary.totalExitedValidators).to.equal(numExpectedExited); + } + + // Check module stuck validators, penalties and rewards + const moduleIdsStuck = idsStuck.get(moduleId)!; + for (const opId of moduleIdsStuck) { + // Verify stuck validators count matches expected + const operatorSummary = await module.getNodeOperatorSummary(opId); + const numExpectedStuck = numKeysReportedByNo.get([moduleId, opId, EXTRA_DATA_TYPE_STUCK_VALIDATORS]); + expect(operatorSummary.stuckValidatorsCount).to.equal(numExpectedStuck); + } + + if (moduleId === CSM_MODULE_ID) { + continue; + } + const moduleNor = module as unknown as LoadedContract; + + if (moduleIdsStuck.length > 0) { + // Find the TransferShares event for module rewards + const receipt = await reportTx.wait(); + const transferSharesEvents = await findEventsWithInterfaces(receipt!, "TransferShares", [ + ctx.contracts.lido.interface, + ]); + const moduleRewardsEvent = transferSharesEvents.find((e) => e.args.to === module.address); + const moduleRewards = moduleRewardsEvent ? moduleRewardsEvent.args.sharesValue : 0n; + + let modulePenaltyShares = 0n; + + // Check each stuck node operator + for (const opId of moduleIdsStuck) { + // Verify operator is penalized + expect(await moduleNor.isOperatorPenalized(opId)).to.be.true; + + // Get operator reward address and current shares balance + const operator = await moduleNor.getNodeOperator(opId, false); + const sharesAfter = await ctx.contracts.lido.sharesOf(operator.rewardAddress); + + // Calculate expected rewards + const rewardsAfter = await calcNodeOperatorRewards( + moduleNor as unknown as LoadedContract, + opId, + moduleRewards, + ); + + // Verify operator received only half the rewards (due to penalty) + const sharesDiff = sharesAfter - sharesBefore.get([moduleId, opId])!; + const expectedReward = rewardsAfter / 2n; + + // Allow for small rounding differences (up to 2 wei) + expect(sharesDiff).to.be.closeTo(expectedReward, 2n); + + // Track total penalty shares + modulePenaltyShares += rewardsAfter / 2n; + } + + // Check if penalty shares were burned + if (modulePenaltyShares > 0n) { + const distributeReceipt = await distributeTxReceipts[String(moduleId)]; + const burnEvents = await findEventsWithInterfaces(distributeReceipt!, "StETHBurnRequested", [ + Burner__factory.createInterface(), + ]); + const totalBurnedShares = burnEvents.reduce((sum, event) => sum + event.args.amountOfShares, 0n); + + // Verify that the burned shares match the penalty shares (with small tolerance for rounding) + expect(totalBurnedShares).to.be.closeTo(modulePenaltyShares, 100n); + } + } + } + }; + } + + for (const norStuckItems of [0, 1]) { + for (const norExitedItems of [0, 1]) { + for (const sdvtStuckItems of [0, 1]) { + for (const sdvtExitedItems of [0, 1]) { + for (const csmStuckItems of withCSM() ? [0, 1] : [0]) { + for (const csmExitedItems of withCSM() ? [0, 1] : [0]) { + if ( + norStuckItems + norExitedItems + sdvtStuckItems + sdvtExitedItems + csmStuckItems + csmExitedItems === + 0 + ) { + continue; + } + it( + `should process extra data with full items for all modules with norStuckItems=${norStuckItems}, norExitedItems=${norExitedItems}, sdvtStuckItems=${sdvtStuckItems}, sdvtExitedItems=${sdvtExitedItems}, csmStuckItems=${csmStuckItems}, csmExitedItems=${csmExitedItems}`, + testReportingModuleWithMaxExtraDataItems({ + norStuckItems, + norExitedItems, + sdvtStuckItems, + sdvtExitedItems, + csmStuckItems, + csmExitedItems, + }), + ); + } + } + } + } + } + } +}); diff --git a/test/integration/core/accounting-oracle-extra-data.integration.ts b/test/integration/core/accounting-oracle-extra-data.integration.ts new file mode 100644 index 000000000..b9571a1d2 --- /dev/null +++ b/test/integration/core/accounting-oracle-extra-data.integration.ts @@ -0,0 +1,257 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; + +import { advanceChainTime, ether, findEventsWithInterfaces, hexToBytes, RewardDistributionState } from "lib"; +import { EXTRA_DATA_FORMAT_LIST, KeyType, prepareExtraData, setAnnualBalanceIncreaseLimit } from "lib/oracle"; +import { getProtocolContext, ProtocolContext, report } from "lib/protocol"; +import { + OracleReportParams, + reportWithoutExtraData, + waitNextAvailableReportTime, +} from "lib/protocol/helpers/accounting"; +import { NOR_MODULE_ID } from "lib/protocol/helpers/staking-module"; + +import { MAX_BASIS_POINTS, Snapshot } from "test/suite"; + +const MODULE_ID = NOR_MODULE_ID; +const NUM_NEWLY_EXITED_VALIDATORS = 1n; + +describe("Integration: AccountingOracle extra data", () => { + let ctx: ProtocolContext; + let stranger: HardhatEthersSigner; + + let snapshot: string; + let originalState: string; + + let stuckKeys: KeyType; + let exitedKeys: KeyType; + + before(async () => { + ctx = await getProtocolContext(); + snapshot = await Snapshot.take(); + + [stranger] = await ethers.getSigners(); + await setBalance(stranger.address, ether("1000000")); + + async function getExitedCount(nodeOperatorId: bigint): Promise { + const { nor } = ctx.contracts; + const nodeOperator = await nor.getNodeOperator(nodeOperatorId, false); + return nodeOperator.totalExitedValidators; + } + + { + // Prepare stuck and exited keys extra data for reusing in tests + const { oracleReportSanityChecker } = ctx.contracts; + + if (ctx.isScratch) { + // Need this to pass the annual balance increase limit check in sanity checker for scratch deploy + // with not that much TVL + await setAnnualBalanceIncreaseLimit(oracleReportSanityChecker, MAX_BASIS_POINTS); + + // Need this to pass the annual balance increase limit check in sanity checker for scratch deploy + // with not that much TVL + await advanceChainTime(15n * 24n * 60n * 60n); + } + + const firstNodeOperatorInRange = ctx.isScratch ? 0 : 20; + const numNodeOperators = Math.min(10, Number(await ctx.contracts.nor.getNodeOperatorsCount())); + const numStuckKeys = 2; + stuckKeys = { + moduleId: Number(MODULE_ID), + nodeOpIds: [], + keysCounts: [], + }; + exitedKeys = { + moduleId: Number(MODULE_ID), + nodeOpIds: [], + keysCounts: [], + }; + for (let i = firstNodeOperatorInRange; i < firstNodeOperatorInRange + numNodeOperators; i++) { + const oldNumExited = await getExitedCount(BigInt(i)); + const numExited = oldNumExited + (i === firstNodeOperatorInRange ? NUM_NEWLY_EXITED_VALIDATORS : 0n); + if (numExited !== oldNumExited) { + exitedKeys.nodeOpIds.push(Number(i)); + exitedKeys.keysCounts.push(Number(numExited)); + } else { + stuckKeys.nodeOpIds.push(Number(i)); + stuckKeys.keysCounts.push(numStuckKeys); + } + } + } + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + after(async () => await Snapshot.restore(snapshot)); + + async function assertModulesRewardDistributionState(expectedState: RewardDistributionState) { + const { nor, sdvt } = ctx.contracts; + + const norState = await nor.getRewardDistributionState(); + const sdvtState = await sdvt.getRewardDistributionState(); + + expect(norState).to.equal(expectedState, "NOR reward distribution state is incorrect"); + expect(sdvtState).to.equal(expectedState, "SDVT reward distribution state is incorrect"); + } + + async function submitMainReport() { + const { nor } = ctx.contracts; + const extraData = prepareExtraData( + { + stuckKeys: [stuckKeys], + exitedKeys: [exitedKeys], + }, + { maxItemsPerChunk: 1 }, + ); + + const { totalExitedValidators } = await nor.getStakingModuleSummary(); + + return await reportWithoutExtraData( + ctx, + [totalExitedValidators + NUM_NEWLY_EXITED_VALIDATORS], + [NOR_MODULE_ID], + extraData, + ); + } + + it("should accept report with multiple keys per node operator (single chunk)", async () => { + const { nor } = ctx.contracts; + + // Get initial summary + const { totalExitedValidators } = await nor.getStakingModuleSummary(); + + const { extraDataItemsCount, extraDataChunks, extraDataChunkHashes } = prepareExtraData({ + stuckKeys: [stuckKeys], + exitedKeys: [exitedKeys], + }); + expect(extraDataChunks.length).to.equal(1); + expect(extraDataChunkHashes.length).to.equal(1); + + const reportData: OracleReportParams = { + excludeVaultsBalances: true, + extraDataFormat: EXTRA_DATA_FORMAT_LIST, + extraDataHash: extraDataChunkHashes[0], + extraDataItemsCount: BigInt(extraDataItemsCount), + extraDataList: hexToBytes(extraDataChunks[0]), + numExitedValidatorsByStakingModule: [totalExitedValidators + NUM_NEWLY_EXITED_VALIDATORS], + stakingModuleIdsWithNewlyExitedValidators: [NOR_MODULE_ID], + }; + + const numExitedBefore = (await nor.getStakingModuleSummary()).totalExitedValidators; + + const { reportTx, extraDataTx } = await report(ctx, reportData); + const reportReceipt = await reportTx?.wait(); + const extraDataReceipt = await extraDataTx?.wait(); + + const processingStartedEvents = await findEventsWithInterfaces(reportReceipt!, "ProcessingStarted", [ + ctx.contracts.accountingOracle.interface, + ]); + expect(processingStartedEvents.length).to.equal(1, "Should emit ProcessingStarted event"); + + const tokenRebasedEvents = await findEventsWithInterfaces(reportReceipt!, "TokenRebased", [ + ctx.contracts.lido.interface, + ]); + expect(tokenRebasedEvents.length).to.equal(1, "Should emit TokenRebased event"); + + const extraDataSubmittedEvents = await findEventsWithInterfaces(extraDataReceipt!, "ExtraDataSubmitted", [ + ctx.contracts.accountingOracle.interface, + ]); + expect(extraDataSubmittedEvents.length).to.equal(1, "Should emit ExtraDataSubmitted event"); + expect(extraDataSubmittedEvents[0].args.itemsProcessed).to.equal(extraDataItemsCount); + expect(extraDataSubmittedEvents[0].args.itemsCount).to.equal(extraDataItemsCount); + + expect((await nor.getStakingModuleSummary()).totalExitedValidators).to.equal( + numExitedBefore + NUM_NEWLY_EXITED_VALIDATORS, + ); + }); + + it("should accept extra data splitted into multiple chunks", async () => { + const { accountingOracle } = ctx.contracts; + + const { submitter, extraDataChunks } = await submitMainReport(); + + // Submit first chunk of extra data + await accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[0])); + + // Check processing state after first chunk submission + const processingStateAfterFirstExtraDataSubmitted = await accountingOracle.getProcessingState(); + expect(processingStateAfterFirstExtraDataSubmitted.extraDataSubmitted).to.be.false; + expect(processingStateAfterFirstExtraDataSubmitted.extraDataItemsCount).to.equal(2n); + expect(processingStateAfterFirstExtraDataSubmitted.extraDataItemsSubmitted).to.equal(1n); + await assertModulesRewardDistributionState(RewardDistributionState.TransferredToModule); + + // Submit second chunk of extra data + await accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[1])); + + // Check processing state after second chunk submission + const processingStateAfterSecondExtraDataSubmitted = await accountingOracle.getProcessingState(); + expect(processingStateAfterSecondExtraDataSubmitted.extraDataSubmitted).to.be.true; + expect(processingStateAfterSecondExtraDataSubmitted.extraDataItemsCount).to.equal(2n); + expect(processingStateAfterSecondExtraDataSubmitted.extraDataItemsSubmitted).to.equal(2n); + await assertModulesRewardDistributionState(RewardDistributionState.ReadyForDistribution); + }); + + it("should revert when extra data submission misses deadline", async () => { + const { accountingOracle } = ctx.contracts; + + const { submitter, extraDataChunks } = await submitMainReport(); + + // Submit first chunk of extra data + await accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[0])); + + // Check processing state after first chunk submission + const processingStateAfterFirstExtraDataSubmitted = await accountingOracle.getProcessingState(); + expect(processingStateAfterFirstExtraDataSubmitted.extraDataSubmitted).to.be.false; + expect(processingStateAfterFirstExtraDataSubmitted.extraDataItemsCount).to.equal(2n); + expect(processingStateAfterFirstExtraDataSubmitted.extraDataItemsSubmitted).to.equal(1n); + await assertModulesRewardDistributionState(RewardDistributionState.TransferredToModule); + + const processingDeadlineTime = processingStateAfterFirstExtraDataSubmitted.processingDeadlineTime; + + await waitNextAvailableReportTime(ctx); + + // Attempt to submit first chunk again after deadline + await expect(accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[0]))) + .to.be.revertedWithCustomError(accountingOracle, "ProcessingDeadlineMissed") + .withArgs(processingDeadlineTime); + + // Attempt to submit second chunk after deadline + await expect(accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[1]))) + .to.be.revertedWithCustomError(accountingOracle, "ProcessingDeadlineMissed") + .withArgs(processingDeadlineTime); + }); + + it("should revert when extra data submission has unexpected hash", async () => { + const { accountingOracle } = ctx.contracts; + + const { submitter, extraDataChunks, extraDataChunkHashes } = await submitMainReport(); + + // Submit second chunk of extra data before first one + await expect(accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[1]))) + .to.be.revertedWithCustomError(accountingOracle, "UnexpectedExtraDataHash") + .withArgs(extraDataChunkHashes[0], extraDataChunkHashes[1]); + + // Submit first chunk of extra data (correct order) + await accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[0])); + + // Try to submit first chunk again (should expect second chunk hash now) + await expect(accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[0]))) + .to.be.revertedWithCustomError(accountingOracle, "UnexpectedExtraDataHash") + .withArgs(extraDataChunkHashes[1], extraDataChunkHashes[0]); + + // Submit second chunk of extra data (correct order) + await accountingOracle.connect(submitter).submitReportExtraDataList(hexToBytes(extraDataChunks[1])); + + // Check processing state after both chunks are submitted + const processingStateAfterExtraDataSubmitted = await accountingOracle.getProcessingState(); + expect(processingStateAfterExtraDataSubmitted.extraDataSubmitted).to.be.true; + expect(processingStateAfterExtraDataSubmitted.extraDataItemsCount).to.equal(2n); + expect(processingStateAfterExtraDataSubmitted.extraDataItemsSubmitted).to.equal(2n); + await assertModulesRewardDistributionState(RewardDistributionState.ReadyForDistribution); + }); +}); diff --git a/test/integration/core/dsm-keys-unvetting.integration.ts b/test/integration/core/dsm-keys-unvetting.integration.ts index 9ad008d03..82ce0ced1 100644 --- a/test/integration/core/dsm-keys-unvetting.integration.ts +++ b/test/integration/core/dsm-keys-unvetting.integration.ts @@ -7,10 +7,21 @@ import { time } from "@nomicfoundation/hardhat-network-helpers"; import { DepositSecurityModule } from "typechain-types"; -import { certainAddress, DSMUnvetMessage, ether, findEventsWithInterfaces, impersonate } from "lib"; +import { + BigIntMath, + certainAddress, + DSMUnvetMessage, + ether, + findEventsWithInterfaces, + impersonate, +} from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { setSingleGuardian } from "lib/protocol/helpers/dsm"; -import { norAddNodeOperator, norAddOperatorKeys, norSetOperatorStakingLimit } from "lib/protocol/helpers/nor"; +import { + norSdvtAddNodeOperator, + norSdvtAddOperatorKeys, + norSdvtSetOperatorStakingLimit, +} from "lib/protocol/helpers/nor-sdvt"; import { Snapshot } from "test/suite"; @@ -139,6 +150,10 @@ describe("Integration: DSM keys unvetting", () => { const nodeOperatorBefore = await nor.getNodeOperator(operatorId, true); const totalVettedValidatorsBefore = nodeOperatorBefore.totalVettedValidators; expect(totalVettedValidatorsBefore).to.be.not.equal(vettedSigningKeysCount); + const totalVettedValidatorsAfter = BigIntMath.max( + vettedSigningKeysCount, + nodeOperatorBefore.totalDepositedValidators, + ); // Unvet signing keys const tx = await dsm @@ -150,11 +165,11 @@ describe("Integration: DSM keys unvetting", () => { const unvetEvents = findEventsWithInterfaces(receipt!, "VettedSigningKeysCountChanged", [nor.interface]); expect(unvetEvents.length).to.equal(1); expect(unvetEvents[0].args.nodeOperatorId).to.equal(operatorId); - expect(unvetEvents[0].args.approvedValidatorsCount).to.equal(vettedSigningKeysCount); + expect(unvetEvents[0].args.approvedValidatorsCount).to.equal(totalVettedValidatorsAfter); // Verify node operator state after unvetting const nodeOperatorAfter = await nor.getNodeOperator(operatorId, true); - expect(nodeOperatorAfter.totalVettedValidators).to.equal(vettedSigningKeysCount); + expect(nodeOperatorAfter.totalVettedValidators).to.equal(totalVettedValidatorsAfter); }); it("Should allow guardian to unvet signing keys directly", async () => { @@ -176,8 +191,12 @@ describe("Integration: DSM keys unvetting", () => { const nonce = await ctx.contracts.stakingRouter.getStakingModuleNonce(stakingModuleId); // Get node operator state before unvetting - const nodeOperatorsBefore = await nor.getNodeOperator(operatorId, true); - const totalDepositedValidatorsBefore = nodeOperatorsBefore.totalDepositedValidators; + const nodeOperatorBefore = await nor.getNodeOperator(operatorId, true); + const totalDepositedValidatorsBefore = nodeOperatorBefore.totalDepositedValidators; + const totalVettedValidatorsAfter = Math.max( + Number(vettedSigningKeysCount), + Number(nodeOperatorBefore.totalDepositedValidators), + ); expect(totalDepositedValidatorsBefore).to.be.gte(1n); // Pack operator IDs into bytes (8 bytes per ID) @@ -199,12 +218,12 @@ describe("Integration: DSM keys unvetting", () => { const unvetEvents = findEventsWithInterfaces(receipt!, "VettedSigningKeysCountChanged", [nor.interface]); expect(unvetEvents.length).to.equal(1); expect(unvetEvents[0].args.nodeOperatorId).to.equal(operatorId); - expect(unvetEvents[0].args.approvedValidatorsCount).to.equal(vettedSigningKeysCount); + expect(unvetEvents[0].args.approvedValidatorsCount).to.equal(totalVettedValidatorsAfter); // Verify node operator state after unvetting const nodeOperatorAfter = await nor.getNodeOperator(operatorId, true); expect(nodeOperatorAfter.totalDepositedValidators).to.equal(totalDepositedValidatorsBefore); - expect(nodeOperatorAfter.totalVettedValidators).to.equal(vettedSigningKeysCount); + expect(nodeOperatorAfter.totalVettedValidators).to.equal(totalVettedValidatorsAfter); }); it("Should allow guardian to decrease vetted signing keys count", async () => { @@ -213,16 +232,22 @@ describe("Integration: DSM keys unvetting", () => { // Add node operator and signing keys const stakingModuleId = 1; const rewardAddress = certainAddress("rewardAddress"); - const operatorId = await norAddNodeOperator(ctx, { + const operatorId = await norSdvtAddNodeOperator(ctx, ctx.contracts.nor, { name: "test", rewardAddress, }); // Add signing keys - await norAddOperatorKeys(ctx, { operatorId, keysToAdd: 10n }); + await norSdvtAddOperatorKeys(ctx, ctx.contracts.nor, { + operatorId, + keysToAdd: 10n, + }); // Set staking limit to 8 - await norSetOperatorStakingLimit(ctx, { operatorId, limit: 8n }); + await norSdvtSetOperatorStakingLimit(ctx, ctx.contracts.nor, { + operatorId, + limit: 8n, + }); // Prepare unvet parameters const blockNumber = await time.latestBlock(); diff --git a/test/integration/core/happy-path.integration.ts b/test/integration/core/happy-path.integration.ts index 9126cb30c..f53fbd1fc 100644 --- a/test/integration/core/happy-path.integration.ts +++ b/test/integration/core/happy-path.integration.ts @@ -8,11 +8,10 @@ import { batch, ether, impersonate, log, updateBalance } from "lib"; import { finalizeWithdrawalQueue, getProtocolContext, - norEnsureOperators, + norSdvtEnsureOperators, OracleReportParams, ProtocolContext, report, - sdvtEnsureOperators, } from "lib/protocol"; import { bailOnFailure, MAX_DEPOSIT, Snapshot, ZERO_HASH } from "test/suite"; @@ -77,10 +76,10 @@ describe("Scenario: Protocol Happy Path", () => { }); it("Should have at least 3 node operators in every module", async () => { - await norEnsureOperators(ctx, 3n, 5n); + await norSdvtEnsureOperators(ctx, ctx.contracts.nor, 3n, 5n); expect(await ctx.contracts.nor.getNodeOperatorsCount()).to.be.at.least(3n); - await sdvtEnsureOperators(ctx, 3n, 5n); + await norSdvtEnsureOperators(ctx, ctx.contracts.sdvt, 3n, 5n); expect(await ctx.contracts.sdvt.getNodeOperatorsCount()).to.be.at.least(3n); }); diff --git a/test/integration/core/hash-consensus.integration.ts b/test/integration/core/hash-consensus.integration.ts index 2654cb680..e5aff52ce 100644 --- a/test/integration/core/hash-consensus.integration.ts +++ b/test/integration/core/hash-consensus.integration.ts @@ -4,6 +4,8 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { HashConsensus } from "typechain-types"; + import { ether, impersonate } from "lib"; import { calcReportDataHash, @@ -16,8 +18,6 @@ import { import { Snapshot, ZERO_HASH } from "test/suite"; -import { HashConsensus } from "../../../typechain-types"; - const UINT64_MAX = 2n ** 64n - 1n; describe("Hash consensus negative scenarios", () => { diff --git a/test/integration/core/node-operators-happy-path.integration.ts b/test/integration/core/node-operators-happy-path.integration.ts index 07862430c..db1f50ab4 100644 --- a/test/integration/core/node-operators-happy-path.integration.ts +++ b/test/integration/core/node-operators-happy-path.integration.ts @@ -3,7 +3,7 @@ import { ethers } from "hardhat"; import { certainAddress, ether, findEventsWithInterfaces, impersonate } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { randomPubkeys, randomSignatures } from "lib/protocol/helpers/nor"; +import { randomPubkeys, randomSignatures } from "lib/protocol/helpers/staking-module"; import { bailOnFailure, Snapshot } from "test/suite"; diff --git a/test/integration/core/staking-limits.integration.ts b/test/integration/core/staking-limits.integration.ts index 6760f6e2e..13affea08 100644 --- a/test/integration/core/staking-limits.integration.ts +++ b/test/integration/core/staking-limits.integration.ts @@ -4,13 +4,13 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { mine } from "@nomicfoundation/hardhat-network-helpers"; +import { Lido } from "typechain-types"; + import { ether } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; import { Snapshot } from "test/suite"; -import { Lido } from "../../../typechain-types"; - describe("Staking limits", () => { let ctx: ProtocolContext; let lido: Lido; diff --git a/test/integration/core/staking-module.integration.ts b/test/integration/core/staking-module.integration.ts index ff798a736..ba85f0e5c 100644 --- a/test/integration/core/staking-module.integration.ts +++ b/test/integration/core/staking-module.integration.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { certainAddress, ether, impersonate } from "lib"; import { LoadedContract } from "lib/contract"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; -import { randomPubkeys, randomSignatures } from "lib/protocol/helpers/nor"; +import { randomPubkeys, randomSignatures } from "lib/protocol/helpers/staking-module"; import { Snapshot } from "test/suite"; diff --git a/test/integration/vaults/happy-path.integration.ts b/test/integration/vaults/happy-path.integration.ts index d1d716460..b1cb7030a 100644 --- a/test/integration/vaults/happy-path.integration.ts +++ b/test/integration/vaults/happy-path.integration.ts @@ -18,14 +18,7 @@ import { log, prepareLocalMerkleTree, } from "lib"; -import { - getProtocolContext, - norEnsureOperators, - OracleReportParams, - ProtocolContext, - report, - sdvtEnsureOperators, -} from "lib/protocol"; +import { getProtocolContext, norSdvtEnsureOperators, OracleReportParams, ProtocolContext, report } from "lib/protocol"; import { reportVaultDataWithProof } from "lib/protocol/helpers/vaults"; import { bailOnFailure, Snapshot } from "test/suite"; @@ -120,8 +113,8 @@ describe("Scenario: Staking Vaults Happy Path", () => { it("Should have at least 10 deposited node operators in NOR", async () => { const { depositSecurityModule, lido } = ctx.contracts; - await norEnsureOperators(ctx, 10n, 1n); - await sdvtEnsureOperators(ctx, 10n, 1n); + await norSdvtEnsureOperators(ctx, ctx.contracts.nor, 10n, 1n); + await norSdvtEnsureOperators(ctx, ctx.contracts.sdvt, 10n, 1n); expect(await ctx.contracts.nor.getNodeOperatorsCount()).to.be.at.least(10n); expect(await ctx.contracts.sdvt.getNodeOperatorsCount()).to.be.at.least(10n);