diff --git a/contracts/harness/AlertingHarness.sol b/contracts/harness/AlertingHarness.sol new file mode 100644 index 0000000000..c6fcee0937 --- /dev/null +++ b/contracts/harness/AlertingHarness.sol @@ -0,0 +1,310 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +// ======================================================================================================= // +// DISCLAIMER: This contract is provided for tooling purposes only and is NOT part of the Lido core protocol. +// It is not audited, and may be updated in the future without notice. +// ======================================================================================================= // + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; + +import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; +import {LazyOracle} from "contracts/0.8.25/vaults/LazyOracle.sol"; +import {PredepositGuarantee} from "contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol"; + +/// @title AlertingHarness +/// @dev this contract is NOT a part of the Lido core protocol logic, it is only used for tooling purposes +contract AlertingHarness { + /// @notice reference to the Lido locator contract used to resolve protocol contract addresses + ILidoLocator public immutable LIDO_LOCATOR; + + /// @notice structure containing relevant data from an underlying contract + /// @param nodeOperator the address of the node operator + /// @param depositor the address of the depositor + /// @param owner the address of the owner + /// @param pendingOwner the address of the pending owner + /// @param stagedBalance the staged balance + /// @param availableBalance the available balance + /// @param beaconChainDepositsPaused the status of the beacon chain deposits + struct ContractInfo { + address nodeOperator; + address depositor; + address owner; + address pendingOwner; + uint256 stagedBalance; + uint256 availableBalance; + bool beaconChainDepositsPaused; + } + + /// @notice structure containing relevant data for a single connected vault + /// @param vault The address of the vault + /// @param connection The current connection parameters for the vault (such as limits and owner info) + /// @param record the current accounting record for the vault (liabilities, report, in/out delta, etc.) + /// @param quarantineInfo the quarantine info (if any) for the vault from LazyOracle + /// @param contractData the data from the underlying staking vault contracts + /// @param pendingActivationsCount the number of pending validator activations in the vault (from PredepositGuarantee) + struct VaultData { + address vault; + VaultHub.VaultConnection connection; + VaultHub.VaultRecord record; + LazyOracle.QuarantineInfo quarantineInfo; + ContractInfo contractInfo; + uint256 pendingActivationsCount; + } + + struct VaultConnectionData { + address vault; + VaultHub.VaultConnection connection; + } + + struct VaultRecordData { + address vault; + VaultHub.VaultRecord record; + } + + struct VaultQuarantineInfoData { + address vault; + LazyOracle.QuarantineInfo quarantineInfo; + } + + struct VaultPendingActivationsData { + address vault; + uint256 pendingActivationsCount; + } + + struct VaultContractInfoData { + address vault; + ContractInfo contractInfo; + } + + error ZeroAddress(string _argument); + + /// @notice initializes the AlertingHarness and stores the locator contract address + /// @param _lidoLocator the address of the Lido locator contract + constructor(address _lidoLocator) { + if (_lidoLocator == address(0)) revert ZeroAddress("_lidoLocator"); + + LIDO_LOCATOR = ILidoLocator(_lidoLocator); + } + + /// @notice retrieves structured data for a single vault + /// @param _vault the address of the vault to query + /// @return vault data for the queried vault + function getVaultData(address _vault) external view returns (VaultData memory) { + return _collectVaultData( + _vault, + _vaultHub(), + _lazyOracle(), + _predepositGuarantee() + ); + } + + /// @notice retrieves structured data for a batch of vaults in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit the maximum number of vaults to return in this batch + /// @return batch of VaultData structs for the requested vaults + function batchVaultData( + uint256 _offset, + uint256 _limit + ) external view returns (VaultData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + + if (batchSize == 0) return new VaultData[](0); + + LazyOracle lazyOracle = _lazyOracle(); + PredepositGuarantee predepositGuarantee = _predepositGuarantee(); + + batch = new VaultData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = _collectVaultData(vault, vaultHub, lazyOracle, predepositGuarantee); + } + } + + /// @notice retrieves batch of VaultHub.VaultConnection structs in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit maximum number of items to return in the batch + /// @return batch of VaultConnectionData structs for the requested vaults + function batchVaultConnections( + uint256 _offset, + uint256 _limit + ) external view returns (VaultConnectionData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + + if (batchSize == 0) return new VaultConnectionData[](0); + + batch = new VaultConnectionData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultConnectionData({ + vault: vault, + connection: vaultHub.vaultConnection(vault) + }); + } + } + + /// @notice retrieves batch of VaultHub.VaultRecord structs in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit maximum number of items to return in the batch + /// @return batch of VaultRecordData structs for the requested vaults + function batchVaultRecords( + uint256 _offset, + uint256 _limit + ) external view returns (VaultRecordData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + + if (batchSize == 0) return new VaultRecordData[](0); + + batch = new VaultRecordData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultRecordData({ + vault: vault, + record: vaultHub.vaultRecord(vault) + }); + } + } + + /// @notice retrieves batch of LazyOracle.QuarantineInfo structs in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit maximum number of items to return in the batch + /// @return batch of VaultQuarantineInfoData structs for the requested vaults + function batchVaultQuarantines( + uint256 _offset, + uint256 _limit + ) external view returns (VaultQuarantineInfoData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + if (batchSize == 0) return new VaultQuarantineInfoData[](0); + + LazyOracle lazyOracle = _lazyOracle(); + + batch = new VaultQuarantineInfoData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultQuarantineInfoData({ + vault: vault, + quarantineInfo: lazyOracle.vaultQuarantine(vault) + }); + } + } + + /// @notice retrieves batch of VaultPendingActivationsData structs in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit maximum number of items to return in the batch + /// @return batch of VaultPendingActivationsData structs for the requested vaults + function batchPendingActivations( + uint256 _offset, + uint256 _limit + ) external view returns (VaultPendingActivationsData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + if (batchSize == 0) return new VaultPendingActivationsData[](0); + + PredepositGuarantee predepositGuarantee = _predepositGuarantee(); + + batch = new VaultPendingActivationsData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultPendingActivationsData({ + vault: vault, + pendingActivationsCount: predepositGuarantee.pendingActivations(IStakingVault(vault)) + }); + } + } + + /// @notice retrieves batch of ContractInfo structs in a single call + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit maximum number of items to return in the batch + /// @return batch of ContractInfo structs for the requested vaults + function batchStakingVaultData( + uint256 _offset, + uint256 _limit + ) external view returns (VaultContractInfoData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + if (batchSize == 0) return new VaultContractInfoData[](0); + + batch = new VaultContractInfoData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultContractInfoData({ + vault: vault, + contractInfo: _collectContractInfo(IStakingVault(vault)) + }); + } + } + /// @notice helper to calculate actual batch size based on vault hub + /// @param _offset the starting vault index in the hub [0, vaultsCount) + /// @param _limit requested batch size + /// @return vaultHub the VaultHub contract instance + /// @return batchSize actual batch size to use (0 if offset out of range) + function _getBatchSize( + uint256 _offset, + uint256 _limit + ) private view returns (VaultHub vaultHub, uint256 batchSize) { + vaultHub = _vaultHub(); + uint256 vaultsCount = vaultHub.vaultsCount(); + + if (_offset >= vaultsCount) return (vaultHub, 0); + + batchSize = _offset + _limit > vaultsCount ? vaultsCount - _offset : _limit; + } + + /// @notice internal utility to collect vault data from multiple protocol contracts + /// @param _vault vault address to collect data for + /// @param vaultHub vaultHub contract instance + /// @param lazyOracle lazyOracle contract instance + /// @param predepositGuarantee predepositGuarantee contract instance + /// @return populated vaultData structure + function _collectVaultData( + address _vault, + VaultHub vaultHub, + LazyOracle lazyOracle, + PredepositGuarantee predepositGuarantee + ) internal view returns (VaultData memory) { + IStakingVault stakingVault = IStakingVault(_vault); + return VaultData({ + vault: _vault, + connection: vaultHub.vaultConnection(_vault), + record: vaultHub.vaultRecord(_vault), + quarantineInfo: lazyOracle.vaultQuarantine(_vault), + pendingActivationsCount: predepositGuarantee.pendingActivations(stakingVault), + contractInfo: _collectContractInfo(stakingVault) + }); + } + + /// @notice helper to collect staking vault data from a single staking vault + /// @param _stakingVault the staking vault to collect data from + /// @return populated stakingVaultData structure + function _collectContractInfo(IStakingVault _stakingVault) internal view returns (ContractInfo memory) { + return ContractInfo({ + nodeOperator: _stakingVault.nodeOperator(), + depositor: _stakingVault.depositor(), + owner: _stakingVault.owner(), + pendingOwner: _stakingVault.pendingOwner(), + stagedBalance: _stakingVault.stagedBalance(), + availableBalance: _stakingVault.availableBalance(), + beaconChainDepositsPaused: _stakingVault.beaconChainDepositsPaused() + }); + } + + /// @notice helper to resolve the current VaultHub contract from the locator + /// @return contract instance of VaultHub + function _vaultHub() internal view returns (VaultHub) { + return VaultHub(payable(LIDO_LOCATOR.vaultHub())); + } + + /// @notice helper to resolve the current LazyOracle contract from the locator + /// @return contract instance of LazyOracle + function _lazyOracle() internal view returns (LazyOracle) { + return LazyOracle(LIDO_LOCATOR.lazyOracle()); + } + + /// @notice helper to resolve the current PredepositGuarantee contract from the locator + /// @return contract instance of PredepositGuarantee + function _predepositGuarantee() internal view returns (PredepositGuarantee) { + return PredepositGuarantee(LIDO_LOCATOR.predepositGuarantee()); + } +} diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 8b14dc15c5..56844cdd60 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -34,6 +34,13 @@ "constructorArgs": ["0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", 12, 1742213400] } }, + "alertingHarness": { + "implementation": { + "contract": "contracts/harness/AlertingHarness.sol", + "address": "0xc12ae7e57c927870939030De93487D06E8Ab69ce", + "constructorArgs": ["0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] + } + }, "apmRegistryFactory": { "contract": "@aragon/os/contracts/factory/APMRegistryFactory.sol", "address": "0x1C839726F7cEb0b96ABD56F4D9Bc4627Fb216AC8", diff --git a/deployed-mainnet.json b/deployed-mainnet.json index c09c5edbf3..5f067480fb 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -25,6 +25,13 @@ ] } }, + "alertingHarness": { + "implementation": { + "contract": "contracts/harness/AlertingHarness.sol", + "address": "0xe3f9D755b3240AF7C988B05588A38461D40Bd558", + "constructorArgs": ["0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"] + } + }, "apmRegistryFactoryAddress": "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A", "app:aragon-agent": { "implementation": { diff --git a/lib/state-file.ts b/lib/state-file.ts index 22b40843e9..b16dda074f 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -116,8 +116,10 @@ export enum Sk { dgEmergencyProtectedTimelock = "dg:emergencyProtectedTimelock", // Easy Track easyTrack = "easyTrack", - vaultsAdapter = "vaultsAdapter", easyTrackEVMScriptExecutor = "easyTrackEVMScriptExecutor", + vaultsAdapter = "vaultsAdapter", + // Harnesses + alertingHarness = "alertingHarness", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/deploy-alerting-harness.sh b/scripts/deploy-alerting-harness.sh new file mode 100755 index 0000000000..00124d53be --- /dev/null +++ b/scripts/deploy-alerting-harness.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e +u +set -o pipefail + +export NETWORK=${NETWORK:="hoodi"} # if defined use the value set to default otherwise +export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise + +export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} # first acc of default mnemonic "test test ..." +export GAS_PRIORITY_FEE=1 +export GAS_MAX_FEE=2 +export GAS_LIMIT=20000000 + +export NETWORK_STATE_FILE=${NETWORK_STATE_FILE:="deployed-hoodi.json"} +export STEPS_FILE=harness/steps-deploy-alerting-harness.json + +yarn hardhat --network $NETWORK run --no-compile scripts/utils/migrate.ts diff --git a/scripts/harness/steps-deploy-alerting-harness.json b/scripts/harness/steps-deploy-alerting-harness.json new file mode 100644 index 0000000000..efacbc01d7 --- /dev/null +++ b/scripts/harness/steps-deploy-alerting-harness.json @@ -0,0 +1,3 @@ +{ + "steps": ["harness/steps/0000-check-env", "harness/steps/0100-deploy-alerting-harness"] +} diff --git a/scripts/harness/steps/0000-check-env.ts b/scripts/harness/steps/0000-check-env.ts new file mode 100644 index 0000000000..dac0c4ec0f --- /dev/null +++ b/scripts/harness/steps/0000-check-env.ts @@ -0,0 +1,33 @@ +import { ethers } from "hardhat"; + +import { cy, log } from "lib"; + +export async function main() { + const deployer = (await ethers.provider.getSigner()).address; + if (deployer !== process.env.DEPLOYER) { + throw new Error(`Deployer address mismatch: env DEPLOYER=${process.env.DEPLOYER}, signer=${deployer}`); + } + + if (!process.env.NETWORK_STATE_FILE) { + throw new Error("Env variable NETWORK_STATE_FILE is not set"); + } + + if (!process.env.GAS_PRIORITY_FEE) { + throw new Error("Env variable GAS_PRIORITY_FEE is not set"); + } + + if (!process.env.GAS_MAX_FEE) { + throw new Error("Env variable GAS_MAX_FEE is not set"); + } + + if (!process.env.GAS_LIMIT) { + throw new Error("Env variable GAS_LIMIT is not set"); + } + + if (process.env.MODE === "scratch" && !process.env.GENESIS_TIME) { + throw new Error("Env variable GENESIS_TIME is not set"); + } + + const latestBlockNumber = await ethers.provider.getBlockNumber(); + log(cy(`Latest block number: ${latestBlockNumber}`)); +} diff --git a/scripts/harness/steps/0100-deploy-alerting-harness.ts b/scripts/harness/steps/0100-deploy-alerting-harness.ts new file mode 100644 index 0000000000..a88083d676 --- /dev/null +++ b/scripts/harness/steps/0100-deploy-alerting-harness.ts @@ -0,0 +1,23 @@ +import assert from "assert"; +import { ethers } from "hardhat"; + +import { deployImplementation, readNetworkState, Sk } from "lib"; + +export async function main(): Promise { + const deployer = (await ethers.provider.getSigner()).address; + assert.equal(process.env.DEPLOYER, deployer); + + const state = readNetworkState(); + + // + // Extract necessary addresses and parameters from the state + // + const locatorAddress = state[Sk.lidoLocator].proxy.address; + + // + // New AlertingHarness deployment + // + const alertingHarness = await deployImplementation(Sk.alertingHarness, "AlertingHarness", deployer, [locatorAddress]); + const alertingHarnessAddress = await alertingHarness.getAddress(); + console.log("AlertingHarness address", alertingHarnessAddress); +} diff --git a/test/0.8.25/vaults/vaulthub/contracts/LazyOracle__MockForVaultHub.sol b/test/0.8.25/vaults/vaulthub/contracts/LazyOracle__MockForVaultHub.sol index 0df3893bb2..b65b9e0612 100644 --- a/test/0.8.25/vaults/vaulthub/contracts/LazyOracle__MockForVaultHub.sol +++ b/test/0.8.25/vaults/vaulthub/contracts/LazyOracle__MockForVaultHub.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.25; import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; - +import {LazyOracle} from "contracts/0.8.25/vaults/LazyOracle.sol"; import "hardhat/console.sol"; contract LazyOracle__MockForVaultHub { @@ -50,4 +50,15 @@ contract LazyOracle__MockForVaultHub { _reportSlashingReserve ); } + + function vaultQuarantine(address _vault) external view returns (LazyOracle.QuarantineInfo memory) { + return + LazyOracle.QuarantineInfo({ + isActive: isVaultQuarantined[_vault], + pendingTotalValueIncrease: 0, + startTimestamp: 0, + endTimestamp: 0, + totalValueRemainder: 0 + }); + } } diff --git a/test/harness/alertingHarness.test.ts b/test/harness/alertingHarness.test.ts new file mode 100644 index 0000000000..f9a3f95bf1 --- /dev/null +++ b/test/harness/alertingHarness.test.ts @@ -0,0 +1,270 @@ +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + AlertingHarness, + LazyOracle__MockForVaultHub, + Lido, + LidoLocator, + StakingVault__MockForVaultHub, + VaultHub, +} from "typechain-types"; + +import { ether } from "lib"; + +import { deployVaults } from "test/deploy/vaults"; +import { Snapshot } from "test/suite"; + +describe("AlertingHarness.sol", () => { + let deployer: HardhatEthersSigner; + let user: HardhatEthersSigner; + let operator: HardhatEthersSigner; + + let lido: Lido; + let locator: LidoLocator; + let vaultHub: VaultHub; + let lazyOracle: LazyOracle__MockForVaultHub; + let alertingHarness: AlertingHarness; + + let createMockStakingVaultAndConnect: ( + owner: HardhatEthersSigner, + operator: HardhatEthersSigner, + ) => Promise; + + let reportVaultHelper: (report: { + vault: StakingVault__MockForVaultHub; + reportTimestamp?: bigint; + totalValue?: bigint; + inOutDelta?: bigint; + liabilityShares?: bigint; + maxLiabilityShares?: bigint; + cumulativeLidoFees?: bigint; + slashingReserve?: bigint; + }) => Promise; + + let originalState: string; + + before(async () => { + [deployer, user, operator] = await ethers.getSigners(); + + const vaultsSetup = await deployVaults({ deployer, admin: user }); + lido = vaultsSetup.lido; + vaultHub = vaultsSetup.vaultHub; + lazyOracle = vaultsSetup.lazyOracle; + createMockStakingVaultAndConnect = vaultsSetup.createMockStakingVaultAndConnect; + reportVaultHelper = vaultsSetup.reportVault; + + locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); + + alertingHarness = await ethers.deployContract("AlertingHarness", [await locator.getAddress()]); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + describe("constructor", () => { + it("reverts if locator is zero address", async () => { + await expect(ethers.deployContract("AlertingHarness", [ZeroAddress])).to.be.revertedWithCustomError( + alertingHarness, + "ZeroAddress", + ); + }); + + it("sets LIDO_LOCATOR correctly", async () => { + expect(await alertingHarness.LIDO_LOCATOR()).to.equal(await locator.getAddress()); + }); + }); + + describe("getVaultData", () => { + it("returns correct vault data for a connected vault", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + await reportVaultHelper({ vault, totalValue: ether("100") }); + + const vaultData = await alertingHarness.getVaultData(await vault.getAddress()); + + expect(vaultData.vault).to.equal(await vault.getAddress()); + expect(vaultData.connection.vaultIndex).to.be.greaterThan(0); + expect(vaultData.record.report.totalValue).to.equal(ether("100")); + expect(vaultData.record.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit + expect(vaultData.pendingActivationsCount).to.equal(0n); + expect(vaultData.contractInfo.nodeOperator).to.equal(await operator.getAddress()); + expect(vaultData.contractInfo.depositor).to.equal(await locator.predepositGuarantee()); + expect(vaultData.contractInfo.owner).to.equal(await locator.vaultHub()); + expect(vaultData.contractInfo.pendingOwner).to.equal(ZeroAddress); + expect(vaultData.contractInfo.stagedBalance).to.equal(0n); + expect(vaultData.contractInfo.availableBalance).to.equal(ether("1")); + expect(vaultData.contractInfo.beaconChainDepositsPaused).to.equal(false); + }); + }); + + describe("batchVaultData", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchVaultData(0, 10); + expect(batch).to.have.length(0); + }); + + it("returns correct data for single vault", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + await reportVaultHelper({ vault, totalValue: ether("100") }); + expect(await vaultHub.vaultsCount()).to.equal(1); + + const batch = await alertingHarness.batchVaultData(0, 10); + + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + }); + + it("returns correct data for multiple vaults", async () => { + const vault1 = await createMockStakingVaultAndConnect(user, operator); + const vault2 = await createMockStakingVaultAndConnect(user, operator); + const vault3 = await createMockStakingVaultAndConnect(user, operator); + + await reportVaultHelper({ vault: vault1, totalValue: ether("100") }); + await reportVaultHelper({ vault: vault2, totalValue: ether("200") }); + await reportVaultHelper({ vault: vault3, totalValue: ether("300") }); + expect(await vaultHub.vaultsCount()).to.equal(3); + + const batch = await alertingHarness.batchVaultData(0, 10); + expect(batch).to.have.length(3); + expect(batch[0].vault).to.equal(await vault1.getAddress()); + expect(batch[1].vault).to.equal(await vault2.getAddress()); + expect(batch[2].vault).to.equal(await vault3.getAddress()); + }); + + it("respects limit parameter", async () => { + await createMockStakingVaultAndConnect(user, operator); + await createMockStakingVaultAndConnect(user, operator); + await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchVaultData(1, 2); + expect(batch).to.have.length(2); + }); + + it("respects offset parameter", async () => { + await createMockStakingVaultAndConnect(user, operator); + const vault = await createMockStakingVaultAndConnect(user, operator); + await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchVaultData(1, 10); + expect(batch).to.have.length(2); + expect(batch[0].vault).to.equal(await vault.getAddress()); + }); + + it("returns empty array when offset exceeds vault count", async () => { + await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchVaultData(100, 10); + expect(batch).to.have.length(0); + }); + + it("returns partial batch when offset + limit exceeds vault count", async () => { + await createMockStakingVaultAndConnect(user, operator); + await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchVaultData(1, 10); + expect(batch).to.have.length(1); + }); + }); + + describe("batchVaultConnections", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchVaultConnections(1, 10); + expect(batch).to.have.length(0); + }); + + it("returns correct connection data for vaults", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchVaultConnections(0, 10); + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + expect(batch[0].connection.vaultIndex).to.be.greaterThan(0); + }); + }); + + describe("batchVaultRecords", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchVaultRecords(0, 10); + expect(batch).to.have.length(0); + }); + + it("returns correct record data for vaults", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + await reportVaultHelper({ vault, totalValue: ether("100") }); + + const batch = await alertingHarness.batchVaultRecords(0, 10); + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + expect(batch[0].record.report.totalValue).to.equal(ether("100")); + expect(batch[0].record.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit + }); + }); + + describe("batchVaultQuarantines", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchVaultQuarantines(0, 10); + expect(batch).to.have.length(0); + }); + + it("returns quarantine info for vaults", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + await lazyOracle.mock__setIsVaultQuarantined(await vault.getAddress(), true); + + const batch = await alertingHarness.batchVaultQuarantines(0, 10); + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + expect(batch[0].quarantineInfo.isActive).to.equal(true); + expect(batch[0].quarantineInfo.pendingTotalValueIncrease).to.equal(0n); + expect(batch[0].quarantineInfo.startTimestamp).to.equal(0n); + expect(batch[0].quarantineInfo.endTimestamp).to.equal(0n); + expect(batch[0].quarantineInfo.totalValueRemainder).to.equal(0n); + }); + }); + + describe("batchPendingActivations", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchPendingActivations(0, 10); + expect(batch).to.have.length(0); + }); + + it("returns pending activations count for vaults", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchPendingActivations(0, 10); + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + expect(batch[0].pendingActivationsCount).to.equal(0n); + }); + }); + + describe("batchStakingVaultData", () => { + it("returns empty array when no vaults exist", async () => { + const batch = await alertingHarness.batchStakingVaultData(0, 10); + expect(batch).to.have.length(0); + }); + + it("returns staking vault data for vaults", async () => { + const vault = await createMockStakingVaultAndConnect(user, operator); + + const batch = await alertingHarness.batchStakingVaultData(0, 10); + + expect(batch).to.have.length(1); + expect(batch[0].vault).to.equal(await vault.getAddress()); + expect(batch[0].contractInfo.nodeOperator).to.equal(await operator.getAddress()); + expect(batch[0].contractInfo.depositor).to.equal(await locator.predepositGuarantee()); + expect(batch[0].contractInfo.owner).to.equal(await locator.vaultHub()); + expect(batch[0].contractInfo.pendingOwner).to.equal(ZeroAddress); + expect(batch[0].contractInfo.stagedBalance).to.equal(0n); + expect(batch[0].contractInfo.availableBalance).to.equal(ether("1")); + expect(batch[0].contractInfo.beaconChainDepositsPaused).to.equal(false); + }); + }); +});