On March 24, Lido protocol on Goerli testnet has been successfully upgraded to V2.
This document is intended for Lido's existing partners and developers looking to understand the scope of the upgrade in terms of how their product interacts with Lido protocol. A high-level upgrade overview can be found in the blog post.
The code of the upgrade is open and available on Lido's GitHub. You can find all the changes in this PR.
The addresses of all the contracts deployed on Goerli are listed on the Lido's docs hub.
Lido UI has also been upgraded to V2 and is now being tested at https://stake.testnet.fi/
No changes has been done to the deposit flow. All the addresses, calls, methods and events remain the same as before the protocol upgrade.
Subgraph and Reward History on Goerli are not operational at the moment. The issue is being addressed, we expect the services to resume by end of April.
Shares mechanics explained in our integration guide remain the same as before the protocol upgrade.
The biggest highlight of the upgrade is enabling withdrawals. The withdrawal flow is quite different from the deposit flow, unlike deposit flow, it is also async.
The process consists of the following steps:
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 finalised. The rate stETH/ETH of the request finalisation can't get higher than it's been at the moment of request creation. the user will be able to claim:
The second option is unlikely, and we haven't ever seen the conditions for it on mainnet so far.
See the detailed description of the withdrawal flow below.
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.
The overview of the most frequent operations around the NFT queue.
Get the NFTs owned by the user (the not yet claimed ones)
getWithdrawalRequests(address _owner)
will return an array of NFT ids.Get NFT asset metadata
tokenURI(uint256)
as declared via ERC721 (Returns empty string currently. Subject to change in the future.)Init withdrawal
stETH
requestWithdrawalsWithPermit(uint256[] calldata amounts, address _owner, PermitInput calldata _permit)
and get the ids of created positions, where msg.sender
will be used to transfer tokens from and the _owner
will be the address that can claim or transfer NFT (defaults to msg.sender
if itβs not provided.
WithdrawalQueueERC721.sol
contract can be approved in a separate upfront transaction (stETH.approve(withdrawalQueueERC712.address, allowance)
), and the requestWithdrawals(uint256[] calldata amounts, address _owner)
method called afterwards.wstETH
requestWithdrawalsWstETHWithPermit(uint256[] calldata amounts, address _owner, PermitInput calldata _permit)
and get the ids of created positions, where msg.sender
will be used to transfer tokens from, and the _owner
will be the address that can claim or transfer NFT (defaults to msg.sender
if itβs not provided.
WithdrawalQueueERC721.sol
contract can be approved in a separate upfront transaction (wstETH.approve(withdrawalQueueERC712.address, allowance)
), and the requestWithdrawalsWstETH(uint256[] calldata amounts, address _owner)
method called afterwards.PermitInput
structure defined as follows:
Get withdrawal request status (statuses refresh every ~4 hours on Goerli and once a day on mainnet)
getWithdrawalStatus(uint256[] calldata _requestIds)
with ids of the user's positions and get the amount of stETH from the returned struct.NOTE: Since stETH is an essential token if the user tries to request a withdrawal using wstETH directly, the amount will be nominated in stETH on request creation.
Claim user's ETH
claimWithdrawal(uint256)
with the NFT Id on behalf of the NFT owner.It is also possible to claim withdrawals in batches, it's being done in two steps:
claimWithdrawals(uint256[] calldata _requestIDs, hints)
where
findCheckpointHints(uint256[] calldata _requestIDs, 1, lastCheckpoint)
getLastCheckpointIndex()
There is an unlikely scenario of Lido protocol going into bunker mode in case of Lido's validators getting slashed. This will result in the withdrawals processing becoming significantly slower, but it isn't meant to interfere with any specific withdrawal flow elements. However, the amount of ETH claimed in this case might be lower than the amount requested in stETH.
The full withdrawal flow is expected to take from a few hours on Goerli or a single day on mainnet to up to several weeks (in case of the bunker mode on). In other words, the request finalization can't happen earlier than on the next Accounting Oracle report, but will take longer if there's not enough ETH in EL buffers, and validator exits will be triggered.
Lido on Ethereum V2 employs not one, but two Oracles: Accounting Oracle & Exit Bus. Former provides rebase data & finalizes withdrawal request, the latter β determines which validators to ask for exiting.
In order to retain backwards compatibility, original Lido Oracle contract is updated & renamed to Legacy Oracle. The data is updated, but the contract is deprecated and would be removed in the upgrades following V2.
Although the old way of calculating the APR would still result in relevant numbers, the math might be off in case of significant withdrawals.
NB: OUTDATED OLD FORMULA
New proper way:
from [1]
:
To calculated APR for the last completed oracle frame:
So, the new formula takes into account preTotalShares
and postTotalShares
values, while, in contrast, the old formula didn't use them:
The new formula also doesn't require to calculate lidoFee
at all (because fee distribution works by changing total shares amount under the hood).
When Lido V2 protocol finalizes withdrawal requests, the Lido
contract sends ether to WithdrawalQueue
(excluding these funds from totalPooledEther
, i.e., decreasing TVL) and assigns to burn underlying locked requests' stETH
shares in return.
In other words, withdrawal finalization decreases both TVL and total shares.
Old formula isn't suitable anymore because it catches TVL changes, but skips total shares changes.
Illustrative example (using smallish numbers far from the real ones for simplicity):
Please note that the APR shown on Lido's UI is not expected to match the APR calculated on-chain. The one displayed on our websites is a 7-days rolling average APR.
permit
function as introduced in the EIP-2621: https://github.com/lidofinance/lido-dao/blob/feature/shapella-upgrade/contracts/0.4.24/StETHPermit.sol#L99-L112isValidSignature(hash, signature)
function as introduced in the EIP-1271: https://github.com/lidofinance/lido-dao/blob/feature/shapella-upgrade/contracts/common/lib/SignatureUtils.sol#L29-L57transferSharesFrom
method has been added for stETH (had only transferShares
before) working within the provided allowance.