A design for bridging rebasable stETH token with minting on non-native side.
We believe that, in order for stETH to become a unit of account, it must gain proper support on L2s and popular non-Ethereum chains. By proper support we mean:
Let's break down the points listed above and discuss alternative options.
Why bother with stETH at all if we have wstETH that can be easily bridged? There are several upsides to having stETH on the non-native side:
Cons:
Pros:
Cons:
Allowing to mint stETH outside of Ethereum adds a number of benefits:
Cons:
Alternatively, we might rely solely on having (w)stETH/ETH pools on non-Ethereum side for the users to get stETH in exchange for ETH.
Pros:
Cons:
As outlined in the beginning of the document, we propose to go with supporting both stETH and its minting on non-Ethereum side.
Below, we'll assume this options selection and describe the integration with a single chain designated as RemoteChain. Names of the RemoteChain contracts will be prefixed with Remote
, and token symbols with R:
. In the later sections, we'll consider the following components of the integration and options for implementing each of them:
The solution can be scaled to a large number of non-Ethereum chains. The technical specification and API is described in a separate document.
The token used for bridging is the Ethereum-native non-rebasable stETH wrapper, wstETH. The counterparty token on the RemoteChain side, R:wstETH, is implemented by the generic RemoteWstEth
token contract deployed and owned by the bridge.
On RemoteChain, a custom RemoteStEth
contract is deployed implementing the rebasable R:stETH token, a wrapper around the non-rebasable R:wstETH. Internally, RemoteStEth
stores balances in the form of Lido pool shares and calculates the R:stETH balance of an address by multiplying its shares balance by the current share price. The amount of R:stETH corresponding to one share can be minted by locking one R:wstETH token, and one R:wstETH token can be released by burning the amount of R:stETH corresponding to one share.
RemoteStEth.getSharePrice()
returns the amount of R:stETH corresponding to one share (share price).RemoteStEth.sharesOf(address)
returns the amount of shares owned by the address.RemoteStEth.balanceOf(address)
returns the R:stETH amount owned by the address. Always equals to RemoteStEth.sharesOf(address) * RemoteStEth.getSharePrice()
.RemoteStEth.totalShares()
returns the total amount of shares owned by all RemoteChain adresses.RemoteStEth.totalSupply()
returns the total amount of R:stETH owned by all RemoteChain adresses. Always equals to RemoteStEth.totalShares() * RemoteStEth.getSharePrice()
.RemoteStEth.fromWstEth(uint256)
transfers the given amount of R:wstETH from the message sender and mints to them the same amount of shares.RemoteStEth.toWstEth(uint256)
burns the given amount of shares from the message sender and transfers to them the same amount of R:wstETH.Updates of the share price are commmunicated from Ethereum to RemoteChain via a channel described in the "Ethereum state communication channel" section.
Apart from wrapping R:wstETH, R:stETH tokens can be minted by submitting R:WETH (the RemoteChain representation of the Wrapped Ether token) to the RemoteStEth
contract. The conversion rate is 1, meaning that the amount of R:stETH minted equals the amount of the R:WETH submitted.
RemoteStEth.canSubmit()
returns whether stETH minting is enabled.RemoteStEth.submit(uint256)
transfers the given amount of R:WETH from the message sender and mints to them the same amount of R:stETH. Reverts if RemoteStEth.canSubmit()
is false
.Under the hood, the amount of Lido pool shares minted by the RemoteStEth
contract is calculated by dividing the submitted R:WETH amount by the current price of one Lido pool share that can be obtained by calling RemoteStEth.getSharePrice()
.
The stETH minting state updates (i.e. whether minting has became disabled or enabled) are communicated from Ethereum to RemoteChain by the means of the same channel that's used for communicating Lido share price updates. When minting stETH is disabled on Ethereum for some reason, e.g. to prevent APR dilution due to the long validator activation queue, the RemoteStEth.submit
function reverts. It should also revert when there were no minting state or share price updates for a set period of time (e.g. 48h).
It should be noted that minting state switches on Ethereum shouldn't be able to happen at arbitrary moments. Instead, any minting state change should be synchronized with Lido oracle report processing and with stETH rebases. This will make integration with different blockchains and protocols much easier by allowing to perform some actions before/after minting state changes.
The R:WETH submitted on RemoteChain is buffered in the RemoteStEth
contract and is periodically bridged to Ethereum, where it's submitted to Lido. The minted stETH amount is then converted to wstETH and transferred back to the RemoteStEth
contract address on RemoteChain via the bridge. This is done by the custom LidoXChainEthStaking
contract instance. Each remote chain should have a corresponding LidoXChainEthStaking
instance deployed at a unique Ethereum address.
RemoteStEth.forwardBufferedEther()
transfers the whole WETH balance of self to the LidoXChainEthStaking
contract address on Ethereum via the bridge.LidoXChainEthStaking.submitBuferedEther()
submits the whole WETH balance of self to Lido, converts the minted stETH to wstETH and transfers it to the RemoteStEth
contract address on RemoteChain via the bridge.RemoteStEth.forwardBufferedEther()
can be called by any address, e.g. a dedicated bot. Also, it's called from RemoteStEth.submit(uint256)
when certain amount of buffered WETH or a certain point in time is reached (will be discussed below).
LidoXChainEthStaking.submitBuferedEther()
can also be called by any address. It's called by a dedicated bot when the WETH balance of LidoXChainEthStaking
address exceeds the configured amount. It's also called by the LidoOracle
contract when oracle quorum is achieved but before it's applied, e.g. right before stETH undergoes a rebase and/or stETH minting state changes. For this, a new callback mechanism is added to LidoOracle
since the current IBeaconReportReceiver
mechanism invokes the callback after stETH is rebased.
Now, there's one important invariant broken by the above stETH minting design: in order for all R:stETH holders to be able to unwrap their R:stETH back to R:wstETH and potentially exit to other chains, the total amount of R:wstETH on the RemoteStEth
contract address should be no less than the total amount of R:stETH shares. This is not the case anymore because of two reasons:
Since RemoteStEth.forwardBufferedEther()
and LidoXChainEthStaking.submitBuferedEther()
are called asynchronously, and since bridges have latency, there will be an extended amount of time between when R:stETH shares are minted and when the corresponding R:wstETH is forwarded back to RemoteStEth
address. During this time, there will be less R:wstETH on the contract than the total amount of shares.
Since, at the moment when Lido oracles achieve consensus and stETH undergoes a rebase, there may be some WETH either sitting buffered in the RemoteStEth
contract or in flight via the bridge, and because updates of Lido share price cannot be forwarded to RemoteChain immediately due to bridge latency, some R:stETH shares may be minted at a lower share price than the price actual stETH shares are minted at from the received WETH. This will result in less R:wstETH being forwarded to RemoteStEth
contract than the amount of its newly-minted shares.
Both of the issues result in the lack of R:wstETH tokens on the RemoteStEth
contract required to back its shares (and thus R:stETH tokens).
Let's assume that, on RemoteChain, StakedPerSec
ETH is staked on average each second, it takes on average BridgeDelaySec
seconds to perform cross-chain token transfer, the size of RemoteStEth
WETH buffer is set to RemoteBufferSize
, the maximum amount of WETH on LidoXChainEthStaking
address is EthereumBufferSize
, and stETH APR is StEthAPR
. For illustrative purposes, let's choose the following values:
StakedPerSec
: 0.04 ETH, i.e. around 3500 ETH per day which is the current 1 month MA on Ethereum.BridgeDelaySec
: 300 sec, i.e. 5 minutes.RemoteBufferSize
: 32 ETH.EthereumBufferSize
: 32 ETH.StEthAPR
: 15% to account for the APR increase after the Merge.We can estimate the average amount of temporarily non-backed R:stETH due to the first issue as TmpNonBackedRStEth = RemoteBufferSize + EthereumBufferSize + 2 * StakedPerSec * BridgeDelaySec
, yielding 88 ETH for the parameter values above. This means that, if all R:stETH holders decide to convert their R:stETH to R:wstETH, conversion of the last 88 R:stETH would temporarily fail.
Regarding the issue 2, the worst-case scenario is that, BridgeDelaySec
seconds before the moment of stETH rebase, the WETH buffer in RmoteStEth
becomes full and thus gets forwarded to Ethereum, but this WETH still hasn't reached Ethereum at the moment of the rebase due to the bridge delay. Since the moment the buffer was emptied, StakedPerSec * BridgeDelaySec
WETH has been submitted to the buffer, sitting on RemoteChain. Another StakedPerSec * BridgeDelaySec
WETH will be submitted before the Lido share price update reaches RemoteChain. This means that RemoteBufferSize + 2 * StakedPerSec * BridgeDelaySec
R:stETH will be minted using the outdated share price which, under normal circumstances, should be StEthAPR / 365.25
percent less than the actual one, leading to NonBackedRStEthIncPerYear = (RemoteBufferSize + 2 * StakedPerSec * BridgeDelaySec) * StEthAPR / 3652.5
R:stETH becoming backed by nothing per day. For the parameter set above, this yields 0.23 R:stETH per day, 84 R:stETH per year.
To work around both issues, Lido DAO could lock a sufficient amount of R:wstETH tokens on the RemoteStEth
address without minting R:stETH tokens, making the R:wstETH balance greater than the number of R:stETH shares. This would compensate for both the temporary lack of R:wstETH on the contract address (issue 1) and for the permanent minting of unbacked R:stETH (issue 2). A pair of functions on RemoteStEth
contract could be added:
RemoteStEth.getExcessWstEthBalance()
returns the amount of R:wstETH on the contract address that is not used to back any R:stETH shares, i.e. the difference between R:wstETH cnRemoteStEth.recoverExcessWstEth()
is only callable by an administrative address and transfers all excess R:wstETH to the caller.Tha amount of the locked R:wstETH could be chosen and maintained like this:
TmpNonBackedRStEth + NonBackedRStEthIncPerYear
ETH worth of R:wstETH.TmpNonBackedRStEth + NonBackedRStEthIncPerYear / 3
.Lido DAO would bear the expenses of compensating the lack of R:wstETH shares resulting from the issue 2 in the form of constantly decreasing R:wstETH balance at the rate of NonBackedRStEthIncPerYear
ETH worth of R:wstETH per year. Given the parameter set above, it's 84 ETH per year, which means that, assuming APR of 15% and Lido taking 5% as fees from that, the total of 11,200 ETH should be staked on RemoteChain to fully compensate these DAO expenses. This amount of staking will be reached in less than four days under the listed parameters.
Pros:
Cons:
It's also possible to virtually eliminate permament minting of non-backed R:stETH shares by allowing to mint stETH shares on Ethereum at the same rate as R:stETH shares were minted on RemoteChain. This will effectively move the burden of compensating latency-induced effects from Lido DAO to stETH holders.
For that, the following design could be used:
LidoOracle
storing the share price effective before the last rebase. This variable is updated on each stETH rebase.submitUsingPreviousSharePrice()
is added to Lido
allowing to submit ETH and mint stETH using the previous share price. This function can only be called by the LidoXChainEthStaking
contract.LidoXChainEthStaking
contract: LidoXChainWETHCollector1
and LidoXChainWETHCollector2
. At any moment, RemoteStEth
forwards collected WETH from RemoteChain to one of these two contracts.LidoXChainEthStaking.submitBuferedEther()
extracts WETH from the WETH collector contract used before the last rebase and submits it to Lido using the Lido.submitUsingPreviousSharePrice()
function. Then, it extracts WETH from the currently-used collector contract and submits it to Lido using the regular Lido.submit(address)
function.This will eliminate the issue 2 and the associated DAO expenses. Lido DAO would still be required to lock some R:wstETH balance on the RemoteStEth
contract to back just-minted R:stETH shares (issue 1).
Pros:
Cons:
Another way of eliminating both issues is to delay minting R:stETH until the backing R:wstETH tokens are received from Ethereum:
In the RemoteStEth
contract, add cumulative counters of the wrapped and unwrapped R:wstETH amounts incremented by fromWstEth
and toWstEth
functions.
When a user submits WETH, mint them an NFT keeping the following pieces of information: 1) the submitted amount Amt
, 2) the current total amount of R:stETH shares TotalShares
, 3) the current R:wstETH balance of RemoteStEth
contract WstEthBal
, 4) cumulative wrapped CW
and unwrapped CU
R:wstETH amounts, 5) the total submitted ETH amount associated with non-burned NTFs NftTotalEth
, excluding the one being minted.
Allow to mint NFT.Amt
of R:stETH in exchange for burning the NFT if:
where
Pros:
Cons:
Considering the cons of solutions 2 and 3, we propose to go with the solution 1. Solution 3 might still be implemented in addition and activated when stETH minting is disabled on Ethereum.
To implement the rebasing and minting design described above, a channel for communicating state changes from Ethereum to RemoteChain is required. The state includes the following:
If the Solution 2 to latency-induced minting issues is to be implemented, the state should also include an epoch number that can be used to direct submitted WETH to one of the two collector contracts. From now on we'll assume that this solution is not implemented, though adding the epoch number to the state should be trivial.
The communication channel could be implemented in several ways described below.
The ideal way would be to have a bridge that allows to perform arbitrary smart contract invocations and use that for passing the information about Ethereum state to the RemoteStEth
contract on the RemoteChain side.
Pros:
Cons:
Currently, there are no wide-adopted decentralized bridges that allow doing so, and those that technically have this support are not always willing to actually enable it for custom contracts. So what we're basically left with are bridges only supporting ERC20 token transfers.
To implement a channel using only ERC20 token and a token bridge, the following set of contracts can be used:
LidoXChainEthAdapter
, is deployed, implementing the IBeaconReportReceiver
interface and processLidoOracleReport()
function. It is installed to LidoOracle
contract as beacon report receiver.LidoXChainEthState
, is deployed, with the initial total supply of zero. The token is only mintable by LidoXChainEthAdapter
and is not burnable. Let's designate the token symbol as lethSTATE.LidoXChainEthState
token is bridged to RemoteChain, with the conterpart generic token contract, RemoteLidoXChainEthState
, being deployed and controlled by the bridge. Let's designate the token symbol as R:lethSTATE.When LidoXChainEthAdapter
receives new beacon state report, it calculates whether stETH minting is enabled and the updated stETH share price and mints an amount of lethSTATE that encodes the state update. The update is encoded in a way that preserves additiveness, meaning that adding multiple encoded updates together as numbers would produce an update equivalent to the set of added updates, so that the end state could be reconstructed by adding all updates together. This is done by composing the minted amount from four numbers: timestamp increase (5 bytes), minting state flip counter increase (3 bytes), share price increase (12 bytes), and share price decrease (12 bytes). Share price increase and decrease cannot be non-zero in a single update. Each number is bit-shifted so that it takes a distinct byte range within the resulting 32-byte number:
After that, LidoXChainEthAdapter
transfers the minted lethSTATE amount to the RemoteStEth
contract address on RemoteChain via the bridge. The additiveness of the encoded updates guarantees that the current state can be obtained on RemoteChain from the R:lethSTATE balance of the RemoteStEth
address by bit-shifting and masking it tp obtain timestampInc
, mintingStateInc
, sharePriceInc
, and sharePriceDec
. Then, share price can be calculated as the difference of sharePriceInc
and sharePriceDec
, and whether minting is enabled can be determined by looking if mintingStateInc
is even.
The maximum values for the state components are the following:
Alternatively, Lido oracle deamon could be tasked with propagating the Ethereum state to all supported chains.
Considering the cons, the proposed option is 2: going with the lestSTATE token for communicating the state via a bridge.
Some bridges take fees and, usualy, the fee is taken in either of the two forms:
The type-1 bridges can be used only if they allow precise calculation of the resulting amount that will be transferred from the bridge to the destination address. In this case, two modifications are needed:
LidoXChainEthAdapter.processLidoOracleReport()
should mint the amount of lethSTATE corrected for the fee, i.e. the amount that, after taking the fee, would equal the encoded update.RemoteStEth.submit()
should mint the amount of R:stETH shares corresponding to the received amount of WETH corrected for the fee being taken twice: the first time when transferring WETH from RemoteChain to Ethereum, the second time when transferring wstETH from Ethereum back to RemoteChain.The type-2 bridges can be used with the following modifications:
LidoXChainEthAdapter
and use this reserve for paying bridge fees associated with transferring wstETH to RemoteChain.RemoteStEth
and use this reserve for paying bridge fees associated with transferring WETH to Ethereum.This will incur additional DAO expenses, though they should be limited due to the buffering on both Ethereum and RemoteChain side.
Some bridges, e.g. Wormhole v2, require an offchain API request followed by a transaction on the recipient chain in order to complete token transfer.
Using these bridges will require maintining a bot monitoring pending lethSTATE and wstETH token transfers and completing them.
TODO
TODO
TODO
TODO