Asymmetry contest - 0xRobocop's results

A protocol to help diversify and decentralize liquid staking derivatives.

General Information

Platform: Code4rena

Start Date: 24/03/2023

Pot Size: $49,200 USDC

Total HM: 20

Participants: 246

Period: 6 days

Judge: Picodes

Total Solo HM: 1

Id: 226

League: ETH

Asymmetry Finance

Findings Distribution

Researcher Performance

Rank: 33/246

Findings: 3

Award: $208.37

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: HollaDieWaldfee

Also found by: 0Kage, 0x52, 0xRobocop, Cryptor, HHK, MiloTruck, ToonVH, adriro, carrotsmuggler, d3e4, igingu

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-210

Awards

195.1013 USDC - $195.10

External Links

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L131

Vulnerability details

Impact

The unstake() and rebalanceToWeights() functions iterate over the derivatives mapping and call the withdraw() function on them.

The withdraw() function on the Reth.sol contract is the following:

function withdraw(uint256 amount) external onlyOwner { RocketTokenRETHInterface(rethAddress()).burn(amount); // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: address(this).balance}(""); require(sent, "Failed to send Ether"); }

It tries to convert rETH back to ETH directly with RocketPool. In case is not possible to do the convertion the execution will revert, causing a DoS on the functions unstake() and rebalanceToWeights().

Proof of Concept

The Rocket Pool Docs docs have the following note:

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

It's possible that if node operators have put all of the staking pool to work on the Beacon chain, then the liquidity pool won't have enough balance to cover your unstaking. In this scenario, you may find other ways to trade your rETH back to ETH (such as a decentralized exchange like Uniswap (opens new window)) - though they will likely come with a small premium."

Also this can be read directly from the Rocket Pool contracts starting at the burn function

Tools Used

Manual Review

Don't rely only on trading rETH back for ETH directly with Rocket Pool, when this is no possible, use an AMM like Uniswap or Balancer

#0 - c4-pre-sort

2023-04-04T19:56:01Z

0xSorryNotSorry marked the issue as duplicate of #210

#1 - c4-judge

2023-04-21T16:36:35Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-04-21T16:36:42Z

Picodes changed the severity to 3 (High Risk)

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L215

Vulnerability details

Impact

The amount of SafEth tokens minted depends on the amount of Eth contributed to the system relative to the eth underlying value already in the system. This is calculated as follows:

uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;

totalStakeValueEth is the amount of Eth contributed by the depositor. This contribution is calculated by adding the ETH value of the derivative tokens that were minted with the deposit of the user:

uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
for (uint i = 0; i < derivativeCount; i++) {
    uint256 weight = weights[i];
    IDerivative derivative = derivatives[i];
    if (weight == 0) continue;
    uint256 ethAmount = (msg.value * weight) / totalWeight;

    // This is slightly less than ethAmount because slippage
    uint256 depositAmount = derivative.deposit{value: ethAmount}();
    uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
         depositAmount
    ) * depositAmount) / 10 ** 18;
    totalStakeValueEth += derivativeReceivedEthValue;
}

preDepositPrice is calculated as follows:

preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;

underlyingValue is calculated by adding up the ETH value of all the balances in each derivative:

uint256 underlyingValue = 0;

// Getting underlying value in terms of ETH for each derivative
for (uint i = 0; i < derivativeCount; i++)
   underlyingValue +=
   (derivatives[i].ethPerDerivative(derivatives[i].balance()) *
       derivatives[i].balance()) /
       10 ** 18;

The Reth.sol contract reads the spot price of the rETH/WETH Uniswap pool, which can be manipulated (This price is read when poolCanDeposit() returns false).

A manipulation of this price, for example, dumping rETH, will cause underlyingValue to decrease making each SafEth token cheaper and allowing the attacker to get more tokens.

Proof of Concept

The conditions needed for a succesful attack are:

1.- Reth.poolCanDeposit(_amount) returns false during the computation of underlyingValue. Note: Reth.poolCanDeposit(_amount) returns false if _amount will cause the deposit pool to reach its limit of 5000 ETH.

2.- The weight of the Reth derivative have been set to zero but a rebalance have not happened.

Giving the conditions from above, the proof of concept is as follows:

The state before the attacker deposits is:

  1. totalSupply of SafEth tokens are: 30
  2. Each derivative contract has: 10 rEth, 10 wsEth, 10 sfrxEth.
  3. Weights. rEth = 0, wsEth = 40, sfrxEth = 40, hence totalWeight = 80

For wsEth and sfrxEth, ethPerDerivative returns 1 ETH / derivative. But the attacker have manipulated the rEth uniswaps spot price, so for rEth, ethPerDerivative returns 0.1 ETH / rETH.

The attacker stakes 10 ETH. First underlyingValue is computed. The contributions are:

1.- The wsEth derivative contract contributes (1 ETH / wsEth) * 10 wsEth = 10 Eth 2.- The sfrxEth derivative contract contributes (1 ETH / sfrxEth) * 10 sfrxEth = 10 Eth 3.- The rEth derivative contract contributes (0.1 ETH / rEth) * 10 rEth = 1 Eth

So underlyingValue equals 20.1 ETH.

preDepositPrice is computed as underlyingValue / totalSupply:

20.1 ETH / 30 SafEth. So, preDepositPrice == 0.67 ETH / SafEth.

Then the attacker's ETH gets deposited to each derivative, because rETH weight have been set to zero, then it is skipped. The contribution of the attacker's ETH will be:

5 wsEth and 5 sfrxEth which its prices have not been manipulated, so totalStakeValueEth will be 10 ETH.

The tokens to mint are calculated as totalStakeValueEth / preDepositPrice. Substituting values we have mintAmount = 10 ETH / 0.67 ETH / SafETH. Which is 14.92 SafETH.

Even though the attacker contributed with the 25% of ETH (10/40), he has 33% (14.92 / 44.92) of the totalSupply of the SafETH token.

The attack can be more profitable by a bigger manipulation of the rETH price or if the Reth.sol derivative contract have a bigger percentage of all the derivatives on the protocol (In the example it was 33%).

Tools Used

Manual Review

Don't use AMM's spot price since those can be manipulated easily via flashloans, instead use TWAPs.

#0 - c4-pre-sort

2023-03-31T18:07:36Z

0xSorryNotSorry marked the issue as high quality report

#1 - c4-pre-sort

2023-04-04T11:35:27Z

0xSorryNotSorry marked the issue as duplicate of #601

#2 - c4-judge

2023-04-21T16:14:35Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-04-21T16:15:13Z

Picodes marked the issue as duplicate of #1125

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter