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: 103/133
Findings: 1
Award: $12.84
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
12.8425 USDC - $12.84
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 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
src/frxETHMinter.sol:L43 mapping(bytes => bool) public activeValidators; // Tracks validators (via their pubkeys) that already have 32 ETH in them src/frxETHMinter.sol:L49 bool public submitPaused; src/frxETHMinter.sol:L50 bool public depositEtherPaused; src/ERC20/ERC20PermitPermissionedMint.sol:L20 mapping(address => bool) public minters; // Mapping is also used for faster verification
Caching the array length outside a loop saves reading it on each iteration, as long as the array's length is not changed during the loop.
src/OperatorRegistry.sol:L114 for (uint256 i = 0; i < original_validators.length; ++i) { src/ERC20/ERC20PermitPermissionedMint.sol:L84 for (uint i = 0; i < minters_array.length; i++){
revert()
/require()
string to save gasCustom errors are available from solidity version 0.8.4, it saves around 50 gas each time they are hit by avoiding having to allocate and store the revert string.
src/frxETHMinter.sol:L79 require(sfrxeth_recieved > 0, 'No sfrxETH was returned'); src/frxETHMinter.sol:L87 require(!submitPaused, "Submit is paused"); src/frxETHMinter.sol:L88 require(msg.value != 0, "Cannot submit 0"); src/frxETHMinter.sol:L122 require(!depositEtherPaused, "Depositing ETH is paused"); src/frxETHMinter.sol:L126 require(numDeposits > 0, "Not enough ETH in contract"); src/frxETHMinter.sol:L140 require(!activeValidators[pubKey], "Validator already has 32 ETH"); src/frxETHMinter.sol:L167 require(amount <= currentWithheldETH, "Not enough withheld ETH in contract"); src/frxETHMinter.sol:L171 require(success, "Invalid transfer"); src/frxETHMinter.sol:L193 require(success, "Invalid transfer"); src/frxETHMinter.sol:L200 require(IERC20(tokenAddress).transfer(owner, tokenAmount), "recoverERC20: Transfer failed"); src/OperatorRegistry.sol:L46 require(msg.sender == timelock_address || msg.sender == owner, "Not owner or timelock"); src/OperatorRegistry.sol:L137 require(numVals != 0, "Validator stack is empty"); src/OperatorRegistry.sol:L182 require(numValidators() == 0, "Clear validator array first"); src/OperatorRegistry.sol:L203 require(_timelock_address != address(0), "Zero address detected"); src/ERC20/ERC20PermitPermissionedMint.sol:L41 require(msg.sender == timelock_address || msg.sender == owner, "Not owner or timelock"); src/ERC20/ERC20PermitPermissionedMint.sol:L46 require(minters[msg.sender] == true, "Only minters"); src/ERC20/ERC20PermitPermissionedMint.sol:L66 require(minter_address != address(0), "Zero address detected"); src/ERC20/ERC20PermitPermissionedMint.sol:L68 require(minters[minter_address] == false, "Address already exists"); src/ERC20/ERC20PermitPermissionedMint.sol:L77 require(minter_address != address(0), "Zero address detected"); src/ERC20/ERC20PermitPermissionedMint.sol:L78 require(minters[minter_address] == true, "Address nonexistant"); src/ERC20/ERC20PermitPermissionedMint.sol:L95 require(_timelock_address != address(0), "Zero address detected");
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting.
src/frxETH.sol:L41 {} src/sfrxETH.sol:L45 {}
> 0
costs more gas than != 0
when used on a uint in a require()
statement.When working with unsigned integer types, comparisons with != 0 are cheaper than with > 0 . This changes saves 6 gas per instance.
src/frxETHMinter.sol:L79 require(sfrxeth_recieved > 0, 'No sfrxETH was returned'); src/frxETHMinter.sol:L126 require(numDeposits > 0, "Not enough ETH in contract");
Use a solidity version of at least 0.8.2 to get simple 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.
src/frxETH.sol:L2 pragma solidity ^0.8.0; src/frxETHMinter.sol:L2 pragma solidity ^0.8.0; src/sfrxETH.sol:L2 pragma solidity ^0.8.0; src/OperatorRegistry.sol:L2 pragma solidity ^0.8.0; src/lib/xERC4626.sol:L4 pragma solidity ^0.8.0; src/ERC20/ERC20PermitPermissionedMint.sol:L2 pragma solidity ^0.8.0;
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
src/frxETHMinter.sol:L159 function setWithholdRatio(uint256 newRatio) external onlyByOwnGov { src/frxETHMinter.sol:L177 function togglePauseSubmits() external onlyByOwnGov { src/frxETHMinter.sol:L184 function togglePauseDepositEther() external onlyByOwnGov { src/frxETHMinter.sol:L191 function recoverEther(uint256 amount) external onlyByOwnGov { src/frxETHMinter.sol:L199 function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyByOwnGov { src/OperatorRegistry.sol:L53 function addValidator(Validator calldata validator) public onlyByOwnGov { src/OperatorRegistry.sol:L61 function addValidators(Validator[] calldata validatorArray) external onlyByOwnGov { src/OperatorRegistry.sol:L69 function swapValidator(uint256 from_idx, uint256 to_idx) public onlyByOwnGov { src/OperatorRegistry.sol:L82 function popValidators(uint256 times) public onlyByOwnGov { src/OperatorRegistry.sol:L93 function removeValidator(uint256 remove_idx, bool dont_care_about_ordering) public onlyByOwnGov { src/OperatorRegistry.sol:L181 function setWithdrawalCredential(bytes memory _new_withdrawal_pubkey) external onlyByOwnGov { src/OperatorRegistry.sol:L190 function clearValidatorArray() external onlyByOwnGov { src/OperatorRegistry.sol:L202 function setTimelock(address _timelock_address) external onlyByOwnGov { src/ERC20/ERC20PermitPermissionedMint.sol:L53 function minter_burn_from(address b_address, uint256 b_amount) public onlyMinters { src/ERC20/ERC20PermitPermissionedMint.sol:L59 function minter_mint(address m_address, uint256 m_amount) public onlyMinters { src/ERC20/ERC20PermitPermissionedMint.sol:L65 function addMinter(address minter_address) public onlyByOwnGov { src/ERC20/ERC20PermitPermissionedMint.sol:L76 function removeMinter(address minter_address) public onlyByOwnGov { src/ERC20/ERC20PermitPermissionedMint.sol:L94 function setTimelock(address _timelock_address) public onlyByOwnGov {
X += Y
costs more gas than X = X + Y
for state variables.Consider changing X += Y
to X = X + Y
to save gas.
src/frxETHMinter.sol:L97 currentWithheldETH += withheld_amt; src/lib/xERC4626.sol:L72 storedTotalAssets += amount;
Saves 6 gas per loop.
src/ERC20/ERC20PermitPermissionedMint.sol:L84 for (uint i = 0; i < minters_array.length; i++){
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.
src/frxETHMinter.sol:L38 uint256 public constant DEPOSIT_SIZE = 32 ether; // ETH 2.0 minimum deposit size src/frxETHMinter.sol:L39 uint256 public constant RATIO_PRECISION = 1e6; // 1,000,000
Keep revert message lower than or equal to 32 bytes to save gas.
src/frxETHMinter.sol:L167 require(amount <= currentWithheldETH, "Not enough withheld ETH in contract");
Explicit initialization with zero is not required for variable declaration because uints are 0 by default. Removing this will reduce contract size and save a bit of gas.
src/frxETHMinter.sol:L94 uint256 withheld_amt = 0; src/frxETHMinter.sol:L129 for (uint256 i = 0; i < numDeposits; ++i) { src/OperatorRegistry.sol:L63 for (uint256 i = 0; i < arrayLength; ++i) { src/OperatorRegistry.sol:L84 for (uint256 i = 0; i < times; ++i) { src/OperatorRegistry.sol:L114 for (uint256 i = 0; i < original_validators.length; ++i) { src/ERC20/ERC20PermitPermissionedMint.sol:L84 for (uint i = 0; i < minters_array.length; i++){