stETH/wstETH integration guide
This document is intended for developers looking to integrate Lido's stETH or wstETH as a token into their dApp, with a focus on money markets, DEXes and blockchain bridges.
Lidoβ
Lido is a family of liquid staking protocols across multiple blockchains, with headquarters on Ethereum. Liquid refers to the ability for a userβs stake to become liquid. Upon user's deposit Lido issues stToken, which represents the deposited tokens along with all the rewards & penalties accrued through deposit's staking. Unlike the staked funds, this stToken is liquid β it can be freely transferred between parties, making it usable across different DeFi applications while still receiving daily staked rewards. It is paramount to preserve this property when integrating stTokens into any DeFi protocol.
This guide refers to Lido on Ethereum (hereinafter referred to as Lido). For ether staked in Lido, it gives users stETH that is equal to the amount staked.
Lido's stTokens are widely adopted across Ethereum ecosystem:
- The most important liquidity venues include stETH/ETH liquidity pool on Curve and wstETH/ETH MetaStable pool on Balancer v2
- stETH is listed as collateral token on AAVE v2 market on Ethereum mainnet
- wstETH is listed as collateral token on Maker
- steCRV (the Curve stETH/ETH LP token) is listed as collateral token on Maker
- there are multiple liquidity strategies built on top of Lido's stTokens, including yearn and Harvest Finance
Integration utilities: ChainLink price feedsβ
- There are live ChainLink stETH/USD and stETH/ETH price feeds on Ethereum.
- There are live ChainLink stETH/USD price feeds on Arbitrum and Optimism. These also have ChainLink wstETH-stETH exchange rate data feeds.
stETH vs. wstETHβ
There are two versions of Lido's stTokens, namely stETH and wstETH. Both are fungible tokens, but they reflect the accrued staking rewards in different ways. stETH implements rebasing mechanics which means the stETH balance increases periodically. In contrary, wstETH balance is constant, while the token increases in value eventually (denominated in stETH). At any moment, any amount of stETH can be converted to wstETH via trustless wrapper and vice versa, thus tokens effectively share liquidity. For instance, undercollateralized wstETH positions on Maker can be liquidated by unwrapping wstETH and swapping it for ether on Curve.
stETHβ
What is stETHβ
stETH is a rebaseable ERC-20 token that represents ether staked with Lido. Unlike staked ether, it is liquid and can be transferred, traded, or used in DeFi applications. Total supply of stETH reflects amount of ether deposited into protocol combined with staking rewards, minus potential validator penalties. stETH tokens are minted upon ether deposit at 1:1 ratio. Since withdrawals from the Beacon chain have been introduced, it is also possible to redeem ether by burning stETH at the same 1:1 ratio (in rare cases it won't preserve 1:1 ratio though).
stETH does not strictly comply with ERC-20. Only exception is that it does not emit Transfer()
on rebase as ERC-20 standard recommends.
Please note, Lido has implemented staking rate limits aimed at reducing the post-Merge staking surge's impact on the staking queue & Lidoβs socialized rewards distribution model. Read more about it here.
stETH is a rebasable ERC-20 token. Normally, the stETH token balances get recalculated daily when the Lido oracle reports Beacon chain ether balance update. The stETH balance update happens automatically on all the addresses holding stETH at the moment of rebase. The rebase mechanics have been implemented via shares (see shares).
Accounting oracleβ
Normally, stETH rebases happen daily when the Lido oracle reports the Beacon chain ether balance update. The rebase can be positive or negative, depending on the validators' performance. In case Lido's validators get slashed or penalized, the stETH balances can decrease according to penalty sizes. However, daily rebases have never been negative by the time of writing.
The accounting oracle has sanity checks on both max APR reported (the APR cannot exceed 27%, which means a daily rebase is limited to 27/365%
) and total staked amount drop (staked ether decrease reported cannot exceed 5%). Currently, the oracle report is based on five oracle daemons hosted by established node operators selected by the DAO. As soon as five out of nine oracle daemons report the same data, reaching the consensus, the report goes to the Lido smart contract, and the rebase occurs. There is a dedicated oracle dashboard to monitor current accounting reports.
Oracle corner casesβ
- In case oracle daemons do not report Beacon chain balance update or do not reach quorum, the oracle does not submit the daily report, and the daily rebase doesn't occur until the quorum is reached.
- In case the quorum hasn't been reached, the oracle can skip the daily report. The report will happen as soon as the quorum for one of the next periods will be reached, and it will include the balance update for all the period since last oracle report.
- Oracle daemons only report the finalized epochs. In case of no finality on the Beacon chain, the daemons won't submit their reports, and the daily rebase won't occur.
- In case sanity checks on max APR or total staked amount drop fail, the oracle report cannot be finalized, and the rebase cannot happen.
stETH internals: share mechanicsβ
Daily rebases result in stETH token balances changing. This mechanism is implemented via shares.
The share
is a basic unit representing the stETH holder's share in the total amount of ether controlled by the protocol. When a new deposit happens, the new shares get minted to reflect what share of the protocol controlled ether has been added to the pool. When the Beacon chain oracle report comes in, the price of 1 share in stETH is being recalculated. Shares aren't normalized, so the contract also stores the sum of all shares to calculate each account's token balance.
Shares balance by stETH balance can be calculated by this formula:
shares[account] = balanceOf(account) * totalShares / totalPooledEther
1-2 wei corner caseβ
stETH balance calculation includes integer division, and there is a common case when the whole stETH balance can't be transferred from the account, while leaving the last 1-2 wei on the sender's account. Same thing can actually happen at any transfer or deposit transaction. In the future, when the stETH/share rate will be greater, the error can become a bit bigger. To avoid it, one can use transferShares
to be precise.
Example:
- User A transfers 1 stETH to User B.
- Under the hood, stETH balance gets converted to shares, integer division happens and rounding down applies.
- Corresponding amount of shares gets transferred from User A to User B.
- Shares balance gets converted to stETH balance for User B.
- In many cases the actually transferred amount is 1-2 wei less than expected.
The issue is documented here: https://github.com/lidofinance/lido-dao/issues/442
Bookkeeping sharesβ
Although user friendly, stETH rebases add a whole level of complexity to integrating stETH into other dApps and protocols. When integrating stETH as a token into any dApp, it's highly recommended to store and operate shares rather than stETH public balances directly, because stETH balances change both upon transfers, mints/burns, and rebases, while shares balances can only change upon transfers and mints/burns.
To figure out the shares balance, getSharesByPooledEth(uint256)
function can be used. It returns the value not affected by future rebases and it can be converted back into stETH by calling getPooledEthByShares
function.
See all available stETH methods here.
Any operation on stETH can be performed on shares directly, with no difference between share and stETH.
The preferred way of operating stETH should be:
1) get stETH token balance; 2) convert stETH balance into shares balance and use it as primary balance unit in your dapp; 3) when any operation on the balance should be done, do it on shares balance; 4) when users interact with stETH, convert shares balance back to stETH token balance.
Please note that 10% APR on shares balance and 10% APR on stETH token balance will ultimately result in different output values over time, because shares balance is stable, while stETH token balance changes eventually.
If using the rebasable stETH token is not an option for your integration, it is recommended to use wstETH instead of stETH. See how it works here.
Transfer shares function for stETHβ
The LIP-11 introduced the transferShares
function which allows to transfer stETH in a "rebase-agnostic" manner: transfer in terms of shares amount.
Normally, we transfer stETH using ERC-20 transfer
and transferFrom
functions which accept as input amount of stETH, not the amount of the underlying shares.
Sometimes we'd better operate with shares directly to avoid possible rounding issues. Rounding issues usually could appear after a token rebase.
This feature is aimed to provide an additional level of precision when operating with stETH.
Read more abut the function in the LIP-11.
Also, V2 upgrade introduced a transferSharesFrom
to completely match ERC-20 set of transfer methods.
Feesβ
Lido collects a percentage of the staking rewards as a protocol fee. The exact fee size is defined by the DAO and can be changed in the future via DAO voting. To collect the fee, the protocol mints new stETH token shares and assigns them to the fee recipients. Currenty, the fee collected by Lido protocol is 10% of staking rewards with half of it going to the node operators and the other half going to the protocol treasury.
Since total amount of Lido pooled ether tends to increase, the combined value of all holders' shares denominated in stETH increases respectively. Thus, the rewards effectively spread between each token holder proportionally to their share in the protocol TVL. So Lido mints new shares to the fee recipient, so that the total cost of the newly-minted shares exactly corresponds to the fee taken (calculated in basis points):
shares2mint * newShareCost = (_totalRewards * feeBasis) / 10000
newShareCost = newTotalPooledEther / (prevTotalShares + shares2mint)
which follows to:
_totalRewards * feeBasis * prevTotalShares
shares2mint = --------------------------------------------------------------
(newTotalPooledEther * 10000) - (feeBasis * _totalRewards)
How to get APR?β
Please refer to this page for correct Lido V2 APR calculation.
It is worth noting that with withdrawals enabled, the APR calculation method for Lido has changed significantly. When Lido V2 protocol finalizes withdrawal requests, the Lido contract excludes funds from TVL and assigns to burn underlying locked requestsβ stETH shares in return. In other words, withdrawal finalization decreases both TVL and total shares. Old V1 formula isnβt suitable anymore because it catches TVL changes, but skips total shares changes.
Do stETH rewards compound?β
Yes, stETH rewards do compound.
All rewards that are withdrawn from the Beacon chain or received as MEV or EL priority fees (that aren't used to fulfil withdrawal requests) are finally restaked to set up new validators and receive more rewards at the end. So, we can say that stETH beccomes fully auto-compounding after V2 release.
wstETHβ
Due to the rebasing nature of stETH, the stETH balance on holder's address is not constant, it changes daily as oracle report comes in. Although rebasable tokens are becoming a common thing in DeFi recently, many dApps do not support rebasing. For example, Maker, UniSwap, and SushiSwap are not designed for rebasable tokens. Listing stETH on these apps can result in holders not receiving their daily staking rewards which effectively defeats the benefits of liquid staking. To integrate with such dApps, there's another form of Lido stTokens called wstETH (wrapped staked ether).
What is wstETHβ
wstETH is an ERC20 token that represents the account's share of the stETH total supply (stETH token wrapper with static balances). For wstETH, 1 wei in shares equals to 1 wei in balance. The wstETH balance can only be changed upon transfers, minting, and burning. wstETH balance does not rebase, wstETH's price denominated in stETH changes instead.
At any given time, anyone holding wstETH can convert any amount of it to stETH at a fixed rate, and vice versa. The rate is the same for everyone at any given moment. Normally, the rate gets updated once a day, when stETH undergoes a rebase. The current rate can be obtained by calling wstETH.stEthPerToken()
Wrap & Unwrapβ
When wrapping stETH to wstETH, the desired amount of stETH is being locked on the WstETH contract balance, and the wstETH is being minted according to the shares bookeeping formula.
When unwrapping, wstETH gets burnt and the corresponding amount of stETH gets unlocked.
Thus, amount of stETH unlocked when unwrapping is different from what has been initially wrapped (given a rebase happened between wrapping and unwrapping stETH).
wstETH shortcutβ
Note, that WstETH contract includes a shortcut to convert ether to wstETH under the hood, which allows to effectively skip the wrapping step and stake ether for wstETH directly. Keep in mind that when using the shortcut, the staking rate limits still apply.
Rewards accountingβ
Since wstETH represents the holder's share in the total amount of Lido-controlled ether, rebases don't affect wstETH balances, but change the wstETH price denominated in stETH.
Basic example:
1) User wraps 1 stETH and gets 0.9803 wstETH (1 stETH = 0.9803 wstETH) 2) A rebase happens, the wstETH price goes up by 5% 3) User unwraps 0.9803 wstETH and gets 1.0499 stETH (1 stETH = 0.9337 wstETH)
Goerli wstETH for testingβ
The most recent testnet version of the Lido protocol lives on Goerli testnet (see the full list of contracts deployed here). Just like on mainnet, Goerli wstETH for testing purposes can be obtained by approving the desired amount of stETH to the WstETH contract on Goerli, and then calling wrap
method on it. The corresponding amount of Goerli stETH will be locked on the WstETH contract, and the wstETH tokens will be minted to your account. Goerli Ether can also be converted to wstETH directly using the wstETH shortcut β just send your Goerli Ether to WstETH contract on Goerli, and the corresponding amount of wstETH will be minted to your account.
wstETH on L2sβ
Currently, wstETH token is present on Arbitrum and Optimism with bridging implemented via the canonical bridges. Unlike on Ethereum mainnet, wstETH on L2s is a plain ERC20 token and cannot be unwrapped to unlock stETH on the corresponding L2 network. The token does not implement shares bookkeeping, which means it is not possible to calculate the wstETH/stETH rate and the rewards accrued on-chain. However, there're live Chainlink wstETH/stETH rate feeds for Arbitrum and Optimism that can and should be used for this purpose.
ERC20Permitβ
wstETH and stETH tokens implement the ERC20 Permit extension allowing approvals to be made via signatures, as defined in EIP-2612.
The permit
method allows users to modify the allowance using a signed message, instead of through msg.sender
.
By not relying on approve
method, you can build interfaces that will approve and use wstETH in one tx.
Staking rate limitsβ
In order to handle the staking surge in case of some unforeseen market conditions, the Lido protocol implemented staking rate limits aimed at reducing the surge's impact on the staking queue & Lidoβs socialized rewards distribution model.
There is a sliding window limit that is parametrized with _maxStakingLimit
and _stakeLimitIncreasePerBlock
. This means it is only possible to submit this much ether to the Lido staking contracts within a 24 hours timeframe. Currently, the daily staking limit is set at 150,000 ether.
You can picture this as a health globe from Diablo 2 with a maximum of _maxStakingLimit
and regenerating with a constant speed per block.
When you deposit ether to the protocol, the level of health is reduced by its amount and the current limit becomes smaller and smaller.
When it hits the ground, transaction gets reverted.
To avoid that, you should check if getCurrentStakeLimit() >= amountToStake
, and if it's not you can go with an alternative route.
The staking rate limits are denominated in ether, thus, it makes no difference if the stake is being deposited for stETH or using the wstETH shortcut, the limits apply in both cases.
Alternative routesβ
- Wait for staking limits to regenerate to higher values and retry depositing ether to Lido later.
- Consider swapping ETH for stETH on DEXes like Curve or Balancer. At specific market conditions stETH may effectively be purchased from there with a discount due to stETH price fluctuations.
Withdrawalsβ
V2 introduced a posibility to withdraw ETH from Lido. A high-level upgrade overview can be found in the blog post. Withdrawals flow are organized as FIFO queue that accepts the requests with stETH attached and these requests are finalized with oracle reports as soon as ether to fulfill the request are available.
So to obtain ether from the protocol, you'll need to proceed with the following steps:
- request the withdrawal, locking your steth in the queue and receiving an NFT, that represents your position in the queue
- wait, until the request is finalized by the oracle report and becomes claimable
- claim your ether, burning the NFT
Request size should be at least 100 wei (in stETH) and at most 1000 stETH. Larger amounts should be withdrawn in multiple requests, which can be batched via in-protocol API. Once requested, withdrawal cannot be canceled. The withdrawal NFT can be transferred to a different address, and the new owner will be able to claim the requested withdrawal once finalized.
The amount of claimable ETH is determined once the withdrawal request is finalized. The rate stETH/ETH of the request finalization can't get higher than it's been at the moment of request creation. The user will be able to claim:
- normally β ETH amount corresponding to the stETH amount at the moment of the request's placement
OR
- discounted - lowered ETH amount corresponding to the oracle-reported share rate in case the protocol had undergone significant losses (slashings and penalties)
The second option is unlikely, and we haven't ever seen the conditions for it on mainnet so far.
The end-user contract to deal with the withdrawals is WithdrawalQueueERC721.sol
, which implements the ERC721 standard. NFT represents the position in the withdrawal queue and may be claimed after finalization of the request.
Let's follow these steps in details:
Request withdrawal and mint NFTβ
You have several options for requesting for withdrawals, they requires you own stETH or wstETH on your address:
stETHβ
- Call
requestWithdrawalsWithPermit(uint256[] _amounts, address _owner, PermitInput _permit)
and get the ids of created positions, wheremsg.sender
will be used to transfer tokens from and the_owner
will be the address that can claim or transfer NFT (defaults tomsg.sender
if itβs not provided - Alternatively, sending stETH on behalf of
WithdrawalQueueERC721.sol
contract can be approved in a separate upfront transaction (stETH.approve(withdrawalQueueERC712.address, allowance)
), and therequestWithdrawals(uint256[] _amounts, address _owner)
method called afterwards
wstETHβ
- Call
requestWithdrawalsWstETHWithPermit(uint256[] _amounts, address _owner, PermitInput _permit)
and get the ids of created positions, wheremsg.sender
will be used to transfer tokens from, and the_owner
will be the address that can claim or transfer NFT (defaults tomsg.sender
if itβs not provide) - Alternatively, sending wstETH on behalf of
WithdrawalQueueERC721.sol
contract can be approved in a separate upfront transaction (wstETH.approve(withdrawalQueueERC712.address, allowance)
), and therequestWithdrawalsWstETH(uint256[] _amounts, address _owner)
method called afterwards
PermitInput
structure defined as follows:
struct PermitInput {
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
After request ERC721 NFT are minted to _owner
address and can be transferred to the other owner which will have all the rights to claim the withdrawal.
Additionally, this NFT implements ERC4906 standard and it's recommended to rely on
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
to update the nft metadata if you're integrating it somewhere where it should be displayed correctly.
Checking the state of withdrawalβ
- You can check all the withdrawal request's for the owner by calling
getWithdrawalRequests(address _owner)
that return an array of NFT ids. - To check the state of the particular NFTs you can call
getWithdrawalStatus(uint256[] _requestIds)
that returns an array ofWithdrawalRequestStatus
struct.
struct WithdrawalRequestStatus {
/// @notice stETH token amount that was locked on withdrawal queue for this request
uint256 amountOfStETH;
/// @notice amount of stETH shares locked on withdrawal queue for this request
uint256 amountOfShares;
/// @notice address that can claim or transfer this request
address owner;
/// @notice timestamp of when the request was created, in seconds
uint256 timestamp;
/// @notice true, if request is finalized
bool isFinalized;
/// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed)
bool isClaimed;
}
NOTE: Since stETH is an essential token if the user requests a withdrawal using wstETH directly, the amount will be nominated in stETH on request creation.
You can call getClaimableEther(uint256[] _requestIds, uint256[] _hints)
to get the exact amount of eth that is reserved for the requests, where _hints
can be found by calling findCheckpointHints(__requestIds, 1, getLastCheckpointIndex())
. It will return non-zero value only if request is claimable (isFinalized && !isClaimed)
Claimingβ
To claim ether you need to call:
claimWithdrawal(uint256 _requestId)
with the NFT Id on behalf of the NFT ownerclaimWithdrawals(uint256[] _requestIDs, uint256[] _hints)
if you want to claim multiple withdrawals in batches or optimize on hint search- hints =
findCheckpointHints(uint256[] calldata _requestIDs, 1, lastCheckpoint)
- lastCheckpoint =
getLastCheckpointIndex()
- hints =
General integration examplesβ
stETH/wstETH as collateralβ
stETH/wstETH as DeFi collateral is beneficial for a number of reasons:
- stETH/wstETH is almost as safe as ether, price-wise: barring catastrophic scenarios, its value tends to hold the ETH peg well;
- stETH/wstETH is a productive token: getting rewards on collateral effectively lowers the cost of borrowing;
- stETH/wstETH is a very liquid token with billions of liquidity locked in liquidity pools (Curve, Balancer v2)
Lido's staked tokens have been listed on major liquidity protocols:
- On Maker, wstETH collateral (scroll down to Dai from WSTETH-A section) can be used to mint DAI stablecoin. See Lido's blog post for more details.
- On AAVE, multiple tokens can be borrowed against stETH. See Lido's blog post for more details. Please note: stETH is only supported on AAVE as lending collateral. Borrowing stETH on AAVE is not currently supported. However, any asset can be borrowed on AAVe via a flashloan. Due to a known 1 wei corner case there's a certain situation when a flashloan transaction can revert. Please visit stETH on AAVE caveats article for more details.
Robust price sources are required for listing on most money markets, with ChainLink price feeds being the industry standard. There're live ChainLink stETH/USD and stETH/ETH price feeds on Ethereum.
Wallet integrationsβ
Lido's Ethereum staking services have been successfully integrated into most popular DeFi wallets, including Ledger, MyEtherWallet, ImToken and others. Having stETH integrated can provide wallet users with great user experience of direct staking from the wallet UI itself.
Lido DAO runs a referral program rewarding wallets and other apps for driving liquidity to the Lido staking protocol. At the moment, the referral program is in whitelist mode. Please contact Lido bizdev team to find out if your wallet might be eligible for referral program participation.
When adding stETH support to a DeFi wallet, it is important to preserve stETH's rebasing nature. Avoid storing cached stETH balance for extended periods of time (over 24 hours), and keep in mind it doesn't necessarily take a transaction to change stETH balance.
Liquidity miningβ
stETH liquidity is mostly concentrated in two biggest liquidity pools:
- stETH/ETH liquidity pool on Curve (contract code)
- wstETH/WETH MetaStable pool on Balancer v2 (read more)
Both pools are incentivised with Lido governance token (LDO) via direct incentives and bribes (veBAL bribes coming soon), and allow the liquidity providers to retain their exposure to getting Lido staking rewards.
- Curve pool allows providing liquidity in the form of any of the pooled tokens or in both of them. From that moment on, all the staking rewards accrued by stETH go to the pool and not to the liquidity provider's address. However, when withdrawing the liquidity, the liquidity provider will be able to get more than they have initially deposited. Please note, when depositing exclusively stETH to Curve, the tokens are split between ether and stETH, with the precise balances fluctuating constantly due to price trading. Thus, the liquidity provider will only be eligible for about a half of rewards accrued by the stETH deposited. To avoid that, provide stETH and ether liquidity in equal parts.
- Unlike Curve, Balancer pool is wstETH-based. wstETH doesn't rebase, it accrues staking rewards by eventually increasing in price instead. Thus, when withdrawing liquidity form the Balancer pool, the liquidity providers get tokens valued higher than what they have initially deposited.
Cross chain bridgingβ
The Lido's stTokens will eventually get bridged to various L2's and sidechains. Most cross chain token bridges have no mechanics to handle rebases. This means bridging stETH to other chains will prevent stakers from collecting their staking rewards. In the most common case, the rewards will naturally go to the bridge smart contract and never make it to the stakers. While working on full-blown bridging solutions, the Lido contributors encourage the users to only bridge the non-rebasable representation of staked ether, namely wstETH.
Risksβ
- Smart contract security. There is an inherent risk that Lido could contain a smart contract vulnerability or bug. The Lido code is open-sourced, audited and covered by an extensive bug bounty program to minimise this risk. To mitigate smart contract risks, all of the core Lido contracts are audited. Audit reports can be found here. Besides, Lido is covered with a massive Immunefi bugbounty program.
- Beacon chain - Technical risk. Lido is built atop experimental technology under active development, and there is no guarantee that Beacon chain has been developed error-free. Any vulnerabilities inherent to Beacon chain brings with it slashing risk, as well as stETH balance fluctuation risk.
- Slashing risk. Beacon chain validators risk staking penalties, with up to 100% of staked funds at risk if validators fail. To minimise this risk, Lido stakes across multiple professional and reputable node operators with heterogeneous setups, with additional mitigation in the form of self-coverage.
- stETH price risk. Users risk an exchange price of stETH which is lower than inherent value due to withdrawal restrictions on Lido, making arbitrage and risk-free market-making impossible. The Lido DAO is driven to mitigate above risks and eliminate them entirely to the extent possible. Despite this, they may still exist and, as such, it is our duty to communicate them.