zkSync Era - K42's results

General Information

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

zkSync

Findings Distribution

Researcher Performance

Rank: 24/24

Findings: 1

Award: $85.50

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: lsaudit

Also found by: Bauchibred, ChaseTheLight, DadeKuma, K42, Pechenite, Sathish9098, aua_oo7, hihen, oualidpro, rjs, slvDev

Labels

bug
G (Gas Optimization)
grade-b
sponsor acknowledged
G-04

Awards

85.5029 USDC - $85.50

External Links

Gas Optimization Report for ZkSync-Era by K42

  • Note: I made sure these are unique in relation to the 4naly3er report.

Possible Optimizations in L1ERC20Bridge.sol

Possible Optimization 1: Efficient Token Transfer

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.

Possible Optimization 2: Batch Withdrawal Finalization Checks

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.

Possible Optimizations in L1SharedBridge.sol

Possible Optimization 1: Efficient Storage of Finalized Withdrawals

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.

Possible Optimization 2: Batch Processing of Token Transfers

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.

Possible Optimizations in Bridgehub.sol

Possible Optimization 1: Cache Frequently Accessed Storage Variables

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.

Possible Optimization 2: Optimize Conditional Checks

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.

Possible Optimizations in Governance.sol

Possible Optimization 1: Reduce Redundant State Reads

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.

Possible Optimization 2: Batch Updates for State Variables

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.

Possible Optimizations in StateTransitionManager.sol

Possible Optimization 1: Consolidate State 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.

Possible Optimization 2: Batch Event Emissions

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.

Possible Optimizations in ValidatorTimelock.sol

Possible Optimization 1: Batch Commit Timestamp 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.

Possible Optimization 2: Simplify Validator Checks

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.

Possible Optimizations in Executor.sol

Possible Optimization 1: Reduce Redundant Storage Reads

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.

Possible Optimization 2: Optimize Log Processing Loop

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.

Possible Optimizations in Getters.sol

Possible Optimization 1: Consolidate View Functions

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.

Possible Optimization 2: Cache Frequently Accessed Storage Variables

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.

Possible Optimizations in Mailbox.sol

Possible Optimization 1: Cache Frequently Accessed Storage Variables

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.

Possible Optimization 2: Batch Processing for Proofs

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.

Possible Optimizations in BaseZkSyncUpgrade.sol

Possible Optimization 1: Batch Event Emissions

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.

Possible Optimization 2: Simplify Verifier Upgrade Logic

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.

Possible Optimizations in Diamond.sol

Possible Optimization 1: Reduce Redundant Storage Reads

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.

Possible Optimization 2: Batch Update for Facet Selectors

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.

Possible Optimizations in BootloaderUtilities.sol

Possible Optimization 1: Consolidate Common Encoding Operations

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.

Possible Optimization 2: Streamline Signature Processing

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.

Possible Optimizations in Compressor.sol

Possible Optimization 1: Optimize Data Decoding and Verification

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.

Possible Optimization 2: Efficient State Diff Compression Verification

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.

Possible Optimizations in ContractDeployer.sol

Possible Optimization 1: Streamline Deployment Process

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.

Possible Optimization 2: Optimize Account Info Storage

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.

Possible Optimizations in DefaultAccount.sol

Possible Optimization 1: Optimize Signature Verification

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.

Possible Optimization 2: Streamline Transaction Execution

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.

Possible Optimizations in L1Messenger.sol

Possible Optimization 1: Optimize Gas Cost Calculations

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.

Possible Optimization 2: Streamline Log Processing

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.

Possible Optimizations in SystemContext.sol

Possible Optimization 1: Batch Hash Storage Optimization

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.

Possible Optimization 2: Efficient L2 Block Hash Calculation

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.

Possible Optimizations in L2SharedBridge.sol

Possible Optimization 1: Efficient Token Deployment

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.

Possible Optimization 2: Streamline Finalize Deposit Logic

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter