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: 35/246
Findings: 3
Award: $206.07
🌟 Selected for report: 1
🚀 Solo Findings: 0
195.1013 USDC - $195.10
rETH withdraw may revert if not enough ETH are available in the deposit pool making the unstaking impossible.
When staking for rETH, ETH are sent to the deposit pool waiting to be picked for new miniPool validators.
This pool can be empty or not have enough ETH than what's being asked for when calling burn.
In this case the burn function reverts.
rETH code:
// Burn rETH for ETH function burn(uint256 _rethAmount) override external { // Check rETH amount require(_rethAmount > 0, "Invalid token burn amount"); require(balanceOf(msg.sender) >= _rethAmount, "Insufficient rETH balance"); // Get ETH amount uint256 ethAmount = getEthValue(_rethAmount); // Get & check ETH balance uint256 ethBalance = getTotalCollateral(); require(ethBalance >= ethAmount, "Insufficient ETH balance for exchange"); // Update balance & supply _burn(msg.sender, _rethAmount); // Withdraw ETH from deposit pool if required withdrawDepositCollateral(ethAmount); // Transfer ETH to sender msg.sender.transfer(ethAmount); // Emit tokens burned event emit TokensBurned(msg.sender, _rethAmount, ethAmount, block.timestamp); }
Manual review
Consider adding a way to sell on the market like wsteth and sfrxeth derivatives do.
#0 - c4-pre-sort
2023-04-02T09:35:21Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T19:55:22Z
0xSorryNotSorry marked the issue as duplicate of #210
#2 - c4-judge
2023-04-21T16:35:38Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-21T16:36:40Z
Picodes changed the severity to 3 (High Risk)
🌟 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.1759 USDC - $0.18
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L63-L101 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L228-L245 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L170-L183 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/WstEth.sol#L56-L66 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L108-L128 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/SfrxEth.sol#L74-L75
rETH derivative can be bought through uniswap if the deposit contract is not open.
While a maxSlippage variable is set, the price of rETH on uniswap is the spot price and is only determined during the transaction opening sandwich opportunity for MEV researchers as long as the slippage stays below maxSlippage.
This is also true for WstETH (on withdraw) and frxETH (on deposit and withdraw) that go through a curve pool when unstaking (and staking for frxETH). While the curve pool has much more liquidity and the assumed price is a 1 - 1 ratio for WstETH and frxETH seem to be using a twap price before applying the slippage, these attacks are less likely to happen so I will only describe rETH.
While the current rETH derivative contract uses uniswapv3 0,05% pool, I'll be using the uniswapv2 formula (https://amm-calculator.vercel.app/) to make this example simplier, in both case sandwiching is possible.
Default slippage is set to 1% on rETH contract at deployment. (see: https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L44)
Let's take a pool of 10,000 rETH for 10,695 eth (same ratio is on the univ3 0,05% pool on the 26th of march).
User wants to stake 100ETH, a third of it will be staked through rETH according to a 1/3 weight for each derivative.
Bundle:
TX1:
Researcher swap 100 ETH in for 92.63 rETH new pool balance: 9907.36 rETH - 10795 ETH
TX2:
User stake his ETH, the rocketPool deposit contract is close so the deposit function takes the current spot price of the pool and then applies 1% slippage to it to get minOut.
Current ratio: eth = 0.9177 rETH ETH to swap for reth: 33.3333~ So minOut -> 33.3333 * 0.9177 * 0.99 = 30.284 rETH
Contract swap 33.3333 ETH for 30.498 rETH (slippage of 0.61% so below 1% and received more than minOut)
New pool balance: 9876.86 rETH - 10828.33 ETH
TX3:
Researcher swap back the 92.63 rETH in for 100.61~ ETH new pool balance: 9969.49 rETH - 10727.72 ETH
Researcher made 0.61~ ETH of profit, could be more as we only applied a 0,61% slippage but we can go as far as 1% in the current rETH contract.
Univ3 pool would could even worse as Researcher with a lot of liquidity could be able to drain one side (liquidity is very concentrated), add liquidity in a tight range execute the stake and then remove liquidity and swap back.
Manual review + https://amm-calculator.vercel.app/
The rETH price should be determined using the TWAP price and users should be able to input minOut in the stake, unstake and rebalanceToWeight function.
#0 - c4-pre-sort
2023-04-04T11:57:37Z
0xSorryNotSorry marked the issue as duplicate of #601
#1 - c4-judge
2023-04-21T16:11:00Z
Picodes marked the issue as duplicate of #1125
#2 - c4-judge
2023-04-21T16:13:35Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-22T09:32:39Z
Picodes marked the issue as selected for report
#4 - c4-judge
2023-04-22T09:33:18Z
Picodes changed the severity to 3 (High Risk)
🌟 Selected for report: Rolezn
Also found by: 0x3b, 0xGordita, 0xSmartContract, 0xhacksmithh, 0xnev, 0xpanicError, 4lulz, Angry_Mustache_Man, ArbitraryExecution, Aymen0909, Bason, BlueAlder, EvanW, Franfran, HHK, Haipls, IgorZuk, JCN, KrisApostolov, Madalad, MiksuJak, MiniGlome, RaymondFam, ReyAdmirado, Rickard, Sathish9098, Udsen, adriro, alexzoid, anodaram, arialblack14, c3phas, carlitox477, ch0bu, chaduke, codeslide, d3e4, dicethedev, ernestognw, fatherOfBlocks, georgits, hunter_w3b, inmarelibero, lukris02, mahdirostami, maxper, pavankv, pixpi, rotcivegaf, smaul, tank, tnevler, wen, yac
10.7864 USDC - $10.79
require(pauseStaking == false, "staking is paused");
if(pauseStaking == false) revert StakingPaused();
if (weight == 0) continue
should be one line aboveThis would save a sload as we wouldn't need to load IDerivative derivative = derivatives[i];
when weight == 0
Declaring a memory d = derivativeCount and then using it in the loop would save gas as each loop will mload instead of sload. Also because derivativeCount is used 2 times in this function it would save an extra sload to declare d at the beginning.
Multiply by 1e18 and then instantly divide by 1e18 is a waste of gas.
localTotalWeight
should be calculated at the beginningCalculate localTotalWeight
first and then add _weight
to get the new totalWeight, this will save one loop and one sload.
ethAmountToRebalance == 0
is uselessIt should be checked before the loop and return or not checked at all since there is low chance this function gets called if there is no eth to rebalance.
#0 - c4-sponsor
2023-04-10T20:11:45Z
elmutt marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-23T19:13:19Z
Picodes marked the issue as grade-b