Skip to content
Open
310 changes: 310 additions & 0 deletions contracts/harness/AlertingHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// 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());
}
}
7 changes: 7 additions & 0 deletions deployed-hoodi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions deployed-mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
]
}
},
"alertingHarness": {
"implementation": {
"contract": "contracts/harness/AlertingHarness.sol",
"address": "0xe3f9D755b3240AF7C988B05588A38461D40Bd558",
"constructorArgs": ["0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"]
}
},
"apmRegistryFactoryAddress": "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A",
"app:aragon-agent": {
"implementation": {
Expand Down
4 changes: 3 additions & 1 deletion lib/state-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions scripts/deploy-alerting-harness.sh
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions scripts/harness/steps-deploy-alerting-harness.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"steps": ["harness/steps/0000-check-env", "harness/steps/0100-deploy-alerting-harness"]
}
Loading
Loading