Skip to main content

LazyOracle

Oracle adapter for stVaults. Stores per-vault reports, applies sanity checks, and forwards vault updates to VaultHub.

What is LazyOracle?​

LazyOracle is a lightweight oracle for stVaults:

  • stores the latest report metadata (timestamp, ref slot, tree root, CID)
  • validates vault proofs against a report tree root
  • applies per-vault accounting updates to VaultHub
  • quarantines vaults with suspicious value deltas

It is called lazy because it stores only the report root and metadata each round; per-vault data is expanded on-demand via Merkle proofs only when a vault operation needs it.

How it works​

  1. AccountingOracle publishes a report root and metadata via updateReportData().
  2. Anyone can submit per-vault updates with Merkle proofs via updateVaultData().
  3. LazyOracle validates proofs and checks reward/fee bounds.
  4. Sanity-checked data is forwarded to VaultHub via applyVaultReport().

Per-vault report submissions are permissionless: any account can call updateVaultData with a valid Merkle proof from the latest report root.

Report freshness​

A vault report freshness is determined by VaultHub based on the report timestamp stored in LazyOracle. When stale, the vault cannot perform operations like withdrawals, mints, beacon chain deposits or disconnect.

Quarantine mechanics​

LazyOracle applies a quarantine buffer for sudden total value jumps that cannot be verified immediately via inOutDelta. Value increases beyond the expected reward threshold are quarantined for a configurable period before being released to VaultHub.

Time 0: Total Value = 100 ETH
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 100 ETH Active β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Time 1: Sudden jump of +50 ETH β†’ start quarantine for 50 ETH
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 100 ETH Active β”‚
β”‚ 50 ETH Quarantined β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Time 2: Another jump of +70 ETH β†’ wait for current quarantine to expire
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 100 ETH Active β”‚
β”‚ 50 ETH Quarantined β”‚
β”‚ 70 ETH Quarantine Queue β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Time 3: First quarantine expires β†’ add 50 ETH to active value, start new quarantine for 70 ETH
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 150 ETH Active β”‚
β”‚ 70 ETH Quarantined β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Time 4: Second quarantine expires β†’ add 70 ETH to active value
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 220 ETH Active β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quarantine state machine​

States:
β€’ NO_QUARANTINE: No active quarantine, all value is immediately available
β€’ QUARANTINE_ACTIVE: Total value increase is quarantined, waiting for expiration
β€’ QUARANTINE_EXPIRED: Quarantine period passed, quarantined value can be released

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ NO_QUARANTINE β”‚ reported > threshold β”‚QUARANTINE_ACTIVE β”‚
β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ β”‚
β”‚ quarantined=0 β”‚ β”‚ quarantined>0 β”‚
β”‚ startTime=0 │◄────────────────────────────── startTime>0 β”‚
β”‚ | β”‚ time<expiration |
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ reported ≀ threshold β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–² (early release) β”‚ β–²
β”‚ β”‚ β”‚ increase > quarantined + rewards
β”‚ time β‰₯ β”‚ β”‚ (release old, start new)
β”‚ quarantine period β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ reported ≀ threshold OR β”‚ QUARANTINE_EXPIRED β”‚
β”‚ increase ≀ quarantined + rewards β”‚ β”‚
β”‚ β”‚ quarantined>0 β”‚
β”‚ β”‚ startTime>0 β”‚
└─────────────────────────────────────── time>=expiration β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Legend:
β€’ threshold = onchainTotalValue * (100% + maxRewardRatio)
β€’ increase = reportedTotalValue - onchainTotalValue
β€’ quarantined = total value increase that is currently quarantined
β€’ rewards = expected EL/CL rewards based on maxRewardRatio
β€’ expiration = quarantine.startTimestamp + quarantinePeriod

Normal top-ups via fund() do not go through quarantine since they can be verified on-chain via inOutDelta. Only consolidations or deposits that bypass the vault's balance are quarantined.

Constants​

ConstantValueDescription
MAX_QUARANTINE_PERIOD30 daysMaximum allowed quarantine period
MAX_REWARD_RATIO65535 (type(uint16).max)Maximum reward ratio (~655%)
MAX_LIDO_FEE_RATE_PER_SECOND10 etherMaximum Lido fee rate per second

Structs​

QuarantineInfo​

struct QuarantineInfo {
bool isActive; // Whether quarantine is active
uint256 pendingTotalValueIncrease; // Amount quarantined
uint256 startTimestamp; // When quarantine started
uint256 endTimestamp; // When quarantine expires
uint256 totalValueRemainder; // Additional value waiting in queue
}

VaultInfo​

Aggregated vault information returned by view methods:

struct VaultInfo {
address vault; // Vault address
uint256 aggregatedBalance; // availableBalance + stagedBalance
int256 inOutDelta; // Current in/out delta
bytes32 withdrawalCredentials; // Vault withdrawal credentials
uint256 liabilityShares; // Current liability shares
uint256 maxLiabilityShares; // Maximum liability shares
uint256 mintableStETH; // Remaining mintable stETH
uint96 shareLimit; // Share limit from connection
uint16 reserveRatioBP; // Reserve ratio in basis points
uint16 forcedRebalanceThresholdBP; // Forced rebalance threshold
uint16 infraFeeBP; // Infrastructure fee
uint16 liquidityFeeBP; // Liquidity fee
uint16 reservationFeeBP; // Reservation fee
bool pendingDisconnect; // Whether vault is pending disconnect
}

View methods​

latestReportData()​

function latestReportData() external view returns (
uint256 timestamp,
uint256 refSlot,
bytes32 treeRoot,
string memory reportCid
)

Returns latest report metadata.

latestReportTimestamp()​

function latestReportTimestamp() external view returns (uint256)

Returns latest report timestamp.

quarantinePeriod()​

function quarantinePeriod() external view returns (uint256)

Returns quarantine period duration in seconds.

maxRewardRatioBP()​

function maxRewardRatioBP() external view returns (uint256)

Returns max reward ratio in basis points. Used to determine quarantine threshold.

maxLidoFeeRatePerSecond()​

function maxLidoFeeRatePerSecond() external view returns (uint256)

Returns max Lido fee rate per second in wei.

quarantineValue(address _vault)​

function quarantineValue(address _vault) external view returns (uint256)

Returns total value pending in quarantine for a vault (includes both pendingTotalValueIncrease and totalValueRemainder).

vaultQuarantine(address _vault)​

function vaultQuarantine(address _vault) external view returns (QuarantineInfo memory)

Returns detailed quarantine info for a vault. Returns zeroed struct if no active quarantine.

vaultsCount()​

function vaultsCount() external view returns (uint256)

Returns number of vaults connected to VaultHub.

batchVaultsInfo(uint256 _offset, uint256 _limit)​

function batchVaultsInfo(uint256 _offset, uint256 _limit) external view returns (VaultInfo[] memory)

Returns vault info for a range of vaults. Offset is 0-indexed from VaultHub vault list.

vaultInfo(address _vault)​

function vaultInfo(address _vault) external view returns (VaultInfo memory)

Returns aggregated info for a specific vault.

batchValidatorStatuses(bytes[] _pubkeys)​

function batchValidatorStatuses(bytes[] calldata _pubkeys)
external
view
returns (IPredepositGuarantee.ValidatorStatus[] memory batch)

Returns validator statuses from PredepositGuarantee for multiple pubkeys.

Methods​

initialize(address _admin, uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond)​

function initialize(
address _admin,
uint256 _quarantinePeriod,
uint256 _maxRewardRatioBP,
uint256 _maxLidoFeeRatePerSecond
) external initializer

Initializes LazyOracle with admin and sanity parameters.

updateSanityParams(...)​

function updateSanityParams(
uint256 _quarantinePeriod,
uint256 _maxRewardRatioBP,
uint256 _maxLidoFeeRatePerSecond
) external

Updates sanity bounds. Requires UPDATE_SANITY_PARAMS_ROLE.

updateReportData(...)​

function updateReportData(
uint256 _vaultsDataTimestamp,
uint256 _vaultsDataRefSlot,
bytes32 _vaultsDataTreeRoot,
string memory _vaultsDataReportCid
) external

Publishes the report root and metadata. Only callable by AccountingOracle.

updateVaultData(...)​

function updateVaultData(
address _vault,
uint256 _totalValue,
uint256 _cumulativeLidoFees,
uint256 _liabilityShares,
uint256 _maxLiabilityShares,
uint256 _slashingReserve,
bytes32[] calldata _proof
) external

Applies a per-vault update with a Merkle proof. Permissionless - anyone can call with valid proof.

Sanity checks performed:

  1. Report must be newer than vault's previous report
  2. Total value increase is quarantined if above reward threshold
  3. Dynamic total value calculation must not underflow
  4. Cumulative Lido fees must be monotonically increasing
  5. Cumulative Lido fees increase must not exceed max rate
  6. maxLiabilityShares must be >= liabilityShares and <= on-chain value

removeVaultQuarantine(address _vault)​

function removeVaultQuarantine(address _vault) external

Removes quarantine for a vault. Only callable by VaultHub.

Permissions​

RoleDescription
DEFAULT_ADMIN_ROLEAdmin role for granting/revoking other roles
UPDATE_SANITY_PARAMS_ROLECan update sanity parameters (quarantine period, reward ratio, fee rate)