Platform: Code4rena
Start Date: 22/09/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 133
Period: 3 days
Judge: 0xean
Total Solo HM: 2
Id: 165
League: ETH
Rank: 38/133
Findings: 2
Award: $68.71
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rotcivegaf
Also found by: 0x040, 0x1f8b, 0x4non, 0xNazgul, 0xSmartContract, 0xf15ers, 8olidity, Aymen0909, B2, Bahurum, Bnke0x0, Ch_301, CodingNameKiki, Deivitto, Diana, Funen, IllIllI, JC, JLevick, KIntern_NA, Lambda, OptimismSec, PaludoX0, RockingMiles, Rolezn, Sm4rty, Soosh, Tagir2003, Tointer, TomJ, Triangle, Trust, V_B, Waze, Yiko, __141345__, a12jmx, ajtra, asutorufos, ayeslick, aysha, bbuddha, bharg4v, bobirichman, brgltd, bytera, c3phas, cryptostellar5, cryptphi, csanuragjain, datapunk, delfin454000, durianSausage, exd0tpy, gogo, got_targ, jag, joestakey, karanctf, ladboy233, leosathya, lukris02, mics, millersplanet, natzuu, neko_nyaa, obront, oyc_109, parashar, peritoflores, rbserver, ret2basic, rokinot, ronnyx2017, rvierdiiev, sach1r0, seyni, sikorico, slowmoses, tnevler, yasir, yongskiws
30.653 USDC - $30.65
The contract VariableSupplyERC20Token have the pragma solidity directive ^0.8.0. It is recommended to specify a fixed compiler version to ensure that the bytecode produced does not vary between builds. This is especially important if you rely on bytecode-level verification of the code.
Lock the pragma.
frxETH.sol#L2 sfrxETH.sol#L2 ERC20PermitPermissionedMint.sol#L2 frxETHMinter.sol#L2 OperatorRegistry.sol#L2 xERC4626.sol#L4
It's a best practice to use the latest compiler version. The specified minimum compiler version is quite old (0.8.0). Older compilers might be susceptible to some bugs. It's recommended changing the solidity version pragma to the latest version to enforce the use of an up-to-date compiler.
A list of known compiler bugs and their severity can be found here: https://etherscan.io/solcbuginfo
To check the bugfixed and improvements of latest versions see the following link
Update the pragma to 0.8.17
frxETH.sol#L2 sfrxETH.sol#L2 ERC20PermitPermissionedMint.sol#L2 frxETHMinter.sol#L2 OperatorRegistry.sol#L2 xERC4626.sol#L4
ERC20PermitPermissionedMint.sol#L34 frxETHMinter.sol#L60-L62 OperatorRegistry.sol#L41
Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
ERC20PermitPermissionedMint.sol#L102-L106 frxETHMinter.sol#L205-L212 OperatorRegistry.sol#L208-L210 OperatorRegistry.sol#L212-L214
OperatorRegistry.sol#L82 OperatorRegistry.sol#L93 sfrxETH.sol#L54 ERC20PermitPermissionedMint.sol#L53 ERC20PermitPermissionedMint.sol#L59 ERC20PermitPermissionedMint.sol#L65 ERC20PermitPermissionedMint.sol#L76 ERC20PermitPermissionedMint.sol#L94 xERC4626.sol#L45 xERC4626.sol#L78
frxETH.sol sfrxETH.sol ERC20PermitPermissionedMint.sol frxETHMinter.sol OperatorRegistry.sol xERC4626.sol
Remove those functions that are internal and not are begin used inside the contract.
sfrxETH.sol#L48 xERC4626.sol#L65 xERC4626.sol#L71
Consider changing the variable to be an unnamed one
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Amithuddar, Aymen0909, B2, Ben, Bnke0x0, Chom, CodingNameKiki, Deivitto, Diana, Fitraldys, Funen, IllIllI, JAGADESH, JC, Metatron, Ocean_Sky, PaludoX0, Pheonix, RaymondFam, ReyAdmirado, RockingMiles, Rohan16, Rolezn, Satyam_Sharma, Sm4rty, SnowMan, SooYa, Tagir2003, TomJ, Tomio, Triangle, V_B, Waze, __141345__, ajtra, albincsergo, asutorufos, aysha, beardofginger, bobirichman, brgltd, bulej93, bytera, c3phas, ch0bu, cryptostellar5, cryptphi, d3e4, delfin454000, dharma09, drdr, durianSausage, emrekocak, erictee, fatherOfBlocks, gogo, got_targ, imare, jag, karanctf, ladboy233, leosathya, lukris02, medikko, mics, millersplanet, natzuu, neko_nyaa, oyc_109, peanuts, prasantgupta52, rbserver, ret2basic, rokinot, ronnyx2017, rotcivegaf, sach1r0, samruna, seyni, slowmoses, tnevler, wagmi, zishansami
38.0644 USDC - $38.06
++I (--I) cost less gas than I++ (I--) especially in a loop.
contract TestPost { function testPost() public { uint256 i; i++; } } contract TestPre { function testPre() public { uint256 i; ++i; } }
ERC20PermitPermissionedMint.sol#L84
Storage array length checks incur an extra Gwarmaccess (100 gas) per loop. Store the array length in a variable and use it in the for loop helps to save gas
ERC20PermitPermissionedMint.sol#L84 OperatorRegistry.sol#L114
In cases where we used a variable from state assigned to a local variable it's the same gas save.
contract TestA { uint256 _paramA; function Test () public returns (bool) { return _paramA > 0; } } contract TestB { uint256 _paramA; function Test () public returns (bool) { return _paramA != 0; } } contract TestC { uint256 _paramA; function Test () public returns (bool) { uint256 aux = _paramA; return aux > 0; } } contract TestD { uint256 _paramA; function Test () public returns (bool) { uint256 aux = _paramA; return aux != 0; } }
frxETHMinter.sol#L79 frxETHMinter.sol#L126
This is especially with state variables. In the following example I'm trying to demostrate the save gas in a loop of 10 iterations.
contract TestA { uint256 i; function Test () public { for(;i<10;){ i += 1; } } } contract TestB { uint256 i; function Test () public { for(;i<10;){ i = i + 1; } } }
frxETHMinter.sol#L97 frxETHMinter.sol#L168 xERC4626.sol#L67 xERC4626.sol#L72
Each extra memory word of bytes past the original 32 incurs an MSTORE which costs 3 gas
if (<x> == true) => if (<x>), if (<x> == false) => if (!<x>)
ERC20PermitPermissionedMint.sol#L46 ERC20PermitPermissionedMint.sol#L68 ERC20PermitPermissionedMint.sol#L78
Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hitby avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas.
ERC20PermitPermissionedMint.sol#L41 ERC20PermitPermissionedMint.sol#L46 ERC20PermitPermissionedMint.sol#L66 ERC20PermitPermissionedMint.sol#L68 ERC20PermitPermissionedMint.sol#L77 ERC20PermitPermissionedMint.sol#L78 ERC20PermitPermissionedMint.sol#L95 frxETHMinter.sol#L79 frxETHMinter.sol#L87 frxETHMinter.sol#L88 frxETHMinter.sol#L122 frxETHMinter.sol#L126 frxETHMinter.sol#L140 frxETHMinter.sol#L160 frxETHMinter.sol#L167 frxETHMinter.sol#L171 frxETHMinter.sol#L193 frxETHMinter.sol#L200 OperatorRegistry.sol#L46 OperatorRegistry.sol#L137 OperatorRegistry.sol#L182 OperatorRegistry.sol#L203
If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table.
frxETHMinter.sol#L38 frxETHMinter.sol#L39
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past.
ERC20PermitPermissionedMint.sol#L20 frxETHMinter.sol#L43 frxETHMinter.sol#L49 frxETHMinter.sol#L50
This situation ocurrs especially in loops
Example,
- for (uint256 i = 0; i < arrayLength; ++i) { + for (uint256 i = 0; i < arrayLength;) { addValidator(validatorArray[i]); + unchecked { + ++i; + } }
ERC20PermitPermissionedMint.sol#L84 frxETHMinter.sol#L129 OperatorRegistry.sol#L63 OperatorRegistry.sol#L84 OperatorRegistry.sol#L114
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address,...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
ERC20PermitPermissionedMint.sol#L84 frxETHMinter.sol#L63 frxETHMinter.sol#L64 frxETHMinter.sol#L94 frxETHMinter.sol#L129 OperatorRegistry.sol#L63 OperatorRegistry.sol#L84 OperatorRegistry.sol#L114
In the for-loop inside the function removeValidator it's accessing to the storage in every loop. Consider to operate with an auxiliar variable and after to assign it to the storage variable and avoid to write in the storage every loop.
// Save the original validators Validator[] memory original_validators = validators; + Validator[] memory auxiliar_validators; // Clear the original validators list delete validators; // Fill the new validators array with all except the value to remove for (uint256 i = 0; i < original_validators.length; ++i) { if (i != remove_idx) { - validators.push(original_validators[i]); + auxiliar_validators.push(original_validators[i]); } } + validators = auxiliar_validators;
OperatorRegistry.sol#L108-L118
It's important to avoid the accessing to the storage if it's possible.
+ address[] storage minter_array_aux = minters_array; + uint256 arrayLength = minter_array_aux.length; - for (uint i = 0; i < minters_array.length; i++){ + for (uint i = 0; i < arrayLength; i++){ - if (minters_array[i] == minter_address) { + if (minter_array_aux[i] == minter_address) { minters_array[i] = address(0); // This will leave a null in the array and keep the indices the same break; } }
Declare local variables when are going to be used if it's possible to revert before it's used. For example, variable lastRewardAmount_ in syncRewards. Move this variable to line 84 due to in line 82 there is an if that allow to revert the transaction.
When retrieving data from a memory location, assigning the data to a memory variable causes all fields of the struct/array to be read from memory, resulting in a Gcoldsload (2100 gas) for each field of the struct/array. When reading fields from new memory variables, they cause an extra MLOAD instead of a cheap stack read. Rather than declaring a variable with the memory keyword, it is much cheaper to declare a variable with the storage keyword and cache all fields that need to be read again in a stack variable, because the fields actually read will only result in a Gcoldsload. The only case where the entire struct/array is read into a memory variable is when the entire struct/array is returned by a function, passed to a function that needs memory, or when the array/struct is read from another store array/struct.
When using elements that are smaller than 32 bytes, your contract's gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. Use a larger size then downcast where needed
xERC4626.sol#L24 xERC4626.sol#L27 xERC4626.sol#L30 xERC4626.sol#L33
Use a solidity version of at least 0.8.2 to get compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value. https://github.com/ethereum/solidity/releases
frxETH.sol#L2 sfrxETH.sol#L2 ERC20PermitPermissionedMint.sol#L2 frxETHMinter.sol#L2 OperatorRegistry.sol#L2 xERC4626.sol#L4