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: 18/246
Findings: 7
Award: $447.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L72-L75 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L211-L216 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L146-L149
When user stake in SafETH
, ETH will be deposited into RETH. Due to an incorrect calculation for underlyingValue from ethForDerivative for RETH, user can overpay or underpay, causing loss of funds to either user or SafETH.
In stake
for SafETH
, underlyingValue of each Derivative, including RETH is derived from ethPerDerivative
which gets the Derivative worth in terms of ETH.
underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
However for ethPerDerivative
in RETH, notice that poolCanDeposit
is called with the derivative balance, the amount staked by SafETH
in RETH
. This is incorrect as it should be the amount of ETH that is staked by user into RETH for the stake
call.
function ethPerDerivative(uint256 _amount) public view returns (uint256) { if (poolCanDeposit(_amount)) return RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); else return (poolPrice() * 10 ** 18) / (10 ** 18); }
In poolCanDeposit
, a lower or higher amount compared to user staked ETH into RETH can cause the incorrect pool price to be used, either Rocket Pool or Rocket Token UniV3Pool, causing user to either underpay or overpay ETH when staking in RETH.
return rocketDepositPool.getBalance() + _amount <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() && _amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();
Manual Review
Recommend using user staked amount in RETH when deriving underlyingValue for RETH.
#0 - c4-pre-sort
2023-04-01T10:57:17Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:40:32Z
0xSorryNotSorry marked the issue as duplicate of #1004
#2 - c4-judge
2023-04-21T14:03:45Z
Picodes marked the issue as duplicate of #1125
#3 - c4-judge
2023-04-21T14:20:35Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-04-21T14:23:22Z
Picodes marked the issue as not a duplicate
#5 - c4-judge
2023-04-21T14:23:36Z
Picodes marked the issue as duplicate of #1004
🌟 Selected for report: rbserver
Also found by: 0xAgro, DadeKuma, DeStinE21, HollaDieWaldfee, IgorZuk, J4de, P7N8ZK, Parad0x, Stiglitz, bytes032, carrotsmuggler, csanuragjain, dec3ntraliz3d, kaden, koxuan, lukris02, rvierdiiev, tnevler
58.9366 USDC - $58.94
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L113-L119
In SafETH, unstake will withdraw ETH from all derivatives for user. There is no removeDerivative function. In the event that any derivative were to fail, all funds in SafETH will be stuck as no stakers can unstake in SafETH
.
In unstake
of SafETH
, notice that every derivative will be withdrawn to add up to the user specified amount of ETH. In the event that any of the derivative fails, all funds in SafETH
will be stuck.
for (uint256 i = 0; i < derivativeCount; i++) { // withdraw a percentage of each asset based on the amount of safETH uint256 derivativeAmount = (derivatives[i].balance() * _safEthAmount) / safEthTotalSupply; if (derivativeAmount == 0) continue; // if derivative empty ignore derivatives[i].withdraw(derivativeAmount); }
Manual Review
Recommend implementing a removeDerivative
function to remove any derivative that is failing to prevent all funds in SafETH
to be stuck for stakers.
#0 - c4-pre-sort
2023-04-03T12:59:04Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:33:35Z
0xSorryNotSorry marked the issue as duplicate of #703
#2 - c4-judge
2023-04-21T15:07:08Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-24T19:36:09Z
Picodes changed the severity to 3 (High Risk)
236.4864 USDC - $236.49
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L72-L75 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111-L117
When staking , ethPerDerivative for SfrxETH is part of the sum that determines the price of the safETH tokens. However, due to an incorrect computation in ethPerDerivative
, the returned sum will be higher and hence price of safETH tokens will be higher than it should be, causing loss of funds to user as they receive lesser tokens in return.
In staking
of safETH
, each derivative will call ethPerDerivative to get the value of the derivative in ETH value. SfrxETH
is on of the derivative.
underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
However, in SfrxETH
, notice return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
. This is incorrect as price_oracle
returns how much 1 frxETH is worth in terms of ETH. This can be confirmed by going to https://curve.fi/#/ethereum/swap and seeing the exchange rate of 1 frxETH to ETH. See price_oracle
return value.
Even though frxETH currently worths less than ETH, the computation will cause ethPerDerivative for SfrxETH to be more than frxETH amount. Even if frxETH suddenly becomes worth more than ETH, the problem will flip to become ethPerDerivative for SfrxETH is less than frxETH amount.
function ethPerDerivative(uint256 _amount) public view returns (uint256) { uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets( 10 ** 18 ); return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle()); }
Manual Review
Recommend using get_virtual_price
that gets the worth of 1 ETH in terms of frxETH. See get_virtual_price
return value.
return ((10 ** 18 * frxAmount) / (10**18/IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).get_virtual_price()));
#0 - c4-pre-sort
2023-04-04T16:49:24Z
0xSorryNotSorry marked the issue as duplicate of #698
#1 - c4-pre-sort
2023-04-04T16:59:40Z
0xSorryNotSorry marked the issue as duplicate of #141
#2 - c4-pre-sort
2023-04-04T17:00:17Z
0xSorryNotSorry marked the issue as duplicate of #698
#3 - c4-judge
2023-04-21T16:04:57Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-04-21T16:05:07Z
Picodes marked the issue as not a duplicate
#5 - c4-judge
2023-04-21T16:05:19Z
Picodes marked the issue as duplicate of #641
🌟 Selected for report: adriro
Also found by: 0xMirce, 0xRajkumar, 0xepley, BPZ, Bahurum, Bauer, Co0nan, Emmanuel, Franfran, HollaDieWaldfee, IgorZuk, MiloTruck, NoamYakov, RedTiger, Ruhum, T1MOH, Tricko, ad3sh_, auditor0517, bin2chen, carrotsmuggler, eyexploit, handsomegiraffe, igingu, jasonxiale, koxuan, lukris02, monrel, nadin, peanuts, rbserver, rvierdiiev, shaka, sinarette, tnevler, y1cunhui
4.5426 USDC - $4.54
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L86-L88 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L72-L75 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L91-L95
In WstEth
ethPerDerivative, WstETH.getStETHByWstETH
is used to derive the derivative worth in terms of stETH. This is incorrect as the current price of stETH in terms of ETH fluctuates between 0.98 to 1.03 around exchanges, and depegging can always take place which causes the incorrect ETH value of WstEth
to be reported. Using stETH as a 1 is to 1 equivalence to ETH will cause user to either overpay or underpay when staking, resulting in loss of funds to either user or SafETH
.
In ethPerDerivative
for WstEth
, notice WstETH.getStETHByWstETH
is used to derive the derivative worth in terms of stETH.
function ethPerDerivative(uint256 _amount) public view returns (uint256) { return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18); }
When staking, the incorrect underlying value will be derived for WstEeth
.
underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
Same for user staked amount of ETH in RETH.
uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue;
Manual Review
Recommend using Chainlink oracle to derive the price for stETH to ETH and computing the total ETH value of WstEth
using the oracle price.
#0 - c4-pre-sort
2023-04-01T10:51:10Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:11:07Z
0xSorryNotSorry marked the issue as duplicate of #588
#2 - c4-judge
2023-04-22T09:06:48Z
Picodes marked the issue as partial-50
#3 - Picodes
2023-04-22T09:06:54Z
Unclear impact
#4 - c4-judge
2023-04-24T20:46:29Z
Picodes marked the issue as satisfactory
🌟 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/main/contracts/SafEth/SafEth.sol#L70-L81 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L211-L216 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L228-L242
When staking in Rocket Pool and Rocket Pool cannot deposit, UniV3Pool will be used to swap ETH to RETH. However, the ethPerDerivative uses PoolPrice
, which uses UniV3Pool.slot0
which is susceptible to price manipulation with flash loan attacks. An attacker can crash Rocket Tokens UniV3Pool with flash loan and gain more SafETH Tokens before swapping back and returning the flash loan, allowing attacker to steal from SafETH.
In stake
of SafETH
, ethPerDerivative
is used to get the eth amount worth of each derivative. underlyingValue
is later divided by total supply to get the price of SafETH token.
// 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;
When we look into ethPerDerivative
for Reth
, if Rocket Pool cannot be deposited, poolPrice()
will be used to derive the eth worth of Rocket Pool Derivative.
function ethPerDerivative(uint256 _amount) public view returns (uint256) { if (poolCanDeposit(_amount)) return RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); else return (poolPrice() * 10 ** 18) / (10 ** 18); }
The issue comes in poolPrice
, notice that UniV3Pool.slot0
is used to derive the price for Rocket Tokens. This is susceptible to price manipulation by attackers as described.
Attacker flash loans a huge amount of Rocket Tokens, selling them in UniV3Pool of Rocket Tokens in exchange for ETH, crashing the price of it in the process.
Attacker uses the swapped ETH to stake in SafETH
. Since prices of RETH have been crashed, underlyingValue
will be lower. Price of SafETH
token will hence be lower. User's ETH can exchange more SafETH
and hence more will be minted. Note that staking ETH will rebalance the Rocket Token UniV3Pool.
SafETH
tokens can be unstaked and since staking ETH in step 2 will rebalance back UniV3Pool, user will gain more ETH. It can be argued that only a proportion will be deposited into RETH but the distribution is not known and hence a large proportion can deposited into Reth. Another way to gain is that SafETH
tokens can be exchanged for ETH through liquidity pools. ETH can be returned to flash loan and attacker can gain the remaining ETH that they stole from SafETH
.
function poolPrice() private view returns (uint256) { address rocketTokenRETHAddress = RocketStorageInterface( ROCKET_STORAGE_ADDRESS ).getAddress( keccak256( abi.encodePacked("contract.address", "rocketTokenRETH") ) ); IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY); IUniswapV3Pool pool = IUniswapV3Pool( factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500) ); (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2); }
Manual Review
Recommend using twap oracle to get the price instead of UniV3Pool.slot0
.
#0 - c4-pre-sort
2023-04-04T11:46:06Z
0xSorryNotSorry marked the issue as duplicate of #601
#1 - c4-judge
2023-04-21T16:11:02Z
Picodes marked the issue as duplicate of #1125
#2 - c4-judge
2023-04-21T16:13:57Z
Picodes marked the issue as satisfactory
🌟 Selected for report: ladboy233
Also found by: 0xkazim, 0xnev, Bauer, J4de, Matin, UniversalCrypto, cryptothemex, jasonxiale, juancito, koxuan, latt1ce, neumo
48.6252 USDC - $48.63
When withdrawing from SfrxEth, slippage control is put in place when swapping sFrxEth for Eth. However, due to division before multiplication when calculating minOut, slippage control can be lower than intended, causing slippage that is more than expected.
In withdraw
of SfrxEth
minOut act as the slippage control. Notice that multiplication before division is done which can cause minOut to be lower than intended. This can cause slippage to be more than expected.
uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) * (10 ** 18 - maxSlippage)) / 10 ** 18; IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange( 1, 0, frxEthBalance, minOut );
Note: This also applies to deposit
in Reth
.
uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) * ((10 ** 18 - maxSlippage))) / 10 ** 18);
Manual Review
Recommend multiplying before dividing.
uint256 minOut = ethPerDerivative(_amount) * _amount * (10 ** 18 - maxSlippage) / 10 ** 18 / 10 ** 18;
#0 - c4-pre-sort
2023-04-04T16:42:17Z
0xSorryNotSorry marked the issue as duplicate of #1044
#1 - c4-judge
2023-04-22T10:34:09Z
Picodes marked the issue as satisfactory
🌟 Selected for report: RaymondFam
Also found by: 7siech, LeoGold, Phantasmagoria, SunSec, adeolu, anodaram, aviggiano, chaduke, d3e4, eyexploit, jasonxiale, juancito, koxuan, mojito_auditor, n33k, neumo, rotcivegaf, yac
17.681 USDC - $17.68
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88-L91
When staking, msg.value will be divided based on the weights of each derivative before depositing into them. However, due to a rounding down error, there will be leftover ETH in SafEth
which will not be refunded, causing loss of funds to user.
In stake
, the amount to deposit into derivative (ethAmount) is computed with uint256 ethAmount = (msg.value * weight) / totalWeight;
. Notice that rounding down error will cause leftovers in SafETH
. There is no refund mechanism implemented to return user their leftovers, causing loss of funds to user.
uint256 ethAmount = (msg.value * weight) / totalWeight; // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}();
Manual Review
Recommend sending back leftovers to users.
uint256 ethAmountToRefund = address(this).balance; (bool sent, ) = address(msg.sender).call{value: ethAmountToRefund}( "" ); require(sent, "Failed to refund Ether");
#0 - c4-pre-sort
2023-04-01T13:36:20Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T16:22:39Z
0xSorryNotSorry marked the issue as duplicate of #455
#2 - c4-judge
2023-04-24T21:23:33Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-24T21:42:33Z
Picodes changed the severity to 2 (Med Risk)