Platform: Code4rena
Start Date: 07/03/2024
Pot Size: $250,000 USDC
Total HM: 5
Participants: 24
Period: 21 days
Judge: 0xsomeone
Total Solo HM: 3
Id: 347
League: ETH
Rank: 24/24
Findings: 1
Award: $85.50
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: lsaudit
Also found by: Bauchibred, ChaseTheLight, DadeKuma, K42, Pechenite, Sathish9098, aua_oo7, hihen, oualidpro, rjs, slvDev
85.5029 USDC - $85.50
Objective: Minimize gas costs associated with token transfers by optimizing the _depositFundsToSharedBridge
function.
Optimized Approach:
function _depositFundsToSharedBridge(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) { uint256 balanceBefore = _token.balanceOf(address(sharedBridge)); _token.safeTransferFrom(_from, address(sharedBridge), _amount); uint256 balanceAfter = _token.balanceOf(address(sharedBridge)); // Optimization: Directly return the transferred amount instead of calculating the difference. return _amount; }
Estimated Gas Saved: This optimization removes the need to calculate the balance difference, which saves gas by avoiding additional SLOAD
operations. The exact savings depend on the ERC20 token implementation and the EVM's current gas pricing for storage operations.
Objective: Reduce gas costs by optimizing the checks for withdrawal finalization.
Optimized Approach:
function finalizeWithdrawal( uint256 _l2BatchNumber, uint256 _l2MessageIndex, uint16 _l2TxNumberInBatch, bytes calldata _message, bytes32[] calldata _merkleProof ) external nonReentrant { // Optimization: Remove redundant storage read by checking finalization status in the shared bridge. require(!sharedBridge.isWithdrawalFinalized(_l2BatchNumber, _l2MessageIndex), "Withdrawal already finalized"); (address l1Receiver, address l1Token, uint256 amount) = sharedBridge.finalizeWithdrawalLegacyErc20Bridge( _l2BatchNumber, _l2MessageIndex, _l2TxNumberInBatch, _message, _merkleProof ); emit WithdrawalFinalized(l1Receiver, l1Token, amount); }
Estimated Gas Saved: This approach reduces gas consumption by eliminating the need for a separate mapping to track withdrawal finalization status, relying instead on the shared bridge's tracking. The savings depend on the frequency of withdrawal finalizations and the cost of storage reads.
Objective: Reduce gas costs associated with marking withdrawals as finalized by optimizing storage access patterns.
Optimized Approach:
// Use a single storage slot to track the finalization status of multiple withdrawals by encoding them into a bitmap. mapping(uint256 => mapping(uint256 => uint256)) public withdrawalFinalizationBitmap; function isWithdrawalFinalized(uint256 _chainId, uint256 _l2BatchNumber, uint256 _l2MessageNumber) public view returns (bool) { uint256 batchBitmap = withdrawalFinalizationBitmap[_chainId][_l2BatchNumber]; uint256 mask = 1 << _l2MessageNumber; return batchBitmap & mask != 0; } function _setWithdrawalFinalized(uint256 _chainId, uint256 _l2BatchNumber, uint256 _l2MessageNumber) internal { uint256 mask = 1 << _l2MessageNumber; withdrawalFinalizationBitmap[_chainId][_l2BatchNumber] |= mask; }
Estimated Gas Saved: This optimization significantly reduces the number of storage slots needed to track withdrawal finalizations, leading to lower gas costs for updates. The exact savings depend on the number of withdrawals being tracked and the gas price.
Objective: Minimize gas costs and improve efficiency for token transfers from the legacy bridge to the shared bridge.
Optimized Approach:
function batchTransferTokensFromLegacy(address[] calldata _tokens, address _target, uint256 _targetChainId) external onlyOwner { for (uint i = 0; i < _tokens.length; i++) { address _token = _tokens[i]; uint256 amount; if (_token == ETH_TOKEN_ADDRESS) { amount = address(this).balance; (bool success, ) = _target.call{value: amount}(""); require(success, "ShB: ETH transfer failed"); } else { uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); IL1ERC20Bridge(_target).tranferTokenToSharedBridge(_token, IERC20(_token).balanceOf(address(legacyBridge))); uint256 balanceAfter = IERC20(_token).balanceOf(address(this)); amount = balanceAfter - balanceBefore; } chainBalance[_targetChainId][_token] += amount; } }
Estimated Gas Saved: By processing multiple token transfers in a single transaction, this approach reduces the overhead associated with transaction initiation and execution. The savings are more pronounced when transferring multiple tokens in one call compared to individual transactions.
Objective: Reduce gas costs associated with repeated storage reads by caching frequently accessed storage variables in memory.
Optimized Approach:
function requestL2TransactionDirect( L2TransactionRequestDirect calldata _request ) external payable override nonReentrant returns (bytes32 canonicalTxHash) { // Cache base token address to reduce storage reads address token = baseToken[_request.chainId]; if (token == ETH_TOKEN_ADDRESS) { require(msg.value == _request.mintValue, "Bridgehub: msg.value mismatch 1"); } else { require(msg.value == 0, "Bridgehub: non-eth bridge with msg.value"); // Proceed with the deposit to the shared bridge } // The rest of the function remains unchanged... }
Estimated Gas Saved: This optimization reduces the gas cost by minimizing the number of SLOAD operations required to read the base token address for each chain ID. The exact savings depend on the frequency of these operations within transaction execution paths.
Objective: Improve the efficiency of conditional checks by restructuring conditions and reducing redundant checks.
Optimized Approach:
function requestL2TransactionTwoBridges(L2TransactionRequestTwoBridgesOuter calldata _request) external payable override nonReentrant returns (bytes32 canonicalTxHash) { address token = baseToken[_request.chainId]; uint256 baseTokenMsgValue; if (token == ETH_TOKEN_ADDRESS) { require(msg.value == _request.mintValue + _request.secondBridgeValue, "Bridgehub: msg.value mismatch 2"); baseTokenMsgValue = _request.mintValue; } else { require(msg.value == _request.secondBridgeValue, "Bridgehub: msg.value mismatch 3"); baseTokenMsgValue = 0; } // Simplify the deposit logic by directly using the calculated baseTokenMsgValue sharedBridge.bridgehubDepositBaseToken{value: baseTokenMsgValue}( _request.chainId, msg.sender, token, _request.mintValue ); // The rest of the function remains unchanged... }
Estimated Gas Saved: This optimization streamlines the conditional logic for handling ETH and ERC20 token deposits, potentially reducing the execution cost by avoiding redundant condition evaluations.
Objective: Minimize redundant reads from state variables by caching their values in memory when they are accessed multiple times within a function.
Code Snippet:
function execute(Operation calldata _operation) external payable onlyOwnerOrSecurityCouncil { bytes32 id = hashOperation(_operation); _checkPredecessorDone(_operation.predecessor); require(isOperationReady(id), "Operation must be ready before execution"); _execute(_operation.calls); // No need to check if operation is ready after execution since it's set to EXECUTED_PROPOSAL_TIMESTAMP timestamps[id] = EXECUTED_PROPOSAL_TIMESTAMP; emit OperationExecuted(id); }
Optimized Approach:
function execute(Operation calldata _operation) external payable onlyOwnerOrSecurityCouncil { bytes32 id = hashOperation(_operation); _checkPredecessorDone(_operation.predecessor); bool operationReady = isOperationReady(id); require(operationReady, "Operation must be ready before execution"); _execute(_operation.calls); timestamps[id] = EXECUTED_PROPOSAL_TIMESTAMP; emit OperationExecuted(id); }
Estimated Gas Saved: This optimization reduces the gas cost by avoiding a redundant check after the execution of an operation. The savings are minor but contribute to more efficient contract execution.
Objective: When multiple state variables need to be updated together, perform these updates in a single function to minimize the number of transactions and reduce gas costs.
Code Snippet:
function updateSecurityCouncil(address _newSecurityCouncil) external onlySelf { emit ChangeSecurityCouncil(securityCouncil, _newSecurityCouncil); securityCouncil = _newSecurityCouncil; } function updateDelay(uint256 _newDelay) external onlySelf { emit ChangeMinDelay(minDelay, _newDelay); minDelay = _newDelay; }
Optimized Approach:
function updateSettings(address _newSecurityCouncil, uint256 _newDelay) external onlySelf { if (securityCouncil != _newSecurityCouncil) { emit ChangeSecurityCouncil(securityCouncil, _newSecurityCouncil); securityCouncil = _newSecurityCouncil; } if (minDelay != _newDelay) { emit ChangeMinDelay(minDelay, _newDelay); minDelay = _newDelay; } }
Estimated Gas Saved: Consolidating updates into a single transaction can significantly reduce gas costs, especially when multiple settings need to be updated frequently. The exact savings depend on the frequency and nature of these updates.
Objective: Minimize the number of state updates by consolidating related state changes into a single function call where possible.
Code Snippet:
function setValidatorTimelock(address _validatorTimelock) external onlyOwnerOrAdmin { validatorTimelock = _validatorTimelock; } function setInitialCutHash(Diamond.DiamondCutData calldata _diamondCut) external onlyOwner { initialCutHash = keccak256(abi.encode(_diamondCut)); }
Optimized Approach:
function updateSettings(address _validatorTimelock, Diamond.DiamondCutData calldata _diamondCut) external onlyOwnerOrAdmin { validatorTimelock = _validatorTimelock; initialCutHash = keccak256(abi.encode(_diamondCut)); emit ValidatorTimelockUpdated(_validatorTimelock); emit InitialCutHashUpdated(initialCutHash); }
Estimated Gas Saved: This optimization reduces the gas cost by combining multiple state updates into a single transaction. The exact savings depend on the frequency and nature of these updates but can be significant over time.
Objective: Reduce the gas cost associated with emitting multiple events by batching related event emissions where feasible.
Code Snippet:
function setPendingAdmin(address _newPendingAdmin) external onlyOwnerOrAdmin { address oldPendingAdmin = pendingAdmin; pendingAdmin = _newPendingAdmin; emit NewPendingAdmin(oldPendingAdmin, _newPendingAdmin); } function acceptAdmin() external { address currentPendingAdmin = pendingAdmin; require(msg.sender == currentPendingAdmin, "n42"); address previousAdmin = admin; admin = currentPendingAdmin; delete pendingAdmin; emit NewPendingAdmin(currentPendingAdmin, address(0)); emit NewAdmin(previousAdmin, pendingAdmin); }
Optimized Approach:
function updateAdmin(address _newAdmin) external onlyOwnerOrAdmin { require(_newAdmin != address(0), "Invalid new admin"); address oldAdmin = admin; address oldPendingAdmin = pendingAdmin; admin = _newAdmin; pendingAdmin = address(0); // Optionally reset pendingAdmin if applicable emit AdminUpdated(oldAdmin, _newAdmin, oldPendingAdmin); }
Estimated Gas Saved: By emitting a single event after updating multiple related state variables, this optimization can save gas compared to emitting multiple events. The savings are more pronounced in contracts with frequent administrative updates.
Objective: Reduce the gas cost associated with updating committed batch timestamps by minimizing storage operations.
Code Snippet:
for (uint256 i = 0; i < _newBatchesData.length; ++i) { committedBatchTimestamp[ERA_CHAIN_ID].set(_newBatchesData[i].batchNumber, timestamp); }
Optimized Approach:
// Assuming LibMap.Uint32Map supports batch operations function commitBatchesSharedBridge( uint256 _chainId, StoredBatchInfo calldata, CommitBatchInfo[] calldata _newBatchesData ) external onlyValidator(_chainId) { uint32 timestamp = uint32(block.timestamp); committedBatchTimestamp[_chainId].setBatch(_newBatchesData, timestamp); _propagateToZkSyncStateTransition(_chainId); }
Estimated Gas Saved: This optimization could significantly reduce gas costs by leveraging batch operations to update multiple entries in a single transaction. The exact savings depend on the number of batches being committed simultaneously.
Objective: Streamline the process of checking if an address is a validator to reduce gas costs.
Code Snippet:
require(validators[_chainId][msg.sender] == true, "ValidatorTimelock: only validator");
Optimized Approach:
// Assuming a more efficient data structure or caching mechanism is used modifier onlyValidator(uint256 _chainId) { require(isValidator(_chainId, msg.sender), "ValidatorTimelock: only validator"); _; } function isValidator(uint256 _chainId, address _validator) internal view returns (bool) { // Implement caching or a more efficient check mechanism return validators[_chainId][_validator]; }
Estimated Gas Saved: While the direct gas savings from optimizing the validator check might be minor, reducing the complexity and cost of storage lookups can lead to improved performance, especially in contracts with frequent validator interactions.
Objective: Minimize gas costs associated with redundant reads from storage by caching frequently accessed storage variables.
Optimized Approach:
function _commitBatches( StoredBatchInfo memory _lastCommittedBatchData, CommitBatchInfo[] calldata _newBatchesData ) internal { // Cache protocol version and total batches committed to reduce storage reads uint256 protocolVersion = s.protocolVersion; uint256 totalBatchesCommitted = s.totalBatchesCommitted; require( IStateTransitionManager(s.stateTransitionManager).protocolVersion() == protocolVersion, "Executor facet: wrong protocol version" ); require(_newBatchesData.length == 1, "e4"); require(s.storedBatchHashes[totalBatchesCommitted] == _hashStoredBatchInfo(_lastCommittedBatchData), "i"); // Rest of the function remains unchanged... }
Estimated Gas Saved: This optimization reduces the gas cost by minimizing the number of SLOAD operations required to read the protocol version and the total number of batches committed. The exact savings depend on the frequency of these operations within transaction execution paths.
Objective: Improve the efficiency of log processing by reducing the overhead associated with loop operations and conditional checks.
Optimized Approach:
function _processL2Logs( CommitBatchInfo calldata _newBatch, bytes32 _expectedSystemContractUpgradeTxHash ) internal pure returns (LogProcessingOutput memory logOutput) { bytes memory emittedL2Logs = _newBatch.systemLogs; uint256 processedLogs = 0; // Pre-calculate the loop iteration count to avoid recalculating it every iteration uint256 logCount = emittedL2Logs.length / L2_TO_L1_LOG_SERIALIZE_SIZE; for (uint256 i = 0; i < logCount; ++i) { uint256 offset = i * L2_TO_L1_LOG_SERIALIZE_SIZE; // Process log based on offset... // Rest of the loop body remains unchanged... } // Rest of the function remains unchanged... }
Estimated Gas Saved: This optimization reduces the computational overhead associated with loop control and conditional checks by pre-calculating the number of iterations required. This can lead to significant gas savings, especially for batches with a large number of logs.
Objective: Reduce the number of external calls by consolidating related view functions into single calls that return multiple values.
Code Snippet:
function getVerifier() external view returns (address) { return address(s.verifier); } function getAdmin() external view returns (address) { return s.admin; }
Optimized Approach:
function getSystemAddresses() external view returns (address verifier, address admin, address bridgehub) { verifier = address(s.verifier); admin = s.admin; bridgehub = address(s.bridgehub); }
Estimated Gas Saved: This optimization can reduce gas costs for clients that need to query multiple pieces of information by reducing the number of external calls required. The exact savings depend on the frequency and pattern of these queries.
Objective: Minimize gas costs associated with reading state variables by caching frequently accessed values in memory.
Code Snippet:
function getTotalBatchesCommitted() external view returns (uint256) { return s.totalBatchesCommitted; } function getTotalBatchesVerified() external view returns (uint256) { return s.totalBatchesVerified; }
Optimized Approach:
// Assuming these values change infrequently, cache them and update the cache on mutation function getTotalBatchCounts() external view returns (uint256 totalBatchesCommitted, uint256 totalBatchesVerified, uint256 totalBatchesExecuted) { totalBatchesCommitted = s.totalBatchesCommitted; totalBatchesVerified = s.totalBatchesVerified; totalBatchesExecuted = s.totalBatchesExecuted; }
Estimated Gas Saved: By reducing the number of external calls needed to fetch related data, this optimization can lead to gas savings, especially for off-chain clients that need to retrieve multiple state variables in a single transaction.
Objective: Reduce gas costs associated with frequently accessing storage variables by caching them in memory.
Code Snippet:
function transferEthToSharedBridge() external onlyBaseTokenBridge { uint256 amount = address(this).balance; address sharedBridgeAddress = s.baseTokenBridge; IL1SharedBridge(sharedBridgeAddress).receiveEth{value: amount}(ERA_CHAIN_ID); }
Optimized Approach:
function transferEthToSharedBridgeOptimized() external onlyBaseTokenBridge { uint256 amount = address(this).balance; // Cache the sharedBridgeAddress in memory to avoid multiple storage reads address sharedBridgeAddress = s.baseTokenBridge; IL1SharedBridge(sharedBridgeAddress).receiveEth{value: amount}(ERA_CHAIN_ID); }
Estimated Gas Saved: This optimization may not directly save a significant amount of gas since it's already optimized in terms of storage reads. However, ensuring that storage variables are not read multiple times unnecessarily in other functions can lead to gas savings.
Objective: Optimize the processing of multiple proofs by batching them to reduce the gas cost per proof.
Code Snippet:
function proveL2MessageInclusion( uint256 _batchNumber, uint256 _index, L2Message memory _message, bytes32[] calldata _proof ) public view returns (bool) { return _proveL2LogInclusion(_batchNumber, _index, _L2MessageToLog(_message), _proof); }
Optimized Approach:
// Introduce a batch processing function for multiple message inclusions function proveMultipleL2MessageInclusions( uint256[] calldata _batchNumbers, uint256[] calldata _indexes, L2Message[] calldata _messages, bytes32[][] calldata _proofs ) external view returns (bool[] memory results) { require(_batchNumbers.length == _indexes.length && _indexes.length == _messages.length && _messages.length == _proofs.length, "Mismatched array lengths"); results = new bool[](_batchNumbers.length); for (uint256 i = 0; i < _batchNumbers.length; i++) { results[i] = _proveL2LogInclusion(_batchNumbers[i], _indexes[i], _L2MessageToLog(_messages[i]), _proofs[i]); } return results; }
Estimated Gas Saved: Batching operations can significantly reduce the gas cost per operation due to shared overhead. The exact savings depend on the number of proofs processed in a batch and the efficiency of batch processing.
Objective: Reduce the gas cost associated with emitting multiple events by batching related event emissions into a single event.
Code Snippet:
emit NewL2BootloaderBytecodeHash(previousBootloaderBytecodeHash, _l2BootloaderBytecodeHash); emit NewL2DefaultAccountBytecodeHash(previousDefaultAccountBytecodeHash, _l2DefaultAccountBytecodeHash); emit NewVerifier(address(oldVerifier), address(_newVerifier)); emit NewVerifierParams(oldVerifierParams, _newVerifierParams);
Optimized Approach:
event UpgradeSummary( bytes32 indexed previousBootloaderBytecodeHash, bytes32 indexed newBootloaderBytecodeHash, bytes32 indexed previousDefaultAccountBytecodeHash, bytes32 newDefaultAccountBytecodeHash, address oldVerifier, address newVerifier, VerifierParams oldVerifierParams, VerifierParams newVerifierParams ); function emitUpgradeSummary( bytes32 _previousBootloaderBytecodeHash, bytes32 _newBootloaderBytecodeHash, bytes32 _previousDefaultAccountBytecodeHash, bytes32 _newDefaultAccountBytecodeHash, address _oldVerifier, address _newVerifier, VerifierParams memory _oldVerifierParams, VerifierParams memory _newVerifierParams ) internal { emit UpgradeSummary( _previousBootloaderBytecodeHash, _newBootloaderBytecodeHash, _previousDefaultAccountBytecodeHash, _newDefaultAccountBytecodeHash, _oldVerifier, _newVerifier, _oldVerifierParams, _newVerifierParams); }
Estimated Gas Saved: This optimization can reduce gas costs by minimizing the overhead associated with logging multiple events. The exact savings depend on the number of optimizations applied and the frequency of upgrade operations.
Objective: Streamline the verifier upgrade process to minimize redundant checks and simplify the logic.
Code Snippet:
function _upgradeVerifier(address _newVerifier, VerifierParams calldata _verifierParams) internal { _setVerifier(IVerifier(_newVerifier)); _setVerifierParams(_verifierParams); }
Optimized Approach:
function _upgradeVerifierOptimized(address _newVerifier, VerifierParams calldata _verifierParams) internal { if (_newVerifier != address(0) && _newVerifier != address(s.verifier)) { IVerifier oldVerifier = s.verifier; s.verifier = IVerifier(_newVerifier); emit NewVerifier(address(oldVerifier), _newVerifier); } if (_verifierParams.recursionNodeLevelVkHash != bytes32(0) || _verifierParams.recursionLeafLevelVkHash != bytes32(0) || _verifierParams.recursionCircuitsSetVksHash != bytes32(0)) { VerifierParams memory oldVerifierParams = s.verifierParams; s.verifierParams = _verifierParams; emit NewVerifierParams(oldVerifierParams, _verifierParams); } }
Estimated Gas Saved: This optimization can save gas by avoiding unnecessary function calls and checks, especially when the new verifier address or verifier parameters do not change. The savings will vary based on the complexity of the upgrade and the frequency of verifier updates.
Objective: Minimize the number of reads from storage by caching frequently accessed storage variables.
Code Snippet:
function _addFunctions(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { DiamondStorage storage ds = getDiamondStorage(); require(_facet.code.length > 0, "G"); _saveFacetIfNew(_facet); uint256 selectorsLength = _selectors.length; for (uint256 i = 0; i < selectorsLength; i = i.uncheckedInc()) { bytes4 selector = _selectors[i]; SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; require(oldFacet.facetAddress == address(0), "J"); // facet for this selector already exists _addOneFunction(_facet, selector, _isFacetFreezable); } }
Optimized Approach:
function _addFunctionsOptimized(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { DiamondStorage storage ds = getDiamondStorage(); require(_facet.code.length > 0, "G"); bool isNewFacet = _saveFacetIfNew(_facet); if (isNewFacet) { for (uint256 i = 0; i < _selectors.length; i++) { _addOneFunction(_facet, _selectors[i], _isFacetFreezable); } } else { // If facet is not new, perform checks and add functions for (uint256 i = 0; i < _selectors.length; i++) { bytes4 selector = _selectors[i]; require(ds.selectorToFacet[selector].facetAddress == address(0), "J"); // facet for this selector already exists _addOneFunction(_facet, selector, _isFacetFreezable); } } }
Estimated Gas Saved: This optimization reduces the number of redundant storage reads, especially in scenarios where multiple selectors are added to a new facet. The exact gas savings depend on the number of selectors processed.
Objective: Optimize the addition or replacement of selectors for a facet by performing batch updates.
Code Snippet:
function _replaceFunctions(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { DiamondStorage storage ds = getDiamondStorage(); require(_facet.code.length > 0, "K"); uint256 selectorsLength = _selectors.length; for (uint256 i = 0; i < selectorsLength; i = i.uncheckedInc()) { bytes4 selector = _selectors[i]; SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; require(oldFacet.facetAddress != address(0), "L"); _removeOneFunction(oldFacet.facetAddress, selector); _saveFacetIfNew(_facet); _addOneFunction(_facet, selector, _isFacetFreezable); } }
Optimized Approach:
function _replaceFunctionsBatch(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { DiamondStorage storage ds = getDiamondStorage(); require(_facet.code.length > 0, "K"); // Perform batch removal first for (uint256 i = 0; i < _selectors.length; i++) { bytes4 selector = _selectors[i]; SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; require(oldFacet.facetAddress != address(0), "L"); // Ensure the selector exists _removeOneFunction(oldFacet.facetAddress, selector); } // Then batch add bool isNewFacet = _saveFacetIfNew(_facet); for (uint256 i = 0; i < _selectors.length; i++) { _addOneFunction(_facet, _selectors[i], _isFacetFreezable); } }
Estimated Gas Saved: By batching the removal and addition of selectors, this approach can reduce the overhead associated with updating the DiamondStorage
mappings and arrays. The gas savings would be more noticeable with a larger number of selectors being replaced at once.
Objective: Reduce redundancy in encoding operations for transaction parameters that are common across different transaction types.
Code Snippet:
The original contract contains separate functions for encoding different transaction types, many of which perform similar encoding operations for common fields such as nonce
, gasLimit
, to
, and value
.
Optimized Approach:
function encodeCommonTransactionFields(Transaction calldata _transaction) internal pure returns (bytes memory encodedFields) { bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); encodedFields = bytes.concat(encodedNonce, encodedGasLimit, encodedTo, encodedValue); }
Estimated Gas Saved: This optimization reduces the gas cost by eliminating redundant encoding operations for each transaction type. The exact savings depend on the frequency of transaction hash computations and the complexity of the transactions.
Objective: Optimize the processing of transaction signatures to reduce computational overhead.
Code Snippet: The original contract decodes and processes the signature for each transaction type separately.
Optimized Approach:
function encodeSignature(bytes memory signature) internal pure returns (bytes memory encodedR, bytes memory encodedS, bytes memory encodedV) { uint256 r; uint256 s; uint8 v; (r, s, v) = splitSignature(signature); encodedR = RLPEncoder.encodeUint256(r); encodedS = RLPEncoder.encodeUint256(s); encodedV = RLPEncoder.encodeUint256(uint256(v)); } function splitSignature(bytes memory signature) internal pure returns (uint256 r, uint256 s, uint8 v) { require(signature.length == 65, "Invalid signature length"); assembly { r := mload(add(signature, 32)) s := mload(add(signature, 64)) v := byte(0, mload(add(signature, 96))) } }
Estimated Gas Saved: This approach centralizes and simplifies signature processing, potentially reducing gas costs associated with multiple signature decoding operations. The savings would be more significant in contracts that frequently compute transaction hashes.
Objective: Streamline the decoding and verification process for compressed data to reduce redundant operations and improve gas efficiency.
Code Snippet: The original contract performs multiple decoding and verification steps that can be optimized for better performance.
Optimized Approach:
function publishCompressedBytecodeOptimized( bytes calldata _bytecode, bytes calldata _rawCompressedData ) external payable onlyCallFromBootloader returns (bytes32 bytecodeHash) { (bytes calldata dictionary, bytes calldata encodedData) = _decodeRawBytecode(_rawCompressedData); require( encodedData.length * 4 == _bytecode.length, "Encoded data length mismatch" ); for (uint256 i = 0; i < encodedData.length; i += 2) { uint256 dictionaryIndex = uint256(encodedData.readUint16(i)) * 8; bytes8 encodedChunk = dictionary.readBytes8(dictionaryIndex); bytes8 originalChunk = _bytecode.readBytes8(i * 4); require(encodedChunk == originalChunk, "Chunk mismatch"); } bytecodeHash = keccak256(_bytecode); emit BytecodePublished(bytecodeHash); }
Estimated Gas Saved: This optimization reduces the number of required operations by directly comparing chunks of the original bytecode with the decoded chunks from the dictionary, potentially saving a significant amount of gas for each verification. The exact savings depend on the size of the bytecode and the efficiency of the _decodeRawBytecode
function.
Objective: Improve the efficiency of state diff compression verification by optimizing data handling and comparisons.
Code Snippet: The original contract's method for verifying compressed state diffs involves multiple steps that can be optimized.
Optimized Approach:
function verifyCompressedStateDiffsOptimized( uint256 _numberOfStateDiffs, bytes calldata _stateDiffs, bytes calldata _compressedStateDiffs ) external payable onlyCallFrom(address(L1_MESSENGER_CONTRACT)) returns (bool verified) { // Assume _decodeCompressedStateDiffs is an optimized version of decoding that returns structured data (StructuredStateDiff[] memory decodedStateDiffs) = _decodeCompressedStateDiffs(_compressedStateDiffs); for (uint256 i = 0; i < _numberOfStateDiffs; i++) { StructuredStateDiff memory decodedDiff = decodedStateDiffs[i]; StateDiff memory originalDiff = _decodeStateDiff(_stateDiffs, i); require( decodedDiff.derivedKey == originalDiff.derivedKey && _verifyOperation(decodedDiff.operation, originalDiff.initValue, originalDiff.finalValue, decodedDiff.compressedValue), "State diff verification failed" ); } verified = true; }
Estimated Gas Saved: By structuring the decoded state diffs and optimizing the verification logic, this approach can significantly reduce the computational overhead and gas costs associated with verifying each state diff. The savings will vary based on the number and complexity of the state diffs being processed.
Objective: Reduce the overhead associated with contract deployment by optimizing the bytecode verification and construction process.
Optimized Approach:
function optimizedDeployAccount( bytes32 _bytecodeHash, bytes calldata _input, AccountAbstractionVersion _aaVersion ) external payable onlySystemCall returns (address) { require(_bytecodeHash != bytes32(0), "BytecodeHash cannot be zero"); address newAddress = calculateNewAddress(_bytecodeHash, _input); require(isAddressAvailable(newAddress), "Address is occupied or invalid"); // Directly perform deployment without separate known bytecode check if hash is pre-validated performDeployment(newAddress, _bytecodeHash, _input, _aaVersion); return newAddress; }
Estimated Gas Saved: This optimization combines several checks and operations into a single function call, potentially reducing the gas cost by minimizing the number of external contract calls and storage operations. The exact savings depend on the complexity of the bytecode and the frequency of deployment operations.
Objective: Improve the efficiency of storing and updating account information to reduce gas costs associated with account version and nonce ordering updates.
Optimized Approach:
function optimizedUpdateAccountInfo( address _address, AccountAbstractionVersion _version, AccountNonceOrdering _nonceOrdering ) external onlySystemCall { AccountInfo storage info = accountInfo[_address]; bool needsUpdate = false; if (_version != AccountAbstractionVersion.None && info.supportedAAVersion != _version) { info.supportedAAVersion = _version; needsUpdate = true; } if (_nonceOrdering == AccountNonceOrdering.Arbitrary && info.nonceOrdering != _nonceOrdering) { info.nonceOrdering = _nonceOrdering; needsUpdate = true; } if (needsUpdate) { emit AccountInfoUpdated(_address, _version, _nonceOrdering); } }
Estimated Gas Saved: By conditionally updating account information only when changes are necessary, this approach can significantly reduce unnecessary storage operations, leading to gas savings. The savings will vary based on the number of accounts being updated and the frequency of updates.
Objective: Reduce the gas cost of signature verification by optimizing the _isValidSignature
function.
Optimized Approach:
function optimizedIsValidSignature(bytes32 _hash, bytes memory _signature) internal pure returns (bool) { // Directly use ecrecover without intermediate variables for r, s, and v (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); address recoveredAddress = ecrecover(_hash, v, r, s); return recoveredAddress != address(0) && recoveredAddress == address(this); } function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Invalid signature length"); assembly { // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } // Adjust for Ethereum's v value if (v < 27) v += 27; require(v == 27 || v == 28, "Invalid v value"); }
Estimated Gas Saved: This optimization simplifies the signature splitting and validation process, potentially saving gas by reducing the number of operations and memory allocations. The exact savings depend on the frequency of transactions and signature verifications.
Objective: Minimize gas consumption during transaction execution by optimizing the _execute
function.
Optimized Approach:
function optimizedExecute(Transaction calldata _transaction) internal { // Simplify the call to EfficientCall.rawCall by removing unnecessary checks (bool success,) = address(uint160(_transaction.to)).call{value: _transaction.value, gas: gasleft()}(_transaction.data); require(success, "Transaction execution failed"); }
Estimated Gas Saved: This approach reduces the overhead associated with transaction execution by directly using Solidity's call
function, avoiding additional checks and operations. The savings will vary based on the complexity of the transactions being executed.
Objective: Reduce the gas cost of calculating gas costs for keccak and sha256 operations by optimizing the keccakGasCost
and sha256GasCost
functions.
Optimized Approach:
// Simplify gas cost calculations by using inline assembly for division and multiplication, reducing overhead. function optimizedKeccakGasCost(uint256 _length) internal pure returns (uint256) { assembly { let rounds := add(div(_length, KECCAK_ROUND_NUMBER_OF_BYTES), 1) mul(rounds, KECCAK_ROUND_GAS_COST) } } function optimizedSha256GasCost(uint256 _length) internal pure returns (uint256) { assembly { let rounds := add(div(add(_length, 8), SHA256_ROUND_NUMBER_OF_BYTES), 1) mul(rounds, SHA256_ROUND_GAS_COST) } }
Estimated Gas Saved: This optimization reduces the computational overhead associated with gas cost calculations, potentially saving gas by utilizing assembly's efficient arithmetic operations. The exact savings depend on the frequency and size of the data being hashed.
Objective: Minimize gas consumption during the processing of L2 to L1 logs by optimizing the _processL2ToL1Log
function.
Optimized Approach:
// Optimize log processing by reducing redundant keccak256 computations and streamlining chained hash updates. function optimizedProcessL2ToL1Log(L2ToL1Log memory _l2ToL1Log) internal returns (uint256 logIdInMerkleTree) { bytes32 hashedLog = keccak256(abi.encode(_l2ToL1Log)); // Directly update the chainedLogsHash without intermediate variables chainedLogsHash = keccak256(abi.encodePacked(chainedLogsHash, hashedLog)); logIdInMerkleTree = numberOfLogsToProcess++; // Emit event with optimized log structure emit L2ToL1LogSent(_l2ToL1Log.l2ShardId, _l2ToL1Log.isService, _l2ToL1Log.txNumberInBlock, _l2ToL1Log.sender, _l2ToL1Log.key, _l2ToL1Log.value); }
Estimated Gas Saved: This approach reduces the gas cost associated with log processing by eliminating unnecessary memory allocations and optimizing hash chain updates. The savings will vary based on the number and size of the logs being processed.
Objective: Reduce gas costs associated with storing batch hashes by optimizing the storage pattern.
Optimized Approach:
// Use a more gas-efficient storage pattern for batch hashes. function _setBatchHash(uint256 _batchNumber, bytes32 _hash) internal { uint256 index = _batchNumber % MINIBATCH_HASHES_TO_STORE; batchHashes[index] = _hash; } function getBatchHash(uint256 _batchNumber) external view returns (bytes32) { uint256 index = _batchNumber % MINIBATCH_HASHES_TO_STORE; return batchHashes[index]; }
Estimated Gas Saved: This optimization reduces the storage access cost by minimizing the number of storage slots used for batch hashes. The exact savings depend on the frequency of batch hash updates and queries.
Objective: Minimize gas costs associated with calculating L2 block hashes by optimizing the hash calculation process.
Optimized Approach:
// Optimize L2 block hash calculation by reducing redundant computations. function _calculateL2BlockHashOptimized( uint128 _blockNumber, uint128 _blockTimestamp, bytes32 _prevL2BlockHash ) internal view returns (bytes32) { // Assuming currentL2BlockTxsRollingHash is updated elsewhere efficiently return keccak256(abi.encode(_blockNumber, _blockTimestamp, _prevL2BlockHash, currentL2BlockTxsRollingHash)); }
Estimated Gas Saved: This approach reduces the computational overhead by assuming that currentL2BlockTxsRollingHash
is efficiently updated elsewhere in the contract, thus saving gas on each L2 block hash calculation.
Objective: Minimize gas costs associated with deploying L2 tokens for L1 counterparts by optimizing the _deployL2Token
function.
Optimized Approach:
// Use minimal proxy pattern for deploying L2 tokens to reduce deployment gas costs. function optimizedDeployL2Token(address _l1Token, bytes calldata _data) internal returns (address) { bytes32 salt = _getCreate2Salt(_l1Token); bytes memory bytecode = abi.encodePacked( type(MinimalProxy).creationCode, abi.encode(address(l2TokenBeacon), _data) ); bytes32 bytecodeHash = keccak256(bytecode); return address(new MinimalProxy{salt: salt}(bytecodeHash)); }
Estimated Gas Saved: This optimization leverages the minimal proxy pattern (EIP-1167) to significantly reduce the gas cost of deploying new token contracts. The exact savings depend on the complexity of the token contracts being deployed.
Objective: Reduce gas consumption during the finalization of deposits by optimizing the finalizeDeposit
function.
Optimized Approach:
// Reduce redundant storage reads and simplify logic in finalizeDeposit. function optimizedFinalizeDeposit( address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes calldata _data ) external payable override { require(AddressAliasHelper.undoL1ToL2Alias(msg.sender) == l1Bridge, "Unauthorized"); address expectedL2Token = l2TokenAddress(_l1Token); if (l1TokenAddress[expectedL2Token] == address(0)) { address deployedToken = optimizedDeployL2Token(_l1Token, _data); require(deployedToken == expectedL2Token, "Token mismatch"); l1TokenAddress[expectedL2Token] = _l1Token; } IL2StandardToken(expectedL2Token).bridgeMint(_l2Receiver, _amount); emit FinalizeDeposit(_l1Sender, _l2Receiver, expectedL2Token, _amount); }
Estimated Gas Saved: This approach reduces the number of storage reads and simplifies the conditional logic, potentially saving a significant amount of gas, especially when finalizing deposits for tokens that have already been deployed.
#0 - c4-sponsor
2024-04-18T02:11:57Z
saxenism (sponsor) acknowledged
#1 - saxenism
2024-04-18T02:11:58Z
Good efforts. However most of the suggestions cannot be implemented.
#2 - c4-judge
2024-04-29T13:48:06Z
alex-ppg marked the issue as grade-b