DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: low
Valid

`unstakeEth` is unreliable and depend on excess RocketDepositPool balance which can brick the whole protocol

Summary

unstakeEth is unreliable and depend on excess RocketDepositPool balance which can brick the whole protocol

Vulnerability Details

unstakeEth is unreliable and depend on excess RocketDepositPool balance which can brick the whole protocol
The protocol allows a user to call BridgeRouterFacet.unstakeEth() to unstake a amount of zETH and get back ETH at all times.
The Reth works differently.
Unstake are made by calling the rocketETHToken.burn() function:

function unstake(address to, uint256 amount) external onlyDiamond {
IRocketTokenRETH rocketETHToken = _getRethContract();
uint256 rethValue = rocketETHToken.getRethValue(amount);
uint256 originalBalance = address(this).balance;
rocketETHToken.burn(rethValue);
uint256 netBalance = address(this).balance - originalBalance;
if (netBalance == 0) revert NetBalanceZero();
(bool sent,) = to.call{value: netBalance}("");
assert(sent);
}

The issue with this is that the RocketTokenRETH.burn function only allows for excess balance to be withdrawn. I.e. ETH that has been deposited by stakers but that is not yet staked on the Ethereum beacon chain. So Rocketpool allows users to burn rETH and withdraw ETH as long as the excess balance is sufficient.If there is no excess balance because enough users burn rETH or the Minipool capacity increases, the protocol is bascially unable to operate.

Impact

Unstake Eth are then impossible

Proof of Concept

I show in this section how the current withdrawal flow for the Reth derivative is dependend on there being excess balance in the RocketDepositPool.

The current withdrawal flow calls RocketTokenRETH.burn which executes this code:
link

function burn(uint256 _rethAmount) override external {
// Check rETH amount
require(_rethAmount > 0, "Invalid token burn amount");
require(balanceOf(msg.sender) >= _rethAmount, "Insufficient rETH balance");
// Get ETH amount
uint256 ethAmount = getEthValue(_rethAmount);
// Get & check ETH balance
uint256 ethBalance = getTotalCollateral();
require(ethBalance >= ethAmount, "Insufficient ETH balance for exchange");
// Update balance & supply
_burn(msg.sender, _rethAmount);
// Withdraw ETH from deposit pool if required
withdrawDepositCollateral(ethAmount);
// Transfer ETH to sender
msg.sender.transfer(ethAmount);
// Emit tokens burned event
emit TokensBurned(msg.sender, _rethAmount, ethAmount, block.timestamp);
}

This executes withdrawDepositCollateral(ethAmount):

Link

function withdrawDepositCollateral(uint256 _ethRequired) private {
// Check rETH contract balance
uint256 ethBalance = address(this).balance;
if (ethBalance >= _ethRequired) { return; }
// Withdraw
RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool"));
rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance));
}

This then calls rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance)) to get the ETH from the excess balance:

link

function withdrawExcessBalance(uint256 _amount) override external onlyThisLatestContract onlyLatestContract("rocketTokenRETH", msg.sender) {
// Load contracts
RocketTokenRETHInterface rocketTokenRETH = RocketTokenRETHInterface(getContractAddress("rocketTokenRETH"));
RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault"));
// Check amount
require(_amount <= getExcessBalance(), "Insufficient excess balance for withdrawal");
// Withdraw ETH from vault
rocketVault.withdrawEther(_amount);
// Transfer to rETH contract
rocketTokenRETH.depositExcess{value: _amount}();
// Emit excess withdrawn event
emit ExcessWithdrawn(msg.sender, _amount, block.timestamp);
}

And this function reverts if the excess balance is insufficient which you can see in the require(_amount <= getExcessBalance(), "Insufficient excess balance for withdrawal"); check.

Tools Used

Vscode

Recommendations

The solution for this issue is to have an alternative withdrawal mechanism in case the excess balance in the RocketDepositPool is insufficient to handle the withdrawal.

The alternative withdrawal mechanism is to sell the rETH tokens via the Uniswap pool.

You can use the RocketDepositPool.getExcessBalance to check if there is sufficient excess ETH to withdraw from Rocketpool or if the withdrawal must be made via Uniswap.

Updates

Lead Judging Commences

0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-503

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.