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: 63/246
Findings: 2
Award: $84.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: monrel
Also found by: 0xRajkumar, 0xfusion, AkshaySrivastav, Bahurum, Brenzee, Cryptor, Dug, Haipls, Koolex, Krace, MiloTruck, RaymondFam, RedTiger, ToonVH, Tricko, Vagner, aga7hokakological, anodaram, bart1e, bin2chen, bytes032, carrotsmuggler, ck, d3e4, giovannidisiena, igingu, juancito, mahdirostami, mert_eren, n33k, nemveer, parsely, pavankv, sashik_eth, shaka, sinarette, ulqiorra, yac
3.4908 USDC - $3.49
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L78-L81 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L98-L99
Due to the way in which the SafEth
share price is calculated, an attacker can front-run the first depositor's transaction and steal funds through an inflation attack. SafEth::stake
calculates the share price by dividing the total asset amount by the total supply of shares, which is 0 at the time of the first deposit and means that the first depositor will receive 1 share for every wei deposited. The exploit is performed by front-running a large initial deposit and inflating the balance of the staked ether derivative contracts by sending a large number of tokens directly. This causes the preDepositPrice
to become very large which can result in the victim receiving little/no shares due to rounding given there is no check that the amount to mint is not zero. The result is that the attacker profits at the expense of the victim.
The preDepositPrice
which is used in calculations of the amount of shares to mint in exchange for a given amount of ether is calculated by:
if (totalSupply == 0) preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
Example exploit steps:
SafEth
with 1 wei for 1 share.preDepositPrice
of 300e18 given totalSupply
of 1e18 and underlyingValue
of 300e18. Thus, the mintAmount
calculation is 200e18/300e18
which floor rounds to 0.The first depositor may not receive shares in exchange for their assets if their transaction is front-run and the total asset amount has been manipulated, so this is evaluated to be HIGH.
Manual review
As stated in other similar reports, one solution to this problem is to burn the first 1000 shares thereby increasing the cost to perform this attack by the same factor. Additionally, ensure the number of shares is non-zero to prevent an attacker from stealing all the funds in the case where subsequent deposits are less than SafEth::underlyingValue
:
require(mintAmount != 0, "No shares minted");
#0 - c4-pre-sort
2023-04-04T12:42:00Z
0xSorryNotSorry marked the issue as duplicate of #715
#1 - c4-judge
2023-04-21T14:54:35Z
Picodes marked the issue as satisfactory
🌟 Selected for report: CodingNameKiki
Also found by: 0xd1r4cde17a, Franfran, MadWookie, MiloTruck, Moliholy, adriro, ast3ros, bin2chen, giovannidisiena, gjaldon, igingu, koxuan, rbserver, rvierdiiev, shaka, slippopz
81.3214 USDC - $81.32
The Asymmetry SafEth protocol aims to help diversify and decentralize liquid staking derivatives, exchanging ether staked in the protocol for staked ether derivative tokens based on some relative weighting. A function exposed by these derivative wrapper contracts, IDerivative::ethPerDerivative
, takes uint256 _amount
as argument which is unused in all invocations except for the case when the derivative contract is Reth
. This is because the rETH deposit pool has a limit above which the balance is capped, currently set to 5000 ether as given by RocketDAOProtocolSettingsDeposit::getMaximumDepositPoolSize
, so any additional rETH is made up by the SafEth protocol through swaps on Uniswap V3.
Due to a bug in SafEth::stake
which passes derivatives[i].balance()
as _amount
to IDerivative::ethPerDerivative
, after a sufficient number of deposits to Reth
, this derivative contract balance of rETH
could exceed the remaining room in the pool, causing ethPerDerivative
to erronously quote the UniswapV3 pool spot price as defined by Reth::poolPrice
instead of RocketTokenRETHInterface::getEthValue
. Also note that whilst this function expects an ether balance, it is in fact receiving an rETH
balance (the same is true for the other derivative tokens; however, as stated above, this value is not used). Given these preconditions, an attacker could use a flash loan to manipulate the spot price of rETH to steal funds.
A highly discounted rETH price would give rise to a small underlyingValue
which in turn leads to a small preDepositPrice
given the existing totalSupply
which is assumed to be comparably large. When calculating totalStakeValueEth
, assuming the attacker's ether deposit does not exceed the remaining rETH pool room, Reth::ethPerDerivative
will this time use the correct rETH pricing which, combined with the small preDepositPrice
, will result in a large number of SafEth tokens being minted. The attacker can then call SafEth::unstake
with this large _safeEthAmount
, withdrawing underlying ether from the derivative contracts for a profit and at the expense of other protocol participants.
Let us first assume that the rETH pool is close to full, with a remaining room of say 200 ether which corresponds to the current max amount of the protocol (note that the owner has the ability to change this value). Assuming deposits totalling 100 ether are made to the rETH pool external to the protocol, and the derivative tokens have relative weights of 250, 250, 500 (WstETH, SfrxETH, Reth respectively), the first condition for this attack is met if protocol participants have deposited a total of 180 ether into the protocol. This means that the rETH pool is 10 ether short of being full but the protocol itself will still accept deposits of up to 20 ether.
The second condition is met if the attacker can use a flash loan to manipulate the rETH spot price to be 0.1 ether, 10x lower than the current spot price of 1 ether which will result in them getting a far more favourable preDepositPrice
. This is because the ethPerDerivative
function will quote the UniswapV3 pool spot price if the derivative contract balance exceeds the remaining room in the rETH pool, which indeed it does given the prior deposits totalling 90 ether.
The attacker can now deposit up to 20 ether to force usage of the actual rETH
price function which will return approximately 1.06 and is negligible by comparison to the manipulated price above. This mints the attacker approximately 10x more SafEth tokens than should have been received by their deposit which they can then use to withdraw 200 ether from the protocol.
This vulnerability allows an attacker to steal funds from the SafEth protocol with minimal required capital and so the severity is evaluated to HIGH.
Manual review
The SafEth::stake
function should be updated to pass the correct _amount
to IDerivative::ethPerDerivative
which is the amount of ether being deposited. This will ensure that the correct rETH price is used when calculating the number of SafEth tokens to mint to the depositor. It should however be noted that use of spot prices in this manner is inherently risky and should be avoided so as to mitigate against other oracle manipulation attacks if/when a new deposit causes the rETH pool to become full.
#0 - c4-pre-sort
2023-04-04T17:52:07Z
0xSorryNotSorry marked the issue as duplicate of #1004
#1 - c4-judge
2023-04-21T14:06:43Z
Picodes marked the issue as satisfactory