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: 22/246
Findings: 5
Award: $298.07
๐ Selected for report: 0
๐ Solo Findings: 0
236.4864 USDC - $236.49
Due to a miscalculation of the price of derivative in terms of ETH, resulting in users getting more share, and the protocol will lose money
In the SfrxEth
contract, the price of derivative in terms of ETH is calculated as below code.This calculation is wrong.
frxAmount:1031241986383403960
price_oracle:998817873681710532
As below code,the price of derivative in terms of ETH is 10**18*1031241986383403960/998817873681710532 = 1032462487462479700.
Actually๏ผthe IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle()
function returns the value of Eth per FrxETH.
Hence,it should be calculated like this: frxAmount *IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle()/1e18 = 1030022928090775200,it will be 2439559371704576 less than the original value.
Hence, the protocol will lose assets and user will get more shares when staking ETH into the SafETH
contract.
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()); }
vscode
function ethPerDerivative(uint256 _amount) public view returns (uint256) { uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets( 10 ** 18 ); return frxAmount * IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle()/1e18; }
#0 - c4-pre-sort
2023-04-04T16:48:31Z
0xSorryNotSorry marked the issue as duplicate of #698
#1 - c4-judge
2023-04-21T16:00:40Z
Picodes marked the issue as not a duplicate
#2 - c4-judge
2023-04-21T16:00:51Z
Picodes marked the issue as duplicate of #641
#3 - c4-judge
2023-04-21T16:01:43Z
Picodes marked the issue as satisfactory
๐ 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
The the minimum output of ETH is calculated incorrectly and could lead to sandwich attack.
The SafETH.unstake()
is used to unstake safETH into ETH. When it is called, the protocol will withdraw shares from third-party contracts and finally swap the tokens to ETH. In the WstETH
contract, the swap is performed on lido CRV pool. The protocol set max slippage for derivative. However,in the WstETH.withdraw()
function, the protocol calculates the minimum output of stETH not ETH.We know that the price of stETH is lowwer then the price of ETH,the the minimum output of ETH is lower which may lead to sandwich attack.
function withdraw(uint256 _amount) external onlyOwner { IWStETH(WST_ETH).unwrap(_amount); uint256 stEthBal = IERC20(STETH_TOKEN).balanceOf(address(this)); IERC20(STETH_TOKEN).approve(LIDO_CRV_POOL, stEthBal); uint256 minOut = (stEthBal * (10 ** 18 - maxSlippage)) / 10 ** 18; IStEthEthPool(LIDO_CRV_POOL).exchange(1, 0, stEthBal, minOut); // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: address(this).balance}( "" ); require(sent, "Failed to send Ether"); }
vscode
add code like this
uint256 minOut = (stEthBal*EthPricePerstETH * (10 ** 18 - maxSlippage)) / 10 ** 18;
#0 - c4-pre-sort
2023-04-04T17:10:40Z
0xSorryNotSorry marked the issue as duplicate of #588
#1 - c4-judge
2023-04-22T09:06:01Z
Picodes marked the issue as partial-50
#2 - Picodes
2023-04-22T09:06:29Z
Impact is partial: the risk is not only sandwich attack but more importantly DoS
#3 - c4-judge
2023-04-24T20:47:19Z
Picodes marked the issue as not a duplicate
#4 - c4-judge
2023-04-24T20:47:37Z
Picodes marked the issue as duplicate of #588
#5 - c4-judge
2023-04-24T21:25:35Z
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/derivatives/Reth.sol#L240
Reth.poolPrice()
uses the UniV3Pool.slot0
to get the price of derivative in liquidity pool. UniV3Pool.slot0
is the most recent data point and is therefore extremely easy to manipulate.
Reth.poolPrice()
the UniV3Pool.slot0 to get the price of derivative in liquidity pool. UniV3Pool.slot0
is the most recent data point and can easily be manipulated.
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); }
The Reth.deposit()
and Reth.ethPerDerivative()
functions directly uses the token values returned by poolPrice()
. This allows a malicious user to manipulate the valuation of the LP. An example of this kind of manipulation would be to use large buys/sells to alter the composition of the LP to make it worth less or more.This allows a malicious user to manipulate the rethPerEth
and ethPerDerivative
. An example of this kind of manipulation would be to use large buys/sells to make it worth less or more.
vscode
Use a TWAP instead of slot0.
#0 - c4-pre-sort
2023-04-04T11:45:04Z
0xSorryNotSorry marked the issue as duplicate of #601
#1 - c4-judge
2023-04-21T16:11:12Z
Picodes marked the issue as duplicate of #1125
#2 - c4-judge
2023-04-21T16:14:03Z
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
Calculate the minOut
like this can reduce unnecessary precision loss and save gas cost.
In the Reth
contract,the protocol will swap weth
to reth if the condition !poolCanDeposit(msg.value)
is met. The swap is performed on uniswap v3, the minOut
is calculated according to code below.Actually, poolPrice()
is the price of eth per reth. Hence ,the minOut
can be calculated like this:uint256 minOut = (((msg.value * 10 ** 18 / poolPrice()) * ((10 ** 18 - maxSlippage))) / 10 ** 18)
This can reduce unnecessary precision loss and save gas cost.
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;
vscode
uint256 minOut = (((msg.value * 10 ** 18/poolPrice()) * ((10 ** 18 - maxSlippage))) / 10 ** 18)
#0 - c4-pre-sort
2023-04-02T19:19:12Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:45:09Z
0xSorryNotSorry marked the issue as duplicate of #1044
#2 - c4-judge
2023-04-22T10:32:01Z
Picodes marked the issue as satisfactory
๐ Selected for report: __141345__
Also found by: 0xWaitress, 0xbepresent, AkshaySrivastav, Bauer, Haipls, HollaDieWaldfee, Lirios, MiloTruck, SaeedAlipoor01988, UdarTeam, adriro, bytes032, ck, d3e4, hihen, hl_, kaden, ladboy233, lopotras, m_Rassska, peanuts, reassor, volodya
8.2654 USDC - $8.27
The frxETHMinter
contract can be paused which block staking
When user stakes ETH into the SafEth contract, the protocol will deposit the ETH into the FrxETHMinter
contract. However,in FrxETHMinter
contract the deposit can be paused.
SfrxEth.sol function deposit() external payable onlyOwner returns (uint256) { IFrxETHMinter frxETHMinterContract = IFrxETHMinter( FRX_ETH_MINTER_ADDRESS ); uint256 sfrxBalancePre = IERC20(SFRX_ETH_ADDRESS).balanceOf( address(this) ); frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this)); uint256 sfrxBalancePost = IERC20(SFRX_ETH_ADDRESS).balanceOf( address(this) ); return sfrxBalancePost - sfrxBalancePre; }
In frxETHMinter
contract:
https://etherscan.io/address/0xbafa44efe7901e04e39dad13167d089c559c1138#code#F1#L87
function submitAndDeposit(address recipient) external payable returns (uint256 shares) { // Give the frxETH to this contract after it is generated _submit(address(this)); // Approve frxETH to sfrxETH for staking frxETHToken.approve(address(sfrxETHToken), msg.value); // Deposit the frxETH and give the generated sfrxETH to the final recipient uint256 sfrxeth_recieved = sfrxETHToken.deposit(msg.value, recipient); require(sfrxeth_recieved > 0, 'No sfrxETH was returned'); return sfrxeth_recieved; } /// @notice Mint frxETH to the recipient using sender's funds. Internal portion function _submit(address recipient) internal nonReentrant { // Initial pause and value checks require(!submitPaused, "Submit is paused"); require(msg.value != 0, "Cannot submit 0"); // Give the sender frxETH frxETHToken.minter_mint(recipient, msg.value);
Note the require in the _submit()
function.
If paused, swap the ETH to sfrxEth ,just like the reth contract.
#0 - c4-pre-sort
2023-04-04T22:03:24Z
0xSorryNotSorry marked the issue as duplicate of #328
#1 - c4-judge
2023-04-21T10:47:14Z
Picodes marked the issue as duplicate of #770
#2 - c4-judge
2023-04-24T18:27:54Z
Picodes marked the issue as satisfactory