LidoAdapterLib
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
| Name | Type | Description |
|---|---|---|
| $ | LidoAdapterStorage |
__LidoAdapter_init
function __LidoAdapter_init(
address weth_,
address lido_,
address wstETH_,
address withdrawalQueue_,
address vault_,
address recipient_
) internal;Parameters
| Name | Type | Description |
|---|---|---|
| weth_ | address | |
| lido_ | address | |
| wstETH_ | address | |
| withdrawalQueue_ | address | |
| vault_ | address | |
| recipient_ | address |
registerInterface
function registerInterface() internal;asset
function asset() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
weth
function weth() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
lido
function lido() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
wstETH
function wstETH() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
withdrawalQueue
function withdrawalQueue() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
vault
function vault() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
rewardRecipient
function rewardRecipient() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
operator
function operator() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| unwrapper_ | address |
_checkOperator
function _checkOperator() private view;bufferBalance
function bufferBalance() internal view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
stakedWstETH
function stakedWstETH() internal view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
pendingWithdrawalAssets
function pendingWithdrawalAssets() internal view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
pendingRequestCount
function pendingRequestCount() internal view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
pendingRequestAt
function pendingRequestAt(uint256 index) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| index | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
minHealthFactor
function minHealthFactor() internal pure returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
healthFactor
function healthFactor() internal pure returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
isPaused
function isPaused() internal view returns (bool);Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
setRewardRecipient
function setRewardRecipient(address recipient) internal;Parameters
| Name | Type | Description |
|---|---|---|
| recipient | address |
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
| Name | Type | Description |
|---|---|---|
| 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
| Name | Type | Description |
|---|---|---|
| deployed | uint256 |
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
| Name | Type | Description |
|---|---|---|
| amount | uint256 | |
| to | address |
Returns
| Name | Type | Description |
|---|---|---|
| withdrawn | uint256 |
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
| Name | Type | Description |
|---|---|---|
| wstAmount | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| requestId | uint256 |
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
| Name | Type | Description |
|---|---|---|
| requestId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| ethReceived | uint256 |
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
| Name | Type | Description |
|---|---|---|
| token | address |
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
| Name | Type | Description |
|---|---|---|
| recovered | uint256 |
_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
| Name | Type | Description |
|---|---|---|
| $ | LidoAdapterStorage | |
| wstAmount | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| requestId | uint256 |