Inconsistency between pricing used to determine amount of SafEth to mint and actual amount of rETH tokens obtained allows attacker to manipulate oracle and partially drain Reth.sol rETH balance.
Proof of Concept
Reth.ethPerDerivative returns ETH/rETH pricing of the deposit pool if the _amount passed can be deposited, otherwise it returns the current Uniswap price.
// Reth.sol:L211-216
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
if (poolCanDeposit(_amount))
return
RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18);
else return (poolPrice() * 10 ** 18) / (10 ** 18);
}
SafEth.stake determines the amount of tokens to mint based on the underlying value of the derivatives contracts.
// stake() - SafEth.sol:L68-99
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;
uint256 totalSupply = totalSupply();
uint256 preDepositPrice; // Price of safETH in regards to ETH
if (totalSupply == 0)
preDepositPrice = 10 ** 18; // initializes with a price of 1
else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
for (uint i = 0; i < derivativeCount; i++) {
uint256 weight = weights[i];
IDerivative derivative = derivatives[i];
if (weight == 0) continue;
uint256 ethAmount = (msg.value * weight) / totalWeight;
// This is slightly less than ethAmount because slippage
uint256 depositAmount = derivative.deposit{value: ethAmount}();
uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
depositAmount
) * depositAmount) / 10 ** 18;
totalStakeValueEth += derivativeReceivedEthValue;
}
// mintAmount represents a percentage of the total assets in the system
uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
_mint(msg.sender, mintAmount);
The problem with this logic is that to retrieve the underlying value, the pricing of each derivative is retrieved with derivatives[i].ethPerDerivative(derivatives[i].balance(). Since the _amount passed to ethPerDerivative is the balance of the derivative contract, and not necessarily the amount being deposited, it's possible that the actual deposit amount is little enough that it can be deposited while the full balance of the contract may not be able to be deposited, causing a price discrepancy between the price used to determine the SafEth mint amount and the actual price used in obtaining rETH tokens. This price discrepancy opens up the following attack vector:
Attacker takes large rETH flashloan (or happens to be a RETH whale)
Attacker sells large amount of rETH in uniswap pool used in Reth.sol, causing the price to decrease significantly
Attacker calls stake with an amount which the rocket deposit pool can process when the total Reth.sol balance is greater than the rocket deposit pool can process
Attacker receives amount of tokens to mint denominated partially in artificially reduced price of rETH in Uniswap pool, resulting in more SafEth being minted than corresponding value added throughout derivatives
Attacker reverses initial Uniswap trade, repaying flashloan if necessary
Attacker calls unstake to withdraw their share of SafEth now that the Uniswap pool is no longer being manipulated
Attacker receives significantly more ETH than they initially staked
Attacker repeats the process as long as it's profitable, draining a significant portion of rETH from Reth.sol
Can be repeated atomically such that pausing staking/unstaking will have no effect
Recommended Mitigation Steps
Ensure pricing is always consistent between execution and retrieving underlying value.
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L211-L216
Vulnerability details
Impact
Inconsistency between pricing used to determine amount of SafEth to mint and actual amount of rETH tokens obtained allows attacker to manipulate oracle and partially drain
Reth.sol
rETH balance.Proof of Concept
Reth.ethPerDerivative
returns ETH/rETH pricing of the deposit pool if the_amount
passed can be deposited, otherwise it returns the current Uniswap price.SafEth.stake
determines the amount of tokens to mint based on the underlying value of the derivatives contracts.The problem with this logic is that to retrieve the underlying value, the pricing of each derivative is retrieved with
derivatives[i].ethPerDerivative(derivatives[i].balance()
. Since the_amount
passed toethPerDerivative
is the balance of the derivative contract, and not necessarily the amount being deposited, it's possible that the actual deposit amount is little enough that it can be deposited while the full balance of the contract may not be able to be deposited, causing a price discrepancy between the price used to determine the SafEth mint amount and the actual price used in obtaining rETH tokens. This price discrepancy opens up the following attack vector:stake
with an amount which the rocket deposit pool can process when the total Reth.sol balance is greater than the rocket deposit pool can processunstake
to withdraw their share of SafEth now that the Uniswap pool is no longer being manipulatedRecommended Mitigation Steps
Ensure pricing is always consistent between execution and retrieving underlying value.