Users should be able to unstake their ETH through the bridge that they used to stake their ETH. With rETH
in is not always possible due to protocol's code implementation.
In DittoETH protocol users are able to deposit and unstake their ETH using LSD
De-Fi protocols (Lido and RocketPool) through Ditto. It means that user can come with their native ETH and deposit it in Ditto. Then Ditto will deposit it in to Lido or RocketPool to mint their tokens (stETH
from Lido or rETH
from RocketPool).
In current code implementations there are no time boundries or any restrictions about depositing and unstaking tokens. It means that user should be able to deposit and unstake their tokens whenever they want. However it is not always true with rETH
. RocketPool says that Trading rETH back for ETH directly with Rocket Pool is only possible when the staking pool has enough ETH in it to handle your trade.
. In other words, unstaking rETH
back to ETH is possible when liquidity pool have enough balance to cover unstaking.
RocketPool documentation also states that ETH in this pool comes from two sources:
ETH that other stakers have deposited, which hasn't been used by a node operator to create a new validator yet
ETH that was returned by a node operator after they exited one of their validators and received their rewards from the Beacon Chain
Unstaking rETH
back to ETH requires calling RocketTokenRETH.burn
function which only allows for excess balance withdrawals.
Let's say that user deposited 10 ETH into Ditto using depositETH
(BridgeRouterFacet.sol).
User selected to deposit their ETH with rETH
bridge (BridgeReth.sol).
rETH
is correctly minted and user virtual zETH
is assigned to his escrowed ETH.
After some time user wants to get back their ETH so he calls unstakeEth
(BridgeRouterFacet.sol) which will eventually call rocketETHToken.burn(rethValue);
.
User did not get his ETH back because liquidty pool couldn't handle user's request (not enough excess balance).
Let's take a closer look at burn
function which is called in unstakeEth
. This will show how current Ditto's withdrawal implementation is dependent on RocketDepositPool excess balance.
In burn
function we are calling withdrawDepositCollateral(ethAmount);
.
It calls rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance));
to ETH from rocketDepositPool.
This function will revert when there is not sufficient excess balance which is checked require(_amount <= getExcessBalance(), "Insufficient excess balance for withdrawal");
using this require.
It means that users are not always able to get their ETH back because of withdrawal code implementation. This is a real issue in case of price fluctuation or black swan event. It is worth noting that Ditto allows for withdrawals at any time.
Users won't be able to withdraw thier funds which, could lead to lose of funds. Also the protocol implementation does not ensure it's complete functionality.
VScode, Manual Review
To solve this issue protocol can implement alternative withdrawal mechanism to handle insufficient excess balance in the rocketDepositPool.
It could be achived by selling rETH tokens on DEX. RocketDepositPool.getExcessBalance
function can be used to determine if there is sufficient excess ETH or if the rETH
tokens should be swapped on Decentralized Exchanges.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.