DittoETH

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

Withdrawals of `rETH` token are dependant on excess balance in RocketDepositPool.

Summary

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.

Vulnerability Details

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:

  1. ETH that other stakers have deposited, which hasn't been used by a node operator to create a new validator yet

  2. 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.

Scenario

  1. Let's say that user deposited 10 ETH into Ditto using depositETH (BridgeRouterFacet.sol).

  2. User selected to deposit their ETH with rETH bridge (BridgeReth.sol).

function depositEth() external payable onlyDiamond returns (uint256) {
IRocketDepositPool rocketDepositPool = IRocketDepositPool(
rocketStorage.getAddress(ROCKET_DEPOSIT_POOL_TYPEHASH)
);
IRocketTokenRETH rocketETHToken = _getRethContract();
uint256 originalBalance = rocketETHToken.balanceOf(address(this));
rocketDepositPool.deposit{value: msg.value}();
uint256 netBalance = rocketETHToken.balanceOf(address(this)) -
originalBalance;
if (netBalance == 0) revert NetBalanceZero();
return rocketETHToken.getEthValue(netBalance);
}
  1. rETH is correctly minted and user virtual zETH is assigned to his escrowed ETH.

  2. After some time user wants to get back their ETH so he calls unstakeEth (BridgeRouterFacet.sol) which will eventually call rocketETHToken.burn(rethValue);.

  3. User did not get his ETH back because liquidty pool couldn't handle user's request (not enough excess balance).

Further explenation

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.

// Burn rETH for ETH
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);
}

In burn function we are calling withdrawDepositCollateral(ethAmount);.

// Withdraw ETH from the deposit pool for collateral if required
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));
}

It calls rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance)); to ETH from rocketDepositPool.

// Withdraw excess deposit pool balance for rETH collateral
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);
}

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.

Impact

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.

Tools Used

VScode, Manual Review

Recommendations

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.

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.