From 43f40d822e9cb641eedf6fddd32d5d7f63375201 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Nov 2025 13:12:52 +0000 Subject: [PATCH 1/6] feat: alerting harness contract --- contracts/harness/AlertingHarness.sol | 244 ++++++++++++++++++ deployed-hoodi.json | 7 + lib/state-file.ts | 2 + scripts/dao-deploy-alerting-harness.sh | 15 ++ .../steps-deploy-alerting-harness.json | 3 + scripts/harness/steps/0000-check-env.ts | 29 +++ .../steps/0100-deploy-alerting-harness.ts | 23 ++ .../contracts/LazyOracle__MockForVaultHub.sol | 13 +- test/harness/alertingHarness.test.ts | 240 +++++++++++++++++ 9 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 contracts/harness/AlertingHarness.sol create mode 100755 scripts/dao-deploy-alerting-harness.sh create mode 100644 scripts/harness/steps-deploy-alerting-harness.json create mode 100644 scripts/harness/steps/0000-check-env.ts create mode 100644 scripts/harness/steps/0100-deploy-alerting-harness.ts create mode 100644 test/harness/alertingHarness.test.ts diff --git a/contracts/harness/AlertingHarness.sol b/contracts/harness/AlertingHarness.sol new file mode 100644 index 0000000000..602d83345f --- /dev/null +++ b/contracts/harness/AlertingHarness.sol @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +// 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 +/// @notice provides a harness for gathering vault-related data +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 for a single connected vault + /// @param vault The address of the vault + /// @param vaultConnection The current connection parameters for the vault (such as limits and owner info) + /// @param vaultRecord the current accounting record for the vault (liabilities, report, in/out delta, etc.) + /// @param vaultQuarantineInfo the quarantine info (if any) for the vault from LazyOracle + /// @param vaultPendingActivationsCount the number of pending validator activations in the vault (from PredepositGuarantee) + struct VaultData { + address vault; + VaultHub.VaultConnection vaultConnection; + VaultHub.VaultRecord vaultRecord; + LazyOracle.QuarantineInfo vaultQuarantineInfo; + uint256 vaultPendingActivationsCount; + } + + struct VaultConnectionData { + address vault; + VaultHub.VaultConnection vaultConnection; + } + + struct VaultRecordData { + address vault; + VaultHub.VaultRecord vaultRecord; + } + + struct VaultQuarantineInfoData { + address vault; + LazyOracle.QuarantineInfo vaultQuarantineInfo; + } + + struct VaultPendingActivationsData { + address vault; + uint256 vaultPendingActivationsCount; + } + + 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, + vaultConnection: 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, + vaultRecord: 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, + vaultQuarantineInfo: 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, + vaultPendingActivationsCount: predepositGuarantee.pendingActivations(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) { + return VaultData({ + vault: _vault, + vaultConnection: vaultHub.vaultConnection(_vault), + vaultRecord: vaultHub.vaultRecord(_vault), + vaultQuarantineInfo: lazyOracle.vaultQuarantine(_vault), + vaultPendingActivationsCount: predepositGuarantee.pendingActivations(IStakingVault(_vault)) + }); + } + + /// @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()); + } +} \ No newline at end of file diff --git a/deployed-hoodi.json b/deployed-hoodi.json index e9b486edf7..d5eb9be5e8 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": "0x1850A3c2F9be809dA70360a76D1A524Ed3e090E3", + "constructorArgs": ["0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] + } + }, "apmRegistryFactory": { "contract": "@aragon/os/contracts/factory/APMRegistryFactory.sol", "address": "0x1C839726F7cEb0b96ABD56F4D9Bc4627Fb216AC8", diff --git a/lib/state-file.ts b/lib/state-file.ts index ce01902116..44a026b076 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -114,6 +114,8 @@ export enum Sk { // Easy Track easyTrack = "easyTrack", vaultsAdapter = "vaultsAdapter", + // Harnesses + alertingHarness = "alertingHarness", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/dao-deploy-alerting-harness.sh b/scripts/dao-deploy-alerting-harness.sh new file mode 100755 index 0000000000..6b6ae99fa5 --- /dev/null +++ b/scripts/dao-deploy-alerting-harness.sh @@ -0,0 +1,15 @@ +#!/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=100 + +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..c5159f9a3a --- /dev/null +++ b/scripts/harness/steps/0000-check-env.ts @@ -0,0 +1,29 @@ +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.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..cafc789b85 --- /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 + // + await deployImplementation(Sk.alertingHarness, "AlertingHarness", deployer, [locatorAddress]); + const newAlertingHarnessAddress = state[Sk.alertingHarness].implementation.address; + console.log("New AlertingHarness address", newAlertingHarnessAddress); +} 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..0eb8cbce5e --- /dev/null +++ b/test/harness/alertingHarness.test.ts @@ -0,0 +1,240 @@ +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.vaultConnection.vaultIndex).to.be.greaterThan(0); + expect(vaultData.vaultRecord.report.totalValue).to.equal(ether("100")); + expect(vaultData.vaultRecord.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit + expect(vaultData.vaultPendingActivationsCount).to.equal(0n); + }); + }); + + 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].vaultConnection.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].vaultRecord.report.totalValue).to.equal(ether("100")); + expect(batch[0].vaultRecord.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].vaultQuarantineInfo.isActive).to.equal(true); + expect(batch[0].vaultQuarantineInfo.pendingTotalValueIncrease).to.equal(0n); + expect(batch[0].vaultQuarantineInfo.startTimestamp).to.equal(0n); + expect(batch[0].vaultQuarantineInfo.endTimestamp).to.equal(0n); + expect(batch[0].vaultQuarantineInfo.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].vaultPendingActivationsCount).to.equal(0n); + }); + }); +}); From 588cda9e97ef0d86937aef68e1550cf06b06929a Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Thu, 13 Nov 2025 17:58:45 +0100 Subject: [PATCH 2/6] feat: add version --- contracts/harness/AlertingHarness.sol | 74 +++++++++++++++++++++++++-- test/harness/alertingHarness.test.ts | 36 +++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/contracts/harness/AlertingHarness.sol b/contracts/harness/AlertingHarness.sol index 602d83345f..797c74b58a 100644 --- a/contracts/harness/AlertingHarness.sol +++ b/contracts/harness/AlertingHarness.sol @@ -18,6 +18,27 @@ contract AlertingHarness { /// @notice reference to the Lido locator contract used to resolve protocol contract addresses ILidoLocator public immutable LIDO_LOCATOR; + /// @notice version of the contract + string public constant VERSION = "1.0.0"; + + /// @notice structure containing relevant data for a single staking vault + /// @param stakingVault the address of the staking vault + /// @param stakingVaultNodeOperator the address of the node operator for the staking vault + /// @param stakingVaultDepositor the address of the depositor for the staking vault + /// @param stakingVaultOwner the address of the owner for the staking vault + /// @param stakingVaultPendingOwner the address of the pending owner for the staking vault + /// @param stakingVaultStagedBalance the staged balance of the staking vault + /// @param stakingVaultAvailableBalance the available balance of the staking vault + struct StakingVaultData { + address stakingVaultNodeOperator; + address stakingVaultDepositor; + address stakingVaultOwner; + address stakingVaultPendingOwner; + uint256 stakingVaultStagedBalance; + uint256 stakingVaultAvailableBalance; + bool stakingVaultBeaconChainDepositsPaused; + } + /// @notice structure containing relevant data for a single connected vault /// @param vault The address of the vault /// @param vaultConnection The current connection parameters for the vault (such as limits and owner info) @@ -30,6 +51,7 @@ contract AlertingHarness { VaultHub.VaultRecord vaultRecord; LazyOracle.QuarantineInfo vaultQuarantineInfo; uint256 vaultPendingActivationsCount; + StakingVaultData stakingVaultData; } struct VaultConnectionData { @@ -52,6 +74,11 @@ contract AlertingHarness { uint256 vaultPendingActivationsCount; } + struct VaultStakingVaultData { + address vault; + StakingVaultData stakingVaultData; + } + error ZeroAddress(string _argument); /// @notice initializes the AlertingHarness and stores the locator contract address @@ -67,9 +94,9 @@ contract AlertingHarness { /// @return vault data for the queried vault function getVaultData(address _vault) external view returns (VaultData memory) { return _collectVaultData( - _vault, - _vaultHub(), - _lazyOracle(), + _vault, + _vaultHub(), + _lazyOracle(), _predepositGuarantee() ); } @@ -186,6 +213,26 @@ contract AlertingHarness { } } + /// @notice retrieves batch of StakingVaultData 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 StakingVaultData structs for the requested vaults + function batchStakingVaultData( + uint256 _offset, + uint256 _limit + ) external view returns (VaultStakingVaultData[] memory batch) { + (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); + if (batchSize == 0) return new VaultStakingVaultData[](0); + + batch = new VaultStakingVaultData[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + address vault = vaultHub.vaultByIndex(_offset + i + 1); + batch[i] = VaultStakingVaultData({ + vault: vault, + stakingVaultData: _collectStakingVaultData(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 @@ -197,7 +244,7 @@ contract AlertingHarness { ) 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; @@ -215,12 +262,29 @@ contract AlertingHarness { LazyOracle lazyOracle, PredepositGuarantee predepositGuarantee ) internal view returns (VaultData memory) { + IStakingVault stakingVault = IStakingVault(_vault); return VaultData({ vault: _vault, vaultConnection: vaultHub.vaultConnection(_vault), vaultRecord: vaultHub.vaultRecord(_vault), vaultQuarantineInfo: lazyOracle.vaultQuarantine(_vault), - vaultPendingActivationsCount: predepositGuarantee.pendingActivations(IStakingVault(_vault)) + vaultPendingActivationsCount: predepositGuarantee.pendingActivations(stakingVault), + stakingVaultData: _collectStakingVaultData(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 _collectStakingVaultData(IStakingVault _stakingVault) internal view returns (StakingVaultData memory) { + return StakingVaultData({ + stakingVaultNodeOperator: _stakingVault.nodeOperator(), + stakingVaultDepositor: _stakingVault.depositor(), + stakingVaultOwner: _stakingVault.owner(), + stakingVaultPendingOwner: _stakingVault.pendingOwner(), + stakingVaultStagedBalance: _stakingVault.stagedBalance(), + stakingVaultAvailableBalance: _stakingVault.availableBalance(), + stakingVaultBeaconChainDepositsPaused: _stakingVault.beaconChainDepositsPaused() }); } diff --git a/test/harness/alertingHarness.test.ts b/test/harness/alertingHarness.test.ts index 0eb8cbce5e..f0546ec8ce 100644 --- a/test/harness/alertingHarness.test.ts +++ b/test/harness/alertingHarness.test.ts @@ -79,6 +79,12 @@ describe("AlertingHarness.sol", () => { }); }); + describe("version", () => { + it("returns correct version", async () => { + expect(await alertingHarness.VERSION()).to.equal("1.0.0"); + }); + }); + describe("getVaultData", () => { it("returns correct vault data for a connected vault", async () => { const vault = await createMockStakingVaultAndConnect(user, operator); @@ -92,6 +98,13 @@ describe("AlertingHarness.sol", () => { expect(vaultData.vaultRecord.report.totalValue).to.equal(ether("100")); expect(vaultData.vaultRecord.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit expect(vaultData.vaultPendingActivationsCount).to.equal(0n); + expect(vaultData.stakingVaultData.stakingVaultNodeOperator).to.equal(await operator.getAddress()); + expect(vaultData.stakingVaultData.stakingVaultDepositor).to.equal(await locator.predepositGuarantee()); + expect(vaultData.stakingVaultData.stakingVaultOwner).to.equal(await locator.vaultHub()); + expect(vaultData.stakingVaultData.stakingVaultPendingOwner).to.equal(ZeroAddress); + expect(vaultData.stakingVaultData.stakingVaultStagedBalance).to.equal(0n); + expect(vaultData.stakingVaultData.stakingVaultAvailableBalance).to.equal(ether("1")); + expect(vaultData.stakingVaultData.stakingVaultBeaconChainDepositsPaused).to.equal(false); }); }); @@ -237,4 +250,27 @@ describe("AlertingHarness.sol", () => { expect(batch[0].vaultPendingActivationsCount).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].stakingVaultData.stakingVaultNodeOperator).to.equal(await operator.getAddress()); + expect(batch[0].stakingVaultData.stakingVaultDepositor).to.equal(await locator.predepositGuarantee()); + expect(batch[0].stakingVaultData.stakingVaultOwner).to.equal(await locator.vaultHub()); + expect(batch[0].stakingVaultData.stakingVaultPendingOwner).to.equal(ZeroAddress); + expect(batch[0].stakingVaultData.stakingVaultStagedBalance).to.equal(0n); + expect(batch[0].stakingVaultData.stakingVaultAvailableBalance).to.equal(ether("1")); + expect(batch[0].stakingVaultData.stakingVaultBeaconChainDepositsPaused).to.equal(false); + }); + }); }); From dad1b1e0cc28f104af242a2aabeb1955d377e1c8 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 14 Nov 2025 11:02:47 -0300 Subject: [PATCH 3/6] feat: update alertingHarness --- deployed-hoodi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index ba9a835462..292815d46e 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -37,7 +37,7 @@ "alertingHarness": { "implementation": { "contract": "contracts/harness/AlertingHarness.sol", - "address": "0x1850A3c2F9be809dA70360a76D1A524Ed3e090E3", + "address": "0xA5707977Efa65521de9ca7fed133a70561b51234", "constructorArgs": ["0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] } }, From ccd1318e1662fb5798a38f88875e72a8ff80b384 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 24 Nov 2025 14:56:35 +0000 Subject: [PATCH 4/6] chore: fixes after review --- contracts/harness/AlertingHarness.sol | 128 +++++++++++++------------- test/harness/alertingHarness.test.ts | 60 ++++++------ 2 files changed, 92 insertions(+), 96 deletions(-) diff --git a/contracts/harness/AlertingHarness.sol b/contracts/harness/AlertingHarness.sol index 797c74b58a..c6fcee0937 100644 --- a/contracts/harness/AlertingHarness.sol +++ b/contracts/harness/AlertingHarness.sol @@ -1,6 +1,11 @@ // 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; @@ -11,72 +16,69 @@ 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 -/// @notice provides a harness for gathering vault-related data +/// @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 version of the contract - string public constant VERSION = "1.0.0"; - - /// @notice structure containing relevant data for a single staking vault - /// @param stakingVault the address of the staking vault - /// @param stakingVaultNodeOperator the address of the node operator for the staking vault - /// @param stakingVaultDepositor the address of the depositor for the staking vault - /// @param stakingVaultOwner the address of the owner for the staking vault - /// @param stakingVaultPendingOwner the address of the pending owner for the staking vault - /// @param stakingVaultStagedBalance the staged balance of the staking vault - /// @param stakingVaultAvailableBalance the available balance of the staking vault - struct StakingVaultData { - address stakingVaultNodeOperator; - address stakingVaultDepositor; - address stakingVaultOwner; - address stakingVaultPendingOwner; - uint256 stakingVaultStagedBalance; - uint256 stakingVaultAvailableBalance; - bool stakingVaultBeaconChainDepositsPaused; + /// @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 vaultConnection The current connection parameters for the vault (such as limits and owner info) - /// @param vaultRecord the current accounting record for the vault (liabilities, report, in/out delta, etc.) - /// @param vaultQuarantineInfo the quarantine info (if any) for the vault from LazyOracle - /// @param vaultPendingActivationsCount the number of pending validator activations in the vault (from PredepositGuarantee) + /// @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 vaultConnection; - VaultHub.VaultRecord vaultRecord; - LazyOracle.QuarantineInfo vaultQuarantineInfo; - uint256 vaultPendingActivationsCount; - StakingVaultData stakingVaultData; + VaultHub.VaultConnection connection; + VaultHub.VaultRecord record; + LazyOracle.QuarantineInfo quarantineInfo; + ContractInfo contractInfo; + uint256 pendingActivationsCount; } struct VaultConnectionData { address vault; - VaultHub.VaultConnection vaultConnection; + VaultHub.VaultConnection connection; } struct VaultRecordData { address vault; - VaultHub.VaultRecord vaultRecord; + VaultHub.VaultRecord record; } struct VaultQuarantineInfoData { address vault; - LazyOracle.QuarantineInfo vaultQuarantineInfo; + LazyOracle.QuarantineInfo quarantineInfo; } struct VaultPendingActivationsData { address vault; - uint256 vaultPendingActivationsCount; + uint256 pendingActivationsCount; } - struct VaultStakingVaultData { + struct VaultContractInfoData { address vault; - StakingVaultData stakingVaultData; + ContractInfo contractInfo; } error ZeroAddress(string _argument); @@ -140,7 +142,7 @@ contract AlertingHarness { address vault = vaultHub.vaultByIndex(_offset + i + 1); batch[i] = VaultConnectionData({ vault: vault, - vaultConnection: vaultHub.vaultConnection(vault) + connection: vaultHub.vaultConnection(vault) }); } } @@ -162,7 +164,7 @@ contract AlertingHarness { address vault = vaultHub.vaultByIndex(_offset + i + 1); batch[i] = VaultRecordData({ vault: vault, - vaultRecord: vaultHub.vaultRecord(vault) + record: vaultHub.vaultRecord(vault) }); } } @@ -185,7 +187,7 @@ contract AlertingHarness { address vault = vaultHub.vaultByIndex(_offset + i + 1); batch[i] = VaultQuarantineInfoData({ vault: vault, - vaultQuarantineInfo: lazyOracle.vaultQuarantine(vault) + quarantineInfo: lazyOracle.vaultQuarantine(vault) }); } } @@ -208,28 +210,28 @@ contract AlertingHarness { address vault = vaultHub.vaultByIndex(_offset + i + 1); batch[i] = VaultPendingActivationsData({ vault: vault, - vaultPendingActivationsCount: predepositGuarantee.pendingActivations(IStakingVault(vault)) + pendingActivationsCount: predepositGuarantee.pendingActivations(IStakingVault(vault)) }); } } - /// @notice retrieves batch of StakingVaultData structs in a single call + /// @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 StakingVaultData structs for the requested vaults + /// @return batch of ContractInfo structs for the requested vaults function batchStakingVaultData( uint256 _offset, uint256 _limit - ) external view returns (VaultStakingVaultData[] memory batch) { + ) external view returns (VaultContractInfoData[] memory batch) { (VaultHub vaultHub, uint256 batchSize) = _getBatchSize(_offset, _limit); - if (batchSize == 0) return new VaultStakingVaultData[](0); + if (batchSize == 0) return new VaultContractInfoData[](0); - batch = new VaultStakingVaultData[](batchSize); + batch = new VaultContractInfoData[](batchSize); for (uint256 i = 0; i < batchSize; ++i) { address vault = vaultHub.vaultByIndex(_offset + i + 1); - batch[i] = VaultStakingVaultData({ + batch[i] = VaultContractInfoData({ vault: vault, - stakingVaultData: _collectStakingVaultData(IStakingVault(vault)) + contractInfo: _collectContractInfo(IStakingVault(vault)) }); } } @@ -245,7 +247,7 @@ contract AlertingHarness { vaultHub = _vaultHub(); uint256 vaultsCount = vaultHub.vaultsCount(); - if (_offset > vaultsCount) return (vaultHub, 0); + if (_offset >= vaultsCount) return (vaultHub, 0); batchSize = _offset + _limit > vaultsCount ? vaultsCount - _offset : _limit; } @@ -265,26 +267,26 @@ contract AlertingHarness { IStakingVault stakingVault = IStakingVault(_vault); return VaultData({ vault: _vault, - vaultConnection: vaultHub.vaultConnection(_vault), - vaultRecord: vaultHub.vaultRecord(_vault), - vaultQuarantineInfo: lazyOracle.vaultQuarantine(_vault), - vaultPendingActivationsCount: predepositGuarantee.pendingActivations(stakingVault), - stakingVaultData: _collectStakingVaultData(stakingVault) + 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 _collectStakingVaultData(IStakingVault _stakingVault) internal view returns (StakingVaultData memory) { - return StakingVaultData({ - stakingVaultNodeOperator: _stakingVault.nodeOperator(), - stakingVaultDepositor: _stakingVault.depositor(), - stakingVaultOwner: _stakingVault.owner(), - stakingVaultPendingOwner: _stakingVault.pendingOwner(), - stakingVaultStagedBalance: _stakingVault.stagedBalance(), - stakingVaultAvailableBalance: _stakingVault.availableBalance(), - stakingVaultBeaconChainDepositsPaused: _stakingVault.beaconChainDepositsPaused() + 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() }); } @@ -305,4 +307,4 @@ contract AlertingHarness { function _predepositGuarantee() internal view returns (PredepositGuarantee) { return PredepositGuarantee(LIDO_LOCATOR.predepositGuarantee()); } -} \ No newline at end of file +} diff --git a/test/harness/alertingHarness.test.ts b/test/harness/alertingHarness.test.ts index f0546ec8ce..f9a3f95bf1 100644 --- a/test/harness/alertingHarness.test.ts +++ b/test/harness/alertingHarness.test.ts @@ -79,12 +79,6 @@ describe("AlertingHarness.sol", () => { }); }); - describe("version", () => { - it("returns correct version", async () => { - expect(await alertingHarness.VERSION()).to.equal("1.0.0"); - }); - }); - describe("getVaultData", () => { it("returns correct vault data for a connected vault", async () => { const vault = await createMockStakingVaultAndConnect(user, operator); @@ -94,17 +88,17 @@ describe("AlertingHarness.sol", () => { const vaultData = await alertingHarness.getVaultData(await vault.getAddress()); expect(vaultData.vault).to.equal(await vault.getAddress()); - expect(vaultData.vaultConnection.vaultIndex).to.be.greaterThan(0); - expect(vaultData.vaultRecord.report.totalValue).to.equal(ether("100")); - expect(vaultData.vaultRecord.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit - expect(vaultData.vaultPendingActivationsCount).to.equal(0n); - expect(vaultData.stakingVaultData.stakingVaultNodeOperator).to.equal(await operator.getAddress()); - expect(vaultData.stakingVaultData.stakingVaultDepositor).to.equal(await locator.predepositGuarantee()); - expect(vaultData.stakingVaultData.stakingVaultOwner).to.equal(await locator.vaultHub()); - expect(vaultData.stakingVaultData.stakingVaultPendingOwner).to.equal(ZeroAddress); - expect(vaultData.stakingVaultData.stakingVaultStagedBalance).to.equal(0n); - expect(vaultData.stakingVaultData.stakingVaultAvailableBalance).to.equal(ether("1")); - expect(vaultData.stakingVaultData.stakingVaultBeaconChainDepositsPaused).to.equal(false); + 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); }); }); @@ -190,7 +184,7 @@ describe("AlertingHarness.sol", () => { 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].vaultConnection.vaultIndex).to.be.greaterThan(0); + expect(batch[0].connection.vaultIndex).to.be.greaterThan(0); }); }); @@ -208,8 +202,8 @@ describe("AlertingHarness.sol", () => { 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].vaultRecord.report.totalValue).to.equal(ether("100")); - expect(batch[0].vaultRecord.report.inOutDelta).to.equal(ether("1")); // connected vault has 1 ETH deposit + 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 }); }); @@ -227,11 +221,11 @@ describe("AlertingHarness.sol", () => { 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].vaultQuarantineInfo.isActive).to.equal(true); - expect(batch[0].vaultQuarantineInfo.pendingTotalValueIncrease).to.equal(0n); - expect(batch[0].vaultQuarantineInfo.startTimestamp).to.equal(0n); - expect(batch[0].vaultQuarantineInfo.endTimestamp).to.equal(0n); - expect(batch[0].vaultQuarantineInfo.totalValueRemainder).to.equal(0n); + 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); }); }); @@ -247,7 +241,7 @@ describe("AlertingHarness.sol", () => { 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].vaultPendingActivationsCount).to.equal(0n); + expect(batch[0].pendingActivationsCount).to.equal(0n); }); }); @@ -264,13 +258,13 @@ describe("AlertingHarness.sol", () => { expect(batch).to.have.length(1); expect(batch[0].vault).to.equal(await vault.getAddress()); - expect(batch[0].stakingVaultData.stakingVaultNodeOperator).to.equal(await operator.getAddress()); - expect(batch[0].stakingVaultData.stakingVaultDepositor).to.equal(await locator.predepositGuarantee()); - expect(batch[0].stakingVaultData.stakingVaultOwner).to.equal(await locator.vaultHub()); - expect(batch[0].stakingVaultData.stakingVaultPendingOwner).to.equal(ZeroAddress); - expect(batch[0].stakingVaultData.stakingVaultStagedBalance).to.equal(0n); - expect(batch[0].stakingVaultData.stakingVaultAvailableBalance).to.equal(ether("1")); - expect(batch[0].stakingVaultData.stakingVaultBeaconChainDepositsPaused).to.equal(false); + 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); }); }); }); From e0d196c46ddba4c32a66a95ac2ee544ecd1123e4 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 24 Nov 2025 15:07:33 +0000 Subject: [PATCH 5/6] feat: deploy new version --- deployed-hoodi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 292815d46e..e1f9827e0f 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -37,7 +37,7 @@ "alertingHarness": { "implementation": { "contract": "contracts/harness/AlertingHarness.sol", - "address": "0xA5707977Efa65521de9ca7fed133a70561b51234", + "address": "0xc12ae7e57c927870939030De93487D06E8Ab69ce", "constructorArgs": ["0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] } }, From c9fd07f3fbc3aaa7878c509a8e15d8cd6d376640 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Wed, 3 Dec 2025 14:59:45 +0000 Subject: [PATCH 6/6] feat: deployed AlertingHarness contract for Lido V3 --- deployed-mainnet.json | 7 +++++++ ...ploy-alerting-harness.sh => deploy-alerting-harness.sh} | 3 ++- scripts/harness/steps/0000-check-env.ts | 4 ++++ scripts/harness/steps/0100-deploy-alerting-harness.ts | 6 +++--- 4 files changed, 16 insertions(+), 4 deletions(-) rename scripts/{dao-deploy-alerting-harness.sh => deploy-alerting-harness.sh} (92%) 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/scripts/dao-deploy-alerting-harness.sh b/scripts/deploy-alerting-harness.sh similarity index 92% rename from scripts/dao-deploy-alerting-harness.sh rename to scripts/deploy-alerting-harness.sh index 6b6ae99fa5..00124d53be 100755 --- a/scripts/dao-deploy-alerting-harness.sh +++ b/scripts/deploy-alerting-harness.sh @@ -7,7 +7,8 @@ export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value s export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} # first acc of default mnemonic "test test ..." export GAS_PRIORITY_FEE=1 -export GAS_MAX_FEE=100 +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 diff --git a/scripts/harness/steps/0000-check-env.ts b/scripts/harness/steps/0000-check-env.ts index c5159f9a3a..dac0c4ec0f 100644 --- a/scripts/harness/steps/0000-check-env.ts +++ b/scripts/harness/steps/0000-check-env.ts @@ -20,6 +20,10 @@ export async function main() { 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"); } diff --git a/scripts/harness/steps/0100-deploy-alerting-harness.ts b/scripts/harness/steps/0100-deploy-alerting-harness.ts index cafc789b85..a88083d676 100644 --- a/scripts/harness/steps/0100-deploy-alerting-harness.ts +++ b/scripts/harness/steps/0100-deploy-alerting-harness.ts @@ -17,7 +17,7 @@ export async function main(): Promise { // // New AlertingHarness deployment // - await deployImplementation(Sk.alertingHarness, "AlertingHarness", deployer, [locatorAddress]); - const newAlertingHarnessAddress = state[Sk.alertingHarness].implementation.address; - console.log("New AlertingHarness address", newAlertingHarnessAddress); + const alertingHarness = await deployImplementation(Sk.alertingHarness, "AlertingHarness", deployer, [locatorAddress]); + const alertingHarnessAddress = await alertingHarness.getAddress(); + console.log("AlertingHarness address", alertingHarnessAddress); }