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: 150/246
Findings: 3
Award: $16.76
🌟 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
An initial depositor can attack the protocol in a way that inflates the perDepositPrice
for future stakers.
This is a variation of the classic first depositor attack. In this case, the attacker can manipulate the underlyingValue
to inflate the perDepositPrice
. This allows the attacker to steal funds from future stakers.
First, the attacker makes a minAmount
deposit into the protocol, receiving safEth in return. Then, the attacker immediately withdraws all but 1 wei of safEth, this leaves the totalSupply
of safEth as 1 wei.
A non-zero totalSupply
will cause future calls to stake()
to be calculated using the underlyingValue
instead of having an initial price of 1 ether.
if (totalSupply == 0) preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
The attacker then sends a tiny amount of Reth, SfrxEth, and WstEth to the address of the corresponding derivatives. Directly altering the balance
of the derivatives will cause the underlyingValue
to increase, but the totalSupply
of safEth will remain at 1 wei. This will cause the perDepositPrice
to be greatly inflated.
For example, by making the derivatives balance
for Reth, SfrxEth, and WstEth each equal to 2 wei, the underlyingValue
will be ~6 wei.
uint256 underlyingValue = 0; // Getting underlying value in terms of ETH for each derivative for (uint i = 0; i < derivativeCount; i++) // @audit - Underlying value can be me manipulated by sending tokens directly to the derivate contracts underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
So, we now have ~6 wei of underlyingValue
, and 1 wei totalSupply
of safEth. This causes the next call to stake()
to set the preDepositPrice
as 6 ether.
preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
This results in safEth being priced 6x higher than it should be for the next staker. Their transaction can then be backrun by the attacker, staking and withdrwaing, to effectively steal funds from the victim.
Manual review
During contract creation, make an initial deposit that is large enough to prevent large swings in the perDepositPrice
. This initial deposit should remain locked in the vault.
#0 - c4-pre-sort
2023-04-04T12:48:32Z
0xSorryNotSorry marked the issue as duplicate of #715
#1 - c4-judge
2023-04-21T14:57:07Z
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/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L170-L185 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L228-L242
When a deposit is made the Reth derivative contract, if the amount is larger than the pool can hold, a swap is made using a Uniswap pool. minOut
is determined using the spot price for the pool.
AMM spot prices can be manipulated by attackers and should not be used directly for price discovery. This results in a frontrunning vulnerability where higher slippage can occur and fewer Reth tokens are held in the safEth contract than expected.
When a deposit is made to the the Reth derivative contract, if there is not room in the pool for the amount deposited, a Uniswap swap is made to acquire Reth.
if (!poolCanDeposit(msg.value)) { uint rethPerEth = (10 ** 36) / poolPrice(); uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) * ((10 ** 18 - maxSlippage))) / 10 ** 18); IWETH(W_ETH_ADDRESS).deposit{value: msg.value}(); uint256 amountSwapped = swapExactInputSingleHop( W_ETH_ADDRESS, rethAddress(), 500, msg.value, minOut ); return amountSwapped; }
There are safeguards in place that calculate a minOut
value that is used to prevent slippage from exceeding maxSlippage
, however, This value is determined using poolPrice()
which is calculated using the spot price of the pool.
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
An attacker can frontrun the deposit, taking out a large flash loan for ETH and swapping it for Reth via the Uniswap pool. This trade will increase the price of Reth because of the increased demand.
An increased price will result in a lower minOut
value, which will result in more slippage than expected when the swap is made and fewer tokens being held in the safEth contract than expected.
Manual review
Use a more robust method for determining the poolPrice. A secure price calculation can be performed by using time-weighted average prices (TWAPs) across longer time intervals.
uint32 _twapDuration = twapDuration; // Duration configured and stored in contract uint32[] memory secondsAgo = new uint32[](2); secondsAgo[0] = _twapDuration; secondsAgo[1] = 0; (int56[] memory tickCumulatives, ) = pool.observe(secondsAgo); return uint256((tickCumulatives[1] - tickCumulatives[0]) / _twapDuration);
#0 - c4-pre-sort
2023-04-04T11:55:56Z
0xSorryNotSorry marked the issue as duplicate of #601
#1 - c4-judge
2023-04-21T16:11:04Z
Picodes marked the issue as duplicate of #1125
#2 - c4-judge
2023-04-21T16:13:38Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-24T21:46:36Z
Picodes changed the severity to 3 (High Risk)
🌟 Selected for report: brgltd
Also found by: 0x3b, 0xAgro, 0xGusMcCrae, 0xNorman, 0xRajkumar, 0xSmartContract, 0xTraub, 0xWagmi, 0xWaitress, 0xffchain, 0xhacksmithh, 0xkazim, 0xnev, 3dgeville, ArbitraryExecution, Aymen0909, BRONZEDISC, Bason, Bloqarl, BlueAlder, Brenzee, CodeFoxInc, CodingNameKiki, Cryptor, DadeKuma, DevABDee, Diana, Dug, Englave, Gde, Haipls, HollaDieWaldfee, Ignite, Infect3d, Jerry0x, Josiah, Kaysoft, Koko1912, KrisApostolov, Lavishq, LeoGold, Madalad, PNS, Rappie, RaymondFam, RedTiger, Rickard, Rolezn, Sathish9098, SunSec, T1MOH, UdarTeam, Udsen, Viktor_Cortess, Wander, adriro, ak1, alejandrocovrr, alexzoid, arialblack14, ayden, bin2chen, brevis, btk, c3phas, carlitox477, catellatech, ch0bu, chaduke, ck, climber2002, codeslide, descharre, dingo2077, ernestognw, fatherOfBlocks, favelanky, georgits, helios, hl_, inmarelibero, juancito, ks__xxxxx, lopotras, lukris02, m_Rassska, mahdirostami, maxper, nadin, navinavu, nemveer, p_crypt0, peanuts, pipoca, pixpi, qpzm, rbserver, reassor, roelio, rotcivegaf, scokaf, siddhpurakaran, slvDev, smaul, tnevler, tsvetanovv, turvy_fuzz, vagrant, wen, yac, zzzitron
13.1298 USDC - $13.13
Unstaked
event data can be manipulatedWhen the unstake()
function is called, withdraw()
is called on each derivative. At the end of each, the derivative sends its full ether balance to the SafEth
contract.
(bool sent, ) = address(msg.sender).call{value: address(this).balance}( "" );
After receiving ether from each derivative, unstake()
sends the total received ether to the user and then emits the Unstaked
event.
emit Unstaked(msg.sender, ethAmountToWithdraw, _safEthAmount);
Because of how this works, a user could send ether to a derivative contract ahead of calling unstake(). This would cause the derivative to send more ether to the SafEth
contract than it normally would as it would include the amount just sent to the derivative.
The funds would ultimately be returned to the user and the Unstaked
event would store this manipulated ethAmountToWithdraw
amount instead of the amount that was actually related to the unstaking of safETH.
As a result, event data and any dependent reporting/analytics would be compromised.
stake()
is called before the first derivative addedThe stake()
function will successfully execute even if no derivatives are added. This means that a user could stake safETH and lose all of their funds sent with the call.
A check should be added that causes the call to revert if mintAmount
is 0
.
minAmount
and maxAmount
are inconsequentialIn code documentation describes the minAmount
and maxAmount
parameters as the minimum and maximum amount a user is allowed to stake. However, a user can easily bypass these checks.
Th max amount check is bypassed by simply calling stake()
more than once.
The min amount check is bypassed by staking the minAmount
and then calling unstake()
to withdraw a partial balance.
Ownable2StepUpgradeable
instead of OwnableUpgradable
to prevent loss of admin functionalityThe SafeEth
contract currently uses OwnableUpgradeable
to manage admin functionality. This contract is vulnerable to a loss of admin functionality if transferOwnership()
is called with the incorrect address.
It is recommended to use the Ownable2StepUpgradeable
contract instead. This contract requires the new owner to accept the ownership transfer before it can be completed. This prevents the loss of functionality if the incorrect address is used.
Generally, it is a good practice to emit events for all state changes. This allows interested parties to easily track changes to the state of the contract.
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol
IDerivative
should include events that are emitted by all derivatives.
#0 - c4-sponsor
2023-04-10T20:15:26Z
elmutt marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T18:43:35Z
Picodes marked the issue as grade-b