code-423n4 / 2022-09-frax-findings

2 stars 1 forks source link

Gas Optimizations #323

Open code423n4 opened 2 years ago

code423n4 commented 2 years ago

G01: custom error save more gas

problem

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained https://blog.soliditylang.org/2021/04/21/custom-errors/. Custom errors are defined using the error statement.

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 66, b' require(minter_address != address(0), "Zero address detected");' src/ERC20/ERC20PermitPermissionedMint.sol, 68, b' require(minters[minter_address] == false, "Address already exists");' src/ERC20/ERC20PermitPermissionedMint.sol, 77, b' require(minter_address != address(0), "Zero address detected");' src/ERC20/ERC20PermitPermissionedMint.sol, 78, b' require(minters[minter_address] == true, "Address nonexistant");' src/ERC20/ERC20PermitPermissionedMint.sol, 95, b' require(_timelock_address != address(0), "Zero address detected"); ' src/frxETHMinter.sol, 79, b" require(sfrxeth_recieved > 0, 'No sfrxETH was returned');" src/frxETHMinter.sol, 87, b' require(!submitPaused, "Submit is paused");' src/frxETHMinter.sol, 88, b' require(msg.value != 0, "Cannot submit 0");' src/frxETHMinter.sol, 122, b' require(!depositEtherPaused, "Depositing ETH is paused");' src/frxETHMinter.sol, 126, b' require(numDeposits > 0, "Not enough ETH in contract");' src/frxETHMinter.sol, 140, b' require(!activeValidators[pubKey], "Validator already has 32 ETH");' src/frxETHMinter.sol, 160, b' require (newRatio <= RATIO_PRECISION, "Ratio cannot surpass 100%");' src/frxETHMinter.sol, 167, b' require(amount <= currentWithheldETH, "Not enough withheld ETH in contract");' src/frxETHMinter.sol, 171, b' require(success, "Invalid transfer");' src/frxETHMinter.sol, 193, b' require(success, "Invalid transfer");' src/frxETHMinter.sol, 200, b' require(IERC20(tokenAddress).transfer(owner, tokenAmount), "recoverERC20: Transfer failed");' src/OperatorRegistry.sol, 137, b' require(numVals != 0, "Validator stack is empty");' src/OperatorRegistry.sol, 182, b' require(numValidators() == 0, "Clear validator array first");' src/OperatorRegistry.sol, 203, b' require(_timelock_address != address(0), "Zero address detected");'

G02: COMPARISONS WITH ZERO FOR UNSIGNED INTEGERS

problem

0 is less gas efficient than !0 if you enable the optimizer at 10k AND you’re in a require statement. Detailed explanation with the opcodes https://twitter.com/gzeon/status/1485428085885640706

prof

src/frxETHMinter.sol, 79, b" require(sfrxeth_recieved > 0, 'No sfrxETH was returned');" src/frxETHMinter.sol, 126, b' require(numDeposits > 0, "Not enough ETH in contract");'

G03: PREFIX INCREMENT SAVE MORE GAS

problem

prefix increment ++i is more cheaper than postfix i++

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 84, b' for (uint i = 0; i < minters_array.length; i++){ '

G04: X += Y COSTS MORE GAS THAN X = X + Y FOR STATE VARIABLES

prof

src/frxETHMinter.sol, 97, b' currentWithheldETH += withheld_amt;' src/frxETHMinter.sol, 168, b' currentWithheldETH -= amount;' lib/ERC4626/src/xERC4626.sol, 67, b' storedTotalAssets -= amount;' lib/ERC4626/src/xERC4626.sol, 72, b' storedTotalAssets += amount;'

G05: ARRAY LENTH SHOULD USE MEMoRY VARIABLE LOAD IT

problem

The overheads outlined below are PER LOOP, excluding the first loop

storage arrays incur a Gwarmaccess (100 gas) memory arrays use MLOAD (3 gas) calldata arrays use CALLDATALOAD (3 gas) Caching the length changes each of these to a DUP (3 gas), and gets rid of the extra DUP needed to store the stack offset

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 84, b' for (uint i = 0; i < minters_array.length; i++){ '

G06: USING BOOLS FOR STORAGE INCURS OVERHEAD

problem

// 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.

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 20, b' mapping(address => bool) public minters; // Mapping is also used for faster verification' src/frxETHMinter.sol, 43, b' mapping(bytes => bool) public activeValidators; // Tracks validators (via their pubkeys) that already have 32 ETH in them'

G07: resign the default value to the variables.

problem

resign the default value to the variables will cost more gas.

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 84, b' for (uint i = 0; i < minters_array.length; i++){ ' src/frxETHMinter.sol, 94, b' uint256 withheld_amt = 0;' src/frxETHMinter.sol, 129, b' for (uint256 i = 0; i < numDeposits; ++i) {' src/OperatorRegistry.sol, 63, b' for (uint256 i = 0; i < arrayLength; ++i) {' src/OperatorRegistry.sol, 84, b' for (uint256 i = 0; i < times; ++i) {' src/OperatorRegistry.sol, 114, b' for (uint256 i = 0; i < original_validators.length; ++i) {'

G08: ++I/I++ SHOULD BE UNCHECKED{++I}/UNCHECKED{I++} WHEN IT IS NOT POSSIBLE FOR THEM TO OVERFLOW, AS IS THE CASE WHEN USED IN FOR- AND WHILE-LOOPS

problem

The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop

prof

src/ERC20/ERC20PermitPermissionedMint.sol, 84, b' for (uint i = 0; i < minters_array.length; i++){ ' src/frxETHMinter.sol, 129, b' for (uint256 i = 0; i < numDeposits; ++i) {' src/OperatorRegistry.sol, 63, b' for (uint256 i = 0; i < arrayLength; ++i) {' src/OperatorRegistry.sol, 84, b' for (uint256 i = 0; i < times; ++i) {' src/OperatorRegistry.sol, 114, b' for (uint256 i = 0; i < original_validators.length; ++i) {'

G09: FUNCTIONS GUARANTEED TO REVERT WHEN CALLED BY NORMAL USERS CAN BE MARKED PAYABLE

problem

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

prof

src/frxETHMinter.sol, 196, b' function recoverEther(uint256 amount) external onlyByOwnGov ' src/frxETHMinter.sol, 203, b' function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyByOwnGov '

G10: USING PRIVATE RATHER THAN PUBLIC FOR CONSTANTS, SAVES GAS

problem:

We can save getter function of public constants.

prof:

src/frxETHMinter.sol, 38, b' uint256 public constant DEPOSIT_SIZE = 32 ether; // ETH 2.0 minimum deposit size' src/frxETHMinter.sol, 39, b' uint256 public constant RATIO_PRECISION = 1e6; // 1,000,000 ' src/frxETHMinter.sol, 45, b' IDepositContract public immutable depositContract; // ETH 2.0 deposit contract' src/frxETHMinter.sol, 46, b' frxETH public immutable frxETHToken;' src/frxETHMinter.sol, 47, b' IsfrxETH public immutable sfrxETHToken;' lib/ERC4626/src/xERC4626.sol, 24, b' uint32 public immutable rewardsCycleLength;'

G11: USAGE OF UINTS/INTS SMALLER THAN 32 BYTES (256 BITS) INCURS OVERHEAD

problem

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.

prof

src/sfrxETH.sol, 50, b' if (block.timestamp >= rewardsCycleEnd) { syncRewards(); } ' src/sfrxETH.sol, 55, b' return convertToAssets(1e18);' src/sfrxETH.sol, 69, b' asset.permit(msg.sender, address(this), amount, deadline, v, r, s);' src/sfrxETH.sol, 85, b' asset.permit(msg.sender, address(this), amount, deadline, v, r, s);' src/ERC20/ERC20PermitPermissionedMint.sol, 54, b' super.burnFrom(b_address, b_amount);' src/ERC20/ERC20PermitPermissionedMint.sol, 60, b' super._mint(m_address, m_amount);' src/ERC20/ERC20PermitPermissionedMint.sol, 66, b' require(minter_address != address(0), "Zero address detected");' src/ERC20/ERC20PermitPermissionedMint.sol, 70, b' minters_array.push(minter_address);' src/ERC20/ERC20PermitPermissionedMint.sol, 77, b' require(minter_address != address(0), "Zero address detected");' src/ERC20/ERC20PermitPermissionedMint.sol, 84, b' for (uint i = 0; i < minters_array.length; i++){ ' src/ERC20/ERC20PermitPermissionedMint.sol, 95, b' require(_timelock_address != address(0), "Zero address detected"); ' src/frxETHMinter.sol, 72, b' _submit(address(this));' src/frxETHMinter.sol, 79, b" require(sfrxeth_recieved > 0, 'No sfrxETH was returned');" src/frxETHMinter.sol, 88, b' require(msg.value != 0, "Cannot submit 0");' src/frxETHMinter.sol, 94, b' uint256 withheld_amt = 0;' src/frxETHMinter.sol, 95, b' if (withholdRatio != 0) {' src/frxETHMinter.sol, 126, b' require(numDeposits > 0, "Not enough ETH in contract");' src/frxETHMinter.sol, 129, b' for (uint256 i = 0; i < numDeposits; ++i) {' src/OperatorRegistry.sol, 54, b' validators.push(validator);' src/OperatorRegistry.sol, 63, b' for (uint256 i = 0; i < arrayLength; ++i) {' src/OperatorRegistry.sol, 85, b' validators.pop();' src/OperatorRegistry.sol, 84, b' for (uint256 i = 0; i < times; ++i) {' src/OperatorRegistry.sol, 116, b' validators.push(original_validators[i]);' src/OperatorRegistry.sol, 114, b' for (uint256 i = 0; i < original_validators.length; ++i) {' src/OperatorRegistry.sol, 100, b' swapValidator(remove_idx, validators.length - 1);' src/OperatorRegistry.sol, 103, b' validators.pop();' src/OperatorRegistry.sol, 137, b' require(numVals != 0, "Validator stack is empty");' src/OperatorRegistry.sol, 141, b' validators.pop();' src/OperatorRegistry.sol, 176, b' return Validator(pubKey, signature, depositDataRoot);' src/OperatorRegistry.sol, 182, b' require(numValidators() == 0, "Clear validator array first");' src/OperatorRegistry.sol, 203, b' require(_timelock_address != address(0), "Zero address detected");' lib/ERC4626/src/xERC4626.sol, 24, b' uint32 public immutable rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 27, b' uint32 public lastSync;' lib/ERC4626/src/xERC4626.sol, 30, b' uint32 public rewardsCycleEnd;' lib/ERC4626/src/xERC4626.sol, 33, b' uint192 public lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 38, b' rewardsCycleLength = rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 40, b' rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 48, b' uint192 lastRewardAmount = lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 48, b' uint192 lastRewardAmount = lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 49, b' uint32 rewardsCycleEnd = rewardsCycleEnd;' lib/ERC4626/src/xERC4626.sol, 49, b' uint32 rewardsCycleEnd = rewardsCycleEnd;' lib/ERC4626/src/xERC4626.sol, 50, b' uint32 lastSync = lastSync;' lib/ERC4626/src/xERC4626.sol, 50, b' uint32 lastSync = lastSync;' lib/ERC4626/src/xERC4626.sol, 52, b' if (block.timestamp >= rewardsCycleEnd) {' lib/ERC4626/src/xERC4626.sol, 55, b' return storedTotalAssets + lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 60, b' uint256 unlockedRewards = (lastRewardAmount * (block.timestamp - lastSync)) / (rewardsCycleEnd - lastSync);' lib/ERC4626/src/xERC4626.sol, 79, b' uint192 lastRewardAmount = lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 79, b' uint192 lastRewardAmount = lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 80, b' uint32 timestamp = block.timestamp.safeCastTo32();' lib/ERC4626/src/xERC4626.sol, 80, b' uint32 timestamp = block.timestamp.safeCastTo32();' lib/ERC4626/src/xERC4626.sol, 82, b' if (timestamp < rewardsCycleEnd) revert SyncError();' lib/ERC4626/src/xERC4626.sol, 82, b' if (timestamp < rewardsCycleEnd) revert SyncError();' lib/ERC4626/src/xERC4626.sol, 85, b' uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets - lastRewardAmount;' lib/ERC4626/src/xERC4626.sol, 89, b' uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 89, b' uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 89, b' uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 89, b' uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) rewardsCycleLength;' lib/ERC4626/src/xERC4626.sol, 92, b' lastRewardAmount = nextRewards.safeCastTo192();' lib/ERC4626/src/xERC4626.sol, 93, b' lastSync = timestamp;' lib/ERC4626/src/xERC4626.sol, 94, b' rewardsCycleEnd = end;'