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: 71/246
Findings: 2
Award: $72.07
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: 0xAgro, DadeKuma, DeStinE21, HollaDieWaldfee, IgorZuk, J4de, P7N8ZK, Parad0x, Stiglitz, bytes032, carrotsmuggler, csanuragjain, dec3ntraliz3d, kaden, koxuan, lukris02, rvierdiiev, tnevler
58.9366 USDC - $58.94
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L113-L119
An attacker can permanently DoS SafEth
by dusting each derivative.
This can happen when the total number of derivatives increases (including derivatives with zero weight).
In this way it's possible to permanently lock some contract functions, including unstake
, leading to irrecoverable funds.
In SafEth
is possible to add new derivatives, but it's not possible to remove them (it's possible to disable them by setting their weight to zero):
function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { derivatives[derivativeCount] = IDerivative(_contractAddress); weights[derivativeCount] = _weight; derivativeCount++; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight; emit DerivativeAdded(_contractAddress, _weight, derivativeCount); }
If a derivative is disabled, SafEth
will still loop through it, for example in unstake
:
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); }
This is limited by the fact that if derivativeAmount
is zero, unstake
doesn't compute withdraw
.
However, considering the following:
derivativeCount
can only grow but not shrink, and can reach a point where this loop will run out of gasderivative
has a custom, costly withdraw
logic, with an average gas cost of 516432
1 wei
to each derivative (even disabled ones) to force a full computation for all derivatives, because derivativeAmount
will be non-zeroThe attacker can permanently DoS the unstake
and rebalanceToWeights
functions, because those transactions will run out of gas.
Manual review
Disabling a derivative by setting the weight to zero it's not enough to prevent this attack.
Consider adding a way to permanently remove a derivative
to shrink the total derivativeCount
.
#0 - c4-pre-sort
2023-04-04T17:33:04Z
0xSorryNotSorry marked the issue as duplicate of #703
#1 - c4-judge
2023-04-21T15:06:38Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-04-24T19:36:09Z
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
Id | Title | Instances |
---|---|---|
[L-01] | No limits to max slippage | 1 |
[L-02] | Missing safe limits in setMinAmount and setMaxAmount | 1 |
[L-03] | Owner can renounce ownership | 17 |
[L-04] | Lack of two-step Ownership | 4 |
Total: 23 instances over 4 issues.
Id | Title | Instances |
---|---|---|
[NC-01] | Static keccak256 computed more than once | 1 |
[NC-02] | Parameter omission in events | 1 |
[NC-03] | Some functions don't follow the Solidity naming conventions | 4 |
[NC-04] | Use of floating pragma | 27 |
[NC-05] | Missing or incomplete NatSpec | 7 |
Total: 40 instances over 5 issues.
A slippage set to 100% could result in massive loss of funds due to frontrunning. Consider setting a max limit to avoid this risk.
There is 1 instance of this issue.
File: contracts/SafEth/SafEth.sol 202: function setMaxSlippage( 203: uint _derivativeIndex, 204: uint _slippage 205: ) external onlyOwner { 206: derivatives[_derivativeIndex].setMaxSlippage(_slippage); 207: emit SetMaxSlippage(_derivativeIndex, _slippage); 208: }
setMinAmount
and setMaxAmount
The minAmount
should never be greater than maxAmount
, as it would lock the stake
function. Consider adding some safechecks to those setters.
There is 1 instance of this issue.
File: contracts/SafEth/SafEth.sol 214: function setMinAmount(uint256 _minAmount) external onlyOwner { 215: minAmount = _minAmount; 216: emit ChangeMinAmount(minAmount); 217: } 223: function setMaxAmount(uint256 _maxAmount) external onlyOwner { 224: maxAmount = _maxAmount; 225: emit ChangeMaxAmount(maxAmount); 226: }
The contract's owner is the account that deploys the contract. As a result, the owner is able to perform certain privileged activities.
Openzeppelin's Ownable
used in this project implements renounceOwnership
. This can represent a certain risk if the ownership is renounced for any other reason than by design.
Renouncing ownership will leave the contract without an Owner, therefore removing any functionality that needs authority.
There are 17 instances of this issue.
File: contracts/SafEth/SafEth.sol 138: function rebalanceToWeights() external onlyOwner { 168: ) external onlyOwner { 185: ) external onlyOwner { 205: ) external onlyOwner { 214: function setMinAmount(uint256 _minAmount) external onlyOwner { 223: function setMaxAmount(uint256 _maxAmount) external onlyOwner { 232: function setPauseStaking(bool _pause) external onlyOwner { 241: function setPauseUnstaking(bool _pause) external onlyOwner { File: contracts/SafEth/derivatives/Reth.sol 58: function setMaxSlippage(uint256 _slippage) external onlyOwner { 107: function withdraw(uint256 amount) external onlyOwner { 156: function deposit() external payable onlyOwner returns (uint256) { File: contracts/SafEth/derivatives/SfrxEth.sol 51: function setMaxSlippage(uint256 _slippage) external onlyOwner { 60: function withdraw(uint256 _amount) external onlyOwner { 94: function deposit() external payable onlyOwner returns (uint256) { File: contracts/SafEth/derivatives/WstEth.sol 48: function setMaxSlippage(uint256 _slippage) external onlyOwner { 56: function withdraw(uint256 _amount) external onlyOwner { 73: function deposit() external payable onlyOwner returns (uint256) {
A single step Ownership change is risky due to the fact that the address could be wrong, so it's better to use a two - step Ownership.
There are 4 instances of this issue.
File: contracts/SafEth/SafEth.sol 9: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; File: contracts/SafEth/derivatives/Reth.sol 13: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; File: contracts/SafEth/derivatives/SfrxEth.sol 6: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; File: contracts/SafEth/derivatives/WstEth.sol 5: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
keccak256
computed more than oncekeccak256
computation produces the same result as there aren't any dynamic components: there's no need to recompute it for each function call. Consider refactoring them as constants to save gas.
There is 1 instance of this issue.
File: contracts/SafEth/derivatives/Reth.sol 69: keccak256( 70: abi.encodePacked("contract.address", "rocketTokenRETH") 71: ) 124: keccak256( 125: abi.encodePacked("contract.address", "rocketDepositPool") 126: ) 135: keccak256( 136: abi.encodePacked( 137: "contract.address", 138: "rocketDAOProtocolSettingsDeposit" 139: ) 161: keccak256( 162: abi.encodePacked("contract.address", "rocketDepositPool") 163: ) 190: keccak256( 191: abi.encodePacked("contract.address", "rocketTokenRETH") 192: ) 232: keccak256( 233: abi.encodePacked("contract.address", "rocketTokenRETH") 234: )
Events are generally emitted when sensitive changes are made to the contracts.
However, some are missing important parameters, as they should include both the new value and old value where possible.
There is 1 instance of this issue.
File: contracts/SafEth/SafEth.sol 207: emit SetMaxSlippage(_derivativeIndex, _slippage);
Follow the Solidity naming convention by adding an underscore before the function name, for any private
and internal
functions.
There are 4 instances of this issue.
File: contracts/SafEth/derivatives/Reth.sol 66: function rethAddress() private view returns (address) { 83: function swapExactInputSingleHop( 120: function poolCanDeposit(uint256 _amount) private view returns (bool) { 228: function poolPrice() private view returns (uint256) {
Locking the pragma helps avoid accidental deploys with an outdated compiler version that may introduce bugs and unexpected vulnerabilities.
Floating pragma is meant to be used for libraries and contracts that have external users and need backward compatibility.
There are 27 instances of this issue.
<details> <summary>Expand findings</summary></details>File: contracts/SafEth/SafEth.sol 2: pragma solidity ^0.8.13; File: contracts/SafEth/derivatives/Reth.sol 2: pragma solidity ^0.8.13; File: contracts/SafEth/derivatives/SfrxEth.sol 2: pragma solidity ^0.8.13; File: contracts/SafEth/derivatives/WstEth.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/IAfEthPool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/ICrvEthPool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/ICrvEthPoolLegacy.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/IFrxEthEthPool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/IFxsEthPool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/curve/IStEthEthPool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/frax/IFrxETHMinter.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/frax/IsFrxEth.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/lido/IWStETH.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/lido/IstETH.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/rocketpool/RocketDAOProtocolSettingsDepositInterface.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/rocketpool/RocketDepositPoolInterface.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/rocketpool/RocketStorageInterface.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/rocketpool/RocketTokenRETHInterface.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/ISwapRouter.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/IUniswapV3Factory.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/IUniswapV3Pool.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolActions.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolDerivedState.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolEvents.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolImmutables.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolOwnerActions.sol 2: pragma solidity ^0.8.13; File: contracts/interfaces/uniswap/pool/IUniswapV3PoolState.sol 2: pragma solidity ^0.8.13;
Some functions miss a NatSpec, or they have an incomplete one. It is recommended that contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI).
Include either @notice
and/or @dev
to describe how the function is supposed to work, a @param
to describe each parameter, and a @return
for any return values.
There are 7 instances of this issue.
File: contracts/SafEth/derivatives/Reth.sol 104: /** 105: @notice - Convert derivative into ETH 106: */ 107: function withdraw(uint256 amount) external onlyOwner { 152: /** 153: @notice - Deposit into derivative 154: @dev - will either get rETH on exchange or deposit into contract depending on availability 155: */ 156: function deposit() external payable onlyOwner returns (uint256) { File: contracts/SafEth/derivatives/SfrxEth.sol 48: /** 49: @notice - Owner only function to set max slippage for derivative 50: */ 51: function setMaxSlippage(uint256 _slippage) external onlyOwner { 90: /** 91: @notice - Owner only function to Deposit into derivative 92: @dev - Owner is set to SafEth contract 93: */ 94: function deposit() external payable onlyOwner returns (uint256) { File: contracts/SafEth/derivatives/WstEth.sol 45: /** 46: @notice - Owner only function to set max slippage for derivative 47: */ 48: function setMaxSlippage(uint256 _slippage) external onlyOwner { 52: /** 53: @notice - Owner only function to Convert derivative into ETH 54: @dev - Owner is set to SafEth contract 55: */ 56: function withdraw(uint256 _amount) external onlyOwner { 69: /** 70: @notice - Owner only function to Deposit ETH into derivative 71: @dev - Owner is set to SafEth contract 72: */ 73: function deposit() external payable onlyOwner returns (uint256) {
#0 - c4-sponsor
2023-04-10T20:58:14Z
elmutt marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T18:44:51Z
Picodes marked the issue as grade-b