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: 134/246
Findings: 2
Award: $23.92
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
Number | Issues Details |
---|---|
[NC-01] | Use latest Solidity version |
[NC-02] | Use stable pragma statement |
[NC-03] | Use named imports instead of plain import FILE.SOL |
[NC-04] | Constants should be defined rather than using magic numbers |
[NC-05] | You can use named parameters in mapping types |
[L-01] | Missing events for critical parameter changes |
[L-02] | Possible division by 0 if contracts are configured improperly |
[L-03] | Possible to pass address 0 in functions that pass ownership |
Solidity pragma versioning should be upgraded to latest available version - 0.8.19
Using a floating pragma statement ^0.8.13
is discouraged as code can compile to different bytecodes with different compiler versions. Use a stable pragma statement to get a deterministic bytecode.
import FILE.SOL
Recommendation: `import {contract1, interface1} from "filename.sol";
SafEth.sol minAmount = 5 * 10 ** 17; maxAmount = 200 * 10 ** 18; preDepositPrice = 10 ** 18; underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18; preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18;
Reth.sol Define "RocketPool" as a private constant and return it in the method function name() public pure returns (string memory) { return "RocketPool"; }
uint rethPerEth = (10 ** 36) / poolPrice(); uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) * ((10 ** 18 - maxSlippage))) / 10 ** 18); (poolPrice() * 10 ** 18) / (10 ** 18); factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500) return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
WstEth.sol Define "Lido" as a private constant and return it in the method function name() public pure returns (string memory) { return "Lido"; }
return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
SfrxEth.sol
uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) * (10 ** 18 - maxSlippage)) / 10 ** 18;
Extract "Frax" to a private constant and return it in the method function name() public pure returns (string memory) { return "Frax"; }
Even though it is out of contest scope I would suggest it:
From Solidity 0.8.18 you can use named parameters in mapping types. This will make the code much more readable. SafEthStorage.sol - for example you can use named parameters here. mapping(uint256 => IDerivative) public derivatives mapping(uint256 => uint256) public weights;
Emitting events allows monitoring activities with off-chain monitoring tools. I would suggest creating custom events and emitting them on critical parameter changes. I am giving an example for a few files.
Reth.sol Create a custom event DepositSuccessful(indexed address depositor)
function deposit( uint256 rethMinted = rethBalance2 - rethBalance1; emit DepositSuccessful(rethAddress())
WstEth.sol Create a custom event DepositSuccessful(indexed address depositor) function deposit( uint256 wstEthAmount = wstEthBalancePost - wstEthBalancePre; emit DepositSuccessful(WST_ETH)
Possible division by 0 can appear contracts if they are configured improperly and cause errors. This will prevent the contract from executing. I'd recommend implementing a check for those and throwing a custom error where they are implemented, even if it is in the constructor. I am providing some examples. SafEth.sol Total weight can be 0 uint256 ethAmount = (msg.value * weight) / totalWeight; Total supply can be 0 _safEthAmount) / safEthTotalSupply;
Reth.sol Looks like poolPrice can be 0 if configured improperly if (!poolCanDeposit(msg.value)) { uint rethPerEth = (10 ** 36) / poolPrice();
There are functions that accept an address as a parameter and I recommend to add a check whether someone is passing address 0. This is not critical but mistakes can happen.
Reth.sol function initialize(address _owner) external initializer { _transferOwnership(_owner); maxSlippage = (1 * 10 ** 16); // 1% }
function swapExactInputSingleHop( address _tokenIn, address _tokenOut, uint24 _poolFee, uint256 _amountIn, uint256 _minOut ) private returns (uint256 amountOut) { IERC20(_tokenIn).approve(UNISWAP_ROUTER, _amountIn);
SafEth.sol
function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { derivatives[derivativeCount] = IDerivative(_contractAddress);
SfrxEth.sol function initialize(address _owner) external initializer { _transferOwnership(_owner); maxSlippage = (1 * 10 ** 16); // 1% }
#0 - c4-sponsor
2023-04-10T18:28:26Z
elmutt marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T17:45:23Z
Picodes marked the issue as grade-b
🌟 Selected for report: Rolezn
Also found by: 0x3b, 0xGordita, 0xSmartContract, 0xhacksmithh, 0xnev, 0xpanicError, 4lulz, Angry_Mustache_Man, ArbitraryExecution, Aymen0909, Bason, BlueAlder, EvanW, Franfran, HHK, Haipls, IgorZuk, JCN, KrisApostolov, Madalad, MiksuJak, MiniGlome, RaymondFam, ReyAdmirado, Rickard, Sathish9098, Udsen, adriro, alexzoid, anodaram, arialblack14, c3phas, carlitox477, ch0bu, chaduke, codeslide, d3e4, dicethedev, ernestognw, fatherOfBlocks, georgits, hunter_w3b, inmarelibero, lukris02, mahdirostami, maxper, pavankv, pixpi, rotcivegaf, smaul, tank, tnevler, wen, yac
10.7864 USDC - $10.79
Number | Issues Details |
---|---|
[GAS-01] | Use if statements instead of require and revert with custom errors instead of using strings |
[GAS-02] | Make some constants private |
[GAS-03] | Use calldata type instead of memory |
[GAS-04] | Compute off-chain and use directly the hashes of contract addresses if possible |
[GAS-05] | Streamline if/else statements to save gas |
[GAS-06] | Assign a local variable for for loops when comparing to state variables to save from SLOAD operations |
Even thought it is a public finding I would strongly advise to replace all require statements if statements and use custom errors. There are many places in the contract where require is being used and that can save quite a bit of gas.
SafEth.sol
require(pauseStaking == false, "staking is paused"); require(msg.value >= minAmount, "amount too low"); require(msg.value <= maxAmount, "amount too high");
require(pauseUnstaking == false, "unstaking is paused");
Reth.sol require(sent, "Failed to send Ether"); require(rethBalance2 > rethBalance1, "No rETH was minted");
SfrxEth.sol require(sent, "Failed to send Ether");
WstEth.sol require(sent, "Failed to send Ether");
Reth.sol All of these can be made private to save gas
address public constant ROCKET_STORAGE_ADDRESS = 0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46; address public constant W_ETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant UNISWAP_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; address public constant UNI_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
SfrxEth.sol address public constant SFRX_ETH_ADDRESS = 0xac3E018457B222d93114458476f3E3416Abbe38F; address public constant FRX_ETH_ADDRESS = 0x5E8422345238F34275888049021821E8E08CAa1f; address public constant FRX_ETH_CRV_POOL_ADDRESS = 0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577; address public constant FRX_ETH_MINTER_ADDRESS = 0xbAFA44EFE7901E04E39Dad13167D089C559c1138;
WstEth.sol
address public constant WST_ETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; address public constant LIDO_CRV_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; address public constant STETH_TOKEN = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
SafEth.sol Here we can use calldata type instead of memory function initialize( string memory _tokenName, string memory _tokenSymbol )
There are multiple places in the contract where we use keccak256 to hash some ABI of the contract. If this can be done off-chain and used in the contract - we will save quite a bit of gas.
Reth.sol We can pass the already generated hash of contract.address and the second parameter. There are 6 occurrences of this in this contract. RocketStorageInterface(ROCKET_STORAGE_ADDRESS).getAddress( keccak256( abi.encodePacked("contract.address", "rocketTokenRETH") ) ); address rocketDepositPoolAddress = RocketStorageInterface( ROCKET_STORAGE_ADDRESS ).getAddress( keccak256( abi.encodePacked("contract.address", "rocketDepositPool") ) ); address rocketProtocolSettingsAddress = RocketStorageInterface( ROCKET_STORAGE_ADDRESS ).getAddress( keccak256( abi.encodePacked( "contract.address", "rocketDAOProtocolSettingsDeposit" ) ) ); address rocketTokenRETHAddress = RocketStorageInterface( ROCKET_STORAGE_ADDRESS ).getAddress( keccak256( abi.encodePacked("contract.address", "rocketTokenRETH") ) ); address rocketTokenRETHAddress = RocketStorageInterface( ROCKET_STORAGE_ADDRESS ).getAddress( keccak256( abi.encodePacked("contract.address", "rocketTokenRETH") ) );
Reth.sol Change the order of the if check and check if amount is bigger or equal than the minimum deposit size of the pool. We will save calling the getBalance function and doing the addition and save gas. BEFORE: return rocketDepositPool.getBalance() + _amount <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() && _amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(); AFTER: _amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit() && rocketDepositPool.getBalance() + _amount <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() You can remove the else keyword here. BEFORE: if (poolCanDeposit(_amount)) return RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); else return (poolPrice() * 10 ** 18) / (10 ** 18); AFTER: if (poolCanDeposit(_amount)) return RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); return (poolPrice() * 10 ** 18) / (10 ** 18);
SafEth.sol Move the if (weight == 0 ) check to be immediately after declaring the variable. You will save gas from sloading from the derivatives mapping (derivatives[i]) BEFORE: for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; uint256 ethAmount = (msg.value * weight) / totalWeight;
AFTER: for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; if (weight == 0) continue; IDerivative derivative = derivatives[i]; uint256 ethAmount = (msg.value * weight) / totalWeight;
Instead of reading directly from state variables and wasting gas on SLOAD operations every time we iterate over the loop, we can assign a local variable to be the length of what we are checking against and do an SLOAD operation just once.
SafEth.sol BEFORE: for (uint i = 0; i < derivativeCount; i++) AFTER: uint derivativeCountLength = derivativeCount for (uint i = 0; i < derivativeCountLength; i++)
BEFORE: for (uint i = 0; i < derivativeCount; i++) { if (derivatives[i].balance() > 0) derivatives[i].withdraw(derivatives[i].balance()); } AFTER: uint derivativeCountLength = derivativeCount for (uint i = 0; i < derivativeCountLength; i++) { if (derivatives[i].balance() > 0) derivatives[i].withdraw(derivatives[i].balance()); }
#0 - c4-sponsor
2023-04-10T18:22:30Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-23T15:37:55Z
Picodes marked the issue as grade-b