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: 88/246
Findings: 2
Award: $50.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
Function unstake
of SafEth.sol
iterates around each derivative to withdraw a percentage of each asset based on the amount of safETH (see code extract below).
In the case where a single loop is not able to be completed (e.g. because a certain derivative contract is paused, encoutering issues, etc.), the entire unstake
function will fail and the user will not be able to withdraw any amount from any of the derivatives. This causes the funds of users to be locked in the contract, and in the possible case where the issues in a single derivative contract cannot be resolved - all funds are lost.
See abovementioned details - code extract as follows:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol (L113 - 119) 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.
Consider breaking up the loop so that even if a single derivative contract is encountering issues, withdrawals to other derivatives in the appropriate percentages can still be made. This limits any potential loss of funds in relation to this dependency.
#0 - c4-pre-sort
2023-04-03T10:38:23Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T20:21:43Z
0xSorryNotSorry marked the issue as duplicate of #770
#2 - c4-judge
2023-04-24T18:29:43Z
Picodes marked the issue as satisfactory
🌟 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
42.0604 USDC - $42.06
RebalanceToWeight
may revertstake
and unstake
may revertIn SafEth.sol
, there is no function available to remove a derivative. There should be such a function available in case a derivative should be removed.
In general, function unstake
should not have a pause modifier. Users should always be able to withdraw their funds.
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol 108: function unstake(uint256 _safEthAmount) external { 109: require(pauseUnstaking == false, "unstaking is paused");
RebalanceToWeight
may revertFunction RebalanceToWeight
in SafEth.sol
withdraws all derivatives and re-deposits them to have the correct weights. When it comes to a point where there are many derivatives added and a huge number of users, the function may revert due to reaching maximum gas limit. Consider breaking up the loop into smaller loops so that each execution can be successful.
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol (L138-155) function rebalanceToWeights() external onlyOwner { uint256 ethAmountBefore = address(this).balance; for (uint i = 0; i < derivativeCount; i++) { if (derivatives[i].balance() > 0) derivatives[i].withdraw(derivatives[i].balance()); } uint256 ethAmountAfter = address(this).balance; uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore; for (uint i = 0; i < derivativeCount; i++) { if (weights[i] == 0 || ethAmountToRebalance == 0) continue; uint256 ethAmount = (ethAmountToRebalance * weights[i]) / totalWeight; // Price will change due to slippage derivatives[i].deposit{value: ethAmount}(); } emit Rebalanced(); }
stake
and unstake
may revertSimilar to [L-03] above, both functions stake
and unstake
of SafEth.sol
iterates over the derivatives to determine the underlyingValue and derivativeAmount respectively. The function may revert due to maximum gas limit reached if there are eventually a large number of derivatives added. Consider breaking up the loop into smaller loops so that each execution can be successful.
Function stake
:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol (L63-75) function stake() external payable { require(pauseStaking == false, "staking is paused"); require(msg.value >= minAmount, "amount too low"); require(msg.value <= maxAmount, "amount too high"); 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;
Function unstake
:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol (L108-119) function unstake(uint256 _safEthAmount) external { require(pauseUnstaking == false, "unstaking is paused"); uint256 safEthTotalSupply = totalSupply(); uint256 ethAmountBefore = address(this).balance; 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); }
In function unstake
of SafEth.sol
, include a check to ensure user has sufficient funds for unstaking to minimize gas loss when the function reverts due to insufficient funds.
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol (L108-129) function unstake(uint256 _safEthAmount) external { require(pauseUnstaking == false, "unstaking is paused"); uint256 safEthTotalSupply = totalSupply(); uint256 ethAmountBefore = address(this).balance; 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); } _burn(msg.sender, _safEthAmount); uint256 ethAmountAfter = address(this).balance; uint256 ethAmountToWithdraw = ethAmountAfter - ethAmountBefore; // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}( "" ); require(sent, "Failed to send Ether"); emit Unstaked(msg.sender, ethAmountToWithdraw, _safEthAmount); }
The test coverage rate of the project is 92%. Testing all functions is best practice in terms of security criteria.
While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.
Using import declarations of the form import {<identifier_name>} from "some/file.sol"
avoids polluting the symbol namespace making flattened files smaller, and speeds up compilation.
For example:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol 4: import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5: import "../interfaces/IWETH.sol"; 6: import "../interfaces/uniswap/ISwapRouter.sol"; 7: import "../interfaces/lido/IWStETH.sol"; 8: import "../interfaces/lido/IstETH.sol"; 9: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 10: import "./SafEthStorage.sol"; 11: import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
Old version of solidity is used, consider using the new one 0.8.17. You can see what new versions offer regarding bug fixed here.
For example:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol 2: pragma solidity ^0.8.13;
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
For example:
File: 2023-03-asymmetry/contracts/SafEth/SafEth.sol 2: pragma solidity ^0.8.13;
In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation.
#0 - c4-sponsor
2023-04-10T18:20:14Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-24T17:36:56Z
Picodes marked the issue as grade-b
#2 - c4-judge
2023-04-24T17:37:02Z
Picodes marked the issue as grade-a