Skip to content
Lattice

LidoAdapterLib

Git Source

Title: LidoAdapterLib

Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from Lido (https://github.com/lidofinance/core)

Logic for a Lido staking strategy under the buffer model. The asset is WETH; on deploy idle WETH is unwrapped to ETH, staked via Lido.submit, and the resulting stETH wrapped into wstETH (held). The synchronous IStrategy.withdraw is served only from the idle WETH buffer and is shortfall-honest. The slow Lido-queue exit runs out-of-band via requestWithdrawal (wstETH → stETH → enqueue) and claimWithdrawal (finalized request → ETH → re-wrap into the buffer). Reentrancy-gated, pause/emergency-aware.

De-peg risk (documented, not corrected): totalAssetsManaged values stETH 1:1 with ETH/WETH via Lido's getStETHByWstETH. A stETH/ETH secondary-market de-peg is not oracle-corrected here; a future oracle hook (haircut on de-peg) is a follow-up.

Async limitation: because Lido withdrawals are a queue, the adapter cannot instantly fully exit — emergencyWithdraw drains the buffer now and enqueues the rest, which completes only once the request finalizes and claimWithdrawal is run.

Functions

lidoAdapterStorage

function lidoAdapterStorage() internal pure returns (LidoAdapterStorage storage $);

Returns

NameTypeDescription
$LidoAdapterStorage

__LidoAdapter_init

function __LidoAdapter_init(
        address weth_,
        address lido_,
        address wstETH_,
        address withdrawalQueue_,
        address vault_,
        address recipient_
    ) internal;

Parameters

NameTypeDescription
weth_address
lido_address
wstETH_address
withdrawalQueue_address
vault_address
recipient_address

registerInterface

function registerInterface() internal;

asset

function asset() internal view returns (address);

Returns

NameTypeDescription
<none>address

weth

function weth() internal view returns (address);

Returns

NameTypeDescription
<none>address

lido

function lido() internal view returns (address);

Returns

NameTypeDescription
<none>address

wstETH

function wstETH() internal view returns (address);

Returns

NameTypeDescription
<none>address

withdrawalQueue

function withdrawalQueue() internal view returns (address);

Returns

NameTypeDescription
<none>address

vault

function vault() internal view returns (address);

Returns

NameTypeDescription
<none>address

rewardRecipient

function rewardRecipient() internal view returns (address);

Returns

NameTypeDescription
<none>address

operator

function operator() internal view returns (address);

Returns

NameTypeDescription
<none>address

_wethUnwrapper

Reverts ProtocolAdapterUnauthorized unless the caller is the wired operator. Placed at the very top of deploy/withdraw/harvest — BEFORE the reentrancy guard — so an unauthorized call never leaves the guard latched. Zero operator ⇒ always reverts.

Returns the per-diamond WETHUnwrapper, CREATE2-deploying it on first use. The address is derived from address(this) (the diamond, under delegatecall) + the fixed salt, so every diamond lazily gets exactly one unwrapper and re-derivation is a pure computation.

function _wethUnwrapper() private returns (address unwrapper_);

Returns

NameTypeDescription
unwrapper_address

_checkOperator

function _checkOperator() private view;

bufferBalance

function bufferBalance() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

stakedWstETH

function stakedWstETH() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

pendingWithdrawalAssets

function pendingWithdrawalAssets() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

pendingRequestCount

function pendingRequestCount() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

pendingRequestAt

function pendingRequestAt(uint256 index) internal view returns (uint256);

Parameters

NameTypeDescription
indexuint256

Returns

NameTypeDescription
<none>uint256

minHealthFactor

function minHealthFactor() internal pure returns (uint256);

Returns

NameTypeDescription
<none>uint256

healthFactor

function healthFactor() internal pure returns (uint256);

Returns

NameTypeDescription
<none>uint256

isPaused

function isPaused() internal view returns (bool);

Returns

NameTypeDescription
<none>bool

totalAssetsManaged

NAV in WETH/ETH units = idle WETH buffer + wstETH valued in stETH + pending in-queue stETH.

stETH is valued 1:1 with ETH/WETH using Lido's PROTOCOL-REDEMPTION rate (getStETHByWstETH), a monotone, protocol-controlled exchange rate — NOT a secondary-market price. The pending term keeps funds-in-flight through the async withdrawal queue from disappearing from accounting between requestWithdrawal and claimWithdrawal.

KNOWN LIMITATION — stETH/ETH de-peg (not corrected here; this NAV is intentionally market-price-blind). During a stress event stETH can trade at a discount to ETH while this function still values the staked + queued legs at par. Consequence: a holder who redeems against the synchronous WETH buffer is paid at the (inflated) par NAV, while the staked leg must exit slowly through the Lido queue at the eventual realized (discounted) value — the difference is socialized onto the remaining shareholders. This is a valuation-vs-realizable mismatch, not an on-chain-manipulable bug (the redemption rate is monotone and not attacker- controlled). MITIGATION until corrected: bound exposure by capping this strategy's vault allocation target. A standing WETH buffer is not a lever: rebalance() calls deploy() and re-stakes the whole buffer each time (#221), after first recalling from it when the strategy is over target. A synchronous recall therefore pays out only what a claimed queue exit put into the buffer before that rebalance; otherwise it is a partial recall until a claim lands. FUTURE FIX: apply a haircut to stakedValue + _pendingAssets from a Chainlink stETH/ETH feed (via the existing ChainlinkAdapter) whenever stETH is below peg.

function totalAssetsManaged() internal view returns (uint256);

Returns

NameTypeDescription
<none>uint256

setRewardRecipient

function setRewardRecipient(address recipient) internal;

Parameters

NameTypeDescription
recipientaddress

setOperator

Sets the authorized operator for deploy/withdraw/harvest (admin-only). Rejects address(0) so the trio cannot be opened to an unauthenticated default.

function setOperator(address operator_) internal;

Parameters

NameTypeDescription
operator_address

deploy

Sweeps the idle WETH buffer into Lido: WETH → ETH → stETH (submit) → wstETH (wrap).

Reverts paused/stopped and on a zero idle balance. Reentrancy-gated. Canonical WETH9 pays withdraw via msg.sender.transfer (2,300-gas stipend) — too little for the diamond's zero-selector routing to the Receive facet — so the unwrap goes through a per-diamond CREATE2 WETHUnwrapper, which absorbs the stipend send and returns the ETH with full gas (the Receive facet accepts it; no guarded state runs, so it is safe inside the nonReentrant window).

function deploy() internal returns (uint256 deployed);

Returns

NameTypeDescription
deployeduint256

withdraw

Synchronous IStrategy.withdraw — served only from the idle WETH buffer.

Shortfall-honest by design. actual = min(amount, bufferBalance); when the buffer is short it transfers what it has and reports that — the staked leg must be liberated out-of-band via requestWithdrawal/claimWithdrawal. The buffer is idle, so this already spends idle first. The StrategyManager accepts the under-delivery as a partial recall (the staked and queued legs stay in this adapter's NAV) and recalls the rest on a later rebalance, once claims refill the buffer. The staked wstETH position is intentionally never touched here.

function withdraw(uint256 amount, address to) internal returns (uint256 withdrawn);

Parameters

NameTypeDescription
amountuint256
toaddress

Returns

NameTypeDescription
withdrawnuint256

requestWithdrawal

Unwraps wstAmount wstETH to stETH and enqueues it in the Lido withdrawal queue (admin/keeper). Records the request id + stETH amount as pending so NAV is unchanged by the move (staked value falls, pending rises by the same stETH amount).

function requestWithdrawal(uint256 wstAmount) internal returns (uint256 requestId);

Parameters

NameTypeDescription
wstAmountuint256

Returns

NameTypeDescription
requestIduint256

claimWithdrawal

Claims a finalized withdrawal request (permissionless/keeper): pulls the owed ETH from the queue (a full-gas payout that lands via the diamond's Receive facet), wraps it 1:1 into the WETH buffer, and clears the request from pending.

function claimWithdrawal(uint256 requestId) internal returns (uint256 ethReceived);

Parameters

NameTypeDescription
requestIduint256

Returns

NameTypeDescription
ethReceiveduint256

harvest

No-op: Lido yield accrues in the wstETH→stETH exchange rate (reflected in NAV), NOT a claimable reward token, so there is nothing to claim or forward on the standard IProtocolAdapter.harvest() path. Kept as a graceful no-op (for an authorized operator) so a keeper sweep never reverts. Use harvestToken to forward a specific stray (airdropped) token. Operator-gated for parity with the other adapters' trio.

function harvest() internal view;

harvestToken

Forwards the adapter's entire balance of a stray token raw to the reward recipient (admin/keeper). The three core tokens (WETH buffer, stETH hop, wstETH stake) are rejected so a sweep can never drain the position or the buffer.

function harvestToken(address token) internal;

Parameters

NameTypeDescription
tokenaddress

emergencyWithdraw

Emergency exit (admin, runs even when stopped): sends the entire WETH buffer to the vault now AND enqueues the full wstETH stake in the Lido withdrawal queue.

Async limitation: the queue leg cannot complete synchronously — the staked ETH returns only once the request finalizes and claimWithdrawal is run, after which a final withdraw sweeps it to the vault. recovered is therefore only the buffer paid out immediately; the enqueued stake remains in NAV as pendingWithdrawalAssets.

function emergencyWithdraw() internal returns (uint256 recovered);

Returns

NameTypeDescription
recovereduint256

_enqueue

Unwraps wstAmount wstETH to stETH, approves the queue, requests a single-entry withdrawal owned by this adapter, and records the request id + stETH amount as pending. Caller is responsible for the reentrancy guard and any balance pre-checks.

function _enqueue(LidoAdapterStorage storage $, uint256 wstAmount) private returns (uint256 requestId);

Parameters

NameTypeDescription
$LidoAdapterStorage
wstAmountuint256

Returns

NameTypeDescription
requestIduint256